mirror of
https://github.com/SchildiChat/SchildiChat-android.git
synced 2025-02-16 20:10:04 +03:00
Changed Encryption algorithm of 4S
This commit is contained in:
parent
e2e4ddf5ba
commit
0064934db9
9 changed files with 297 additions and 113 deletions
|
@ -16,26 +16,25 @@
|
|||
|
||||
package im.vector.matrix.android.internal.crypto.ssss
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import im.vector.matrix.android.InstrumentedTest
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec
|
||||
import im.vector.matrix.android.api.session.securestorage.EncryptedSecretContent
|
||||
import im.vector.matrix.android.api.session.securestorage.KeySigner
|
||||
import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
|
||||
import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
|
||||
import im.vector.matrix.android.api.util.Optional
|
||||
import im.vector.matrix.android.common.CommonTestHelper
|
||||
import im.vector.matrix.android.common.SessionTestParams
|
||||
import im.vector.matrix.android.common.TestConstants
|
||||
import im.vector.matrix.android.common.TestMatrixCallback
|
||||
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
|
||||
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService
|
||||
import im.vector.matrix.android.internal.crypto.tools.withOlmDecryption
|
||||
import im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
|
@ -71,7 +70,7 @@ class QuadSTests : InstrumentedTest {
|
|||
|
||||
val TEST_KEY_ID = "my.test.Key"
|
||||
|
||||
val ssssKeyCreationInfo = mTestHelper.doSync<SsssKeyCreationInfo> {
|
||||
mTestHelper.doSync<SsssKeyCreationInfo> {
|
||||
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it)
|
||||
}
|
||||
|
||||
|
@ -95,16 +94,9 @@ class QuadSTests : InstrumentedTest {
|
|||
assertNotNull("Key should be stored in account data", accountData)
|
||||
val parsed = SecretStorageKeyContent.fromJson(accountData!!.content)
|
||||
assertNotNull("Key Content cannot be parsed", parsed)
|
||||
assertEquals("Unexpected Algorithm", SSSS_ALGORITHM_CURVE25519_AES_SHA2, parsed!!.algorithm)
|
||||
assertEquals("Unexpected Algorithm", SSSS_ALGORITHM_AES_HMAC_SHA2, parsed!!.algorithm)
|
||||
assertEquals("Unexpected key name", "Test Key", parsed.name)
|
||||
assertNull("Key was not generated from passphrase", parsed.passphrase)
|
||||
assertNotNull("Pubkey should be defined", parsed.publicKey)
|
||||
|
||||
val privateKeySpec = Curve25519AesSha2KeySpec.fromRecoveryKey(ssssKeyCreationInfo.recoveryKey)
|
||||
val pubKey = withOlmDecryption { olmPkDecryption ->
|
||||
olmPkDecryption.setPrivateKey(privateKeySpec!!.privateKey)
|
||||
}
|
||||
assertEquals("Unexpected Public Key", pubKey, parsed.publicKey)
|
||||
|
||||
// Set as default key
|
||||
quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback<Unit> {})
|
||||
|
@ -137,13 +129,15 @@ class QuadSTests : InstrumentedTest {
|
|||
val keyId = "My.Key"
|
||||
val info = generatedSecret(aliceSession, keyId, true)
|
||||
|
||||
val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey)
|
||||
|
||||
// Store a secret
|
||||
val clearSecret = Base64.encodeToString("42".toByteArray(), Base64.NO_PADDING or Base64.NO_WRAP)
|
||||
val clearSecret = "42".toByteArray().toBase64NoPadding()
|
||||
mTestHelper.doSync<Unit> {
|
||||
aliceSession.sharedSecretStorageService.storeSecret(
|
||||
"secret.of.life",
|
||||
clearSecret,
|
||||
null, // default key
|
||||
listOf(Pair<String?, SsssKeySpec?>(null, keySpec)), // default key
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@ -157,14 +151,13 @@ class QuadSTests : InstrumentedTest {
|
|||
val secret = EncryptedSecretContent.fromJson(encryptedContent?.get(keyId))
|
||||
assertNotNull(secret?.ciphertext)
|
||||
assertNotNull(secret?.mac)
|
||||
assertNotNull(secret?.ephemeral)
|
||||
assertNotNull(secret?.initializationVector)
|
||||
|
||||
// Try to decrypt??
|
||||
|
||||
val keySpec = Curve25519AesSha2KeySpec.fromRecoveryKey(info.recoveryKey)
|
||||
|
||||
val decryptedSecret = mTestHelper.doSync<String> {
|
||||
aliceSession.sharedSecretStorageService.getSecret("secret.of.life",
|
||||
aliceSession.sharedSecretStorageService.getSecret(
|
||||
"secret.of.life",
|
||||
null, // default key
|
||||
keySpec!!,
|
||||
it
|
||||
|
@ -209,7 +202,10 @@ class QuadSTests : InstrumentedTest {
|
|||
aliceSession.sharedSecretStorageService.storeSecret(
|
||||
"my.secret",
|
||||
mySecretText.toByteArray().toBase64NoPadding(),
|
||||
listOf(keyId1, keyId2),
|
||||
listOf(
|
||||
keyId1 to RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey),
|
||||
keyId2 to RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)
|
||||
),
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@ -226,7 +222,7 @@ class QuadSTests : InstrumentedTest {
|
|||
mTestHelper.doSync<String> {
|
||||
aliceSession.sharedSecretStorageService.getSecret("my.secret",
|
||||
keyId1,
|
||||
Curve25519AesSha2KeySpec.fromRecoveryKey(key1Info.recoveryKey)!!,
|
||||
RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@ -234,7 +230,7 @@ class QuadSTests : InstrumentedTest {
|
|||
mTestHelper.doSync<String> {
|
||||
aliceSession.sharedSecretStorageService.getSecret("my.secret",
|
||||
keyId2,
|
||||
Curve25519AesSha2KeySpec.fromRecoveryKey(key2Info.recoveryKey)!!,
|
||||
RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!,
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@ -255,7 +251,7 @@ class QuadSTests : InstrumentedTest {
|
|||
aliceSession.sharedSecretStorageService.storeSecret(
|
||||
"my.secret",
|
||||
mySecretText.toByteArray().toBase64NoPadding(),
|
||||
listOf(keyId1),
|
||||
listOf(keyId1 to RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)),
|
||||
it
|
||||
)
|
||||
}
|
||||
|
@ -264,7 +260,7 @@ class QuadSTests : InstrumentedTest {
|
|||
var error = false
|
||||
aliceSession.sharedSecretStorageService.getSecret("my.secret",
|
||||
keyId1,
|
||||
Curve25519AesSha2KeySpec.fromPassphrase(
|
||||
RawBytesKeySpec.fromPassphrase(
|
||||
"A bad passphrase",
|
||||
key1Info.content?.passphrase?.salt ?: "",
|
||||
key1Info.content?.passphrase?.iterations ?: 0,
|
||||
|
@ -289,7 +285,7 @@ class QuadSTests : InstrumentedTest {
|
|||
mTestHelper.doSync<String> {
|
||||
aliceSession.sharedSecretStorageService.getSecret("my.secret",
|
||||
keyId1,
|
||||
Curve25519AesSha2KeySpec.fromPassphrase(
|
||||
RawBytesKeySpec.fromPassphrase(
|
||||
passphrase,
|
||||
key1Info.content?.passphrase?.salt ?: "",
|
||||
key1Info.content?.passphrase?.iterations ?: 0,
|
||||
|
|
|
@ -32,7 +32,8 @@ data class EncryptedSecretContent(
|
|||
/** unpadded base64-encoded ciphertext */
|
||||
@Json(name = "ciphertext") val ciphertext: String? = null,
|
||||
@Json(name = "mac") val mac: String? = null,
|
||||
@Json(name = "ephemeral") val ephemeral: String? = null
|
||||
@Json(name = "ephemeral") val ephemeral: String? = null,
|
||||
@Json(name = "iv") val initializationVector: String? = null
|
||||
) : AccountDataContent {
|
||||
companion object {
|
||||
/**
|
||||
|
|
|
@ -27,5 +27,6 @@ sealed class SharedSecretStorageError(message: String?) : Throwable(message) {
|
|||
|
||||
object BadKeyFormat : SharedSecretStorageError("Bad Key Format")
|
||||
object ParsingError : SharedSecretStorageError("parsing Error")
|
||||
object BadMac : SharedSecretStorageError("Bad mac")
|
||||
data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage)
|
||||
}
|
||||
|
|
|
@ -42,7 +42,7 @@ interface SharedSecretStorageService {
|
|||
*/
|
||||
fun generateKey(keyId: String,
|
||||
keyName: String,
|
||||
keySigner: KeySigner,
|
||||
keySigner: KeySigner?,
|
||||
callback: MatrixCallback<SsssKeyCreationInfo>)
|
||||
|
||||
/**
|
||||
|
@ -92,7 +92,7 @@ interface SharedSecretStorageService {
|
|||
* @param secret The secret contents.
|
||||
* @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret.
|
||||
*/
|
||||
fun storeSecret(name: String, secretBase64: String, keys: List<String>?, callback: MatrixCallback<Unit>)
|
||||
fun storeSecret(name: String, secretBase64: String, keys: List<Pair<String?, SsssKeySpec?>>, callback: MatrixCallback<Unit>)
|
||||
|
||||
/**
|
||||
* Use this call to determine which SSSSKeySpec to use for requesting secret
|
||||
|
@ -104,7 +104,7 @@ interface SharedSecretStorageService {
|
|||
*
|
||||
* @param name The name of the secret
|
||||
* @param keyId The id of the key that should be used to decrypt (null for default key)
|
||||
* @param secretKey the secret key to use (@see #Curve25519AesSha2KeySpec)
|
||||
* @param secretKey the secret key to use (@see #RawBytesKeySpec)
|
||||
*
|
||||
*/
|
||||
fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback<String>)
|
||||
|
|
|
@ -23,14 +23,14 @@ import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyF
|
|||
/** Tag class */
|
||||
interface SsssKeySpec
|
||||
|
||||
data class Curve25519AesSha2KeySpec(
|
||||
data class RawBytesKeySpec(
|
||||
val privateKey: ByteArray
|
||||
) : SsssKeySpec {
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromPassphrase(passphrase: String, salt: String, iterations: Int, progressListener: ProgressListener?): Curve25519AesSha2KeySpec {
|
||||
return Curve25519AesSha2KeySpec(
|
||||
fun fromPassphrase(passphrase: String, salt: String, iterations: Int, progressListener: ProgressListener?): RawBytesKeySpec {
|
||||
return RawBytesKeySpec(
|
||||
privateKey = deriveKey(
|
||||
passphrase,
|
||||
salt,
|
||||
|
@ -40,9 +40,9 @@ data class Curve25519AesSha2KeySpec(
|
|||
)
|
||||
}
|
||||
|
||||
fun fromRecoveryKey(recoveryKey: String): Curve25519AesSha2KeySpec? {
|
||||
fun fromRecoveryKey(recoveryKey: String): RawBytesKeySpec? {
|
||||
return extractCurveKeyFromRecoveryKey(recoveryKey)?.let {
|
||||
Curve25519AesSha2KeySpec(
|
||||
RawBytesKeySpec(
|
||||
privateKey = it
|
||||
)
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ data class Curve25519AesSha2KeySpec(
|
|||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as Curve25519AesSha2KeySpec
|
||||
other as RawBytesKeySpec
|
||||
|
||||
if (!privateKey.contentEquals(other.privateKey)) return false
|
||||
|
||||
|
|
|
@ -35,6 +35,8 @@ const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-
|
|||
* Secured Shared Storage algorithm constant
|
||||
*/
|
||||
const val SSSS_ALGORITHM_CURVE25519_AES_SHA2 = "m.secret_storage.v1.curve25519-aes-sha2"
|
||||
/* Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. **/
|
||||
const val SSSS_ALGORITHM_AES_HMAC_SHA2 = "m.secret_storage.v1.aes-hmac-sha2"
|
||||
|
||||
// TODO Refacto: use this constants everywhere
|
||||
const val ed25519 = "ed25519"
|
||||
|
|
|
@ -17,38 +17,42 @@
|
|||
package im.vector.matrix.android.internal.crypto.secrets
|
||||
|
||||
import im.vector.matrix.android.api.MatrixCallback
|
||||
import im.vector.matrix.android.api.extensions.orFalse
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.accountdata.AccountDataService
|
||||
import im.vector.matrix.android.api.session.events.model.toContent
|
||||
import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec
|
||||
import im.vector.matrix.android.api.session.securestorage.EncryptedSecretContent
|
||||
import im.vector.matrix.android.api.session.securestorage.IntegrityResult
|
||||
import im.vector.matrix.android.api.session.securestorage.KeyInfo
|
||||
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
|
||||
import im.vector.matrix.android.api.session.securestorage.KeySigner
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssPassphrase
|
||||
import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
|
||||
import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent
|
||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageError
|
||||
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec
|
||||
import im.vector.matrix.android.api.session.securestorage.SsssPassphrase
|
||||
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
|
||||
import im.vector.matrix.android.internal.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.fromBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.generatePrivateKeyWithPassword
|
||||
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey
|
||||
import im.vector.matrix.android.internal.crypto.tools.HkdfSha256
|
||||
import im.vector.matrix.android.internal.crypto.tools.withOlmDecryption
|
||||
import im.vector.matrix.android.internal.crypto.tools.withOlmEncryption
|
||||
import im.vector.matrix.android.internal.extensions.foldToCallback
|
||||
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.matrix.olm.OlmPkMessage
|
||||
import java.security.SecureRandom
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.IvParameterSpec
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import javax.inject.Inject
|
||||
|
||||
private data class Key(
|
||||
val publicKey: String,
|
||||
@Suppress("ArrayInDataClass")
|
||||
val privateKey: ByteArray
|
||||
)
|
||||
import kotlin.experimental.and
|
||||
|
||||
internal class DefaultSharedSecretStorageService @Inject constructor(
|
||||
private val accountDataService: AccountDataService,
|
||||
|
@ -58,14 +62,12 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
|
||||
override fun generateKey(keyId: String,
|
||||
keyName: String,
|
||||
keySigner: KeySigner,
|
||||
keySigner: KeySigner?,
|
||||
callback: MatrixCallback<SsssKeyCreationInfo>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
val key = try {
|
||||
withOlmDecryption { olmPkDecryption ->
|
||||
val pubKey = olmPkDecryption.generateKey()
|
||||
val privateKey = olmPkDecryption.privateKey()
|
||||
Key(pubKey, privateKey)
|
||||
ByteArray(32).also {
|
||||
SecureRandom().nextBytes(it)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
|
@ -74,12 +76,11 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
|
||||
val storageKeyContent = SecretStorageKeyContent(
|
||||
name = keyName,
|
||||
algorithm = SSSS_ALGORITHM_CURVE25519_AES_SHA2,
|
||||
passphrase = null,
|
||||
publicKey = key.publicKey
|
||||
algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2,
|
||||
passphrase = null
|
||||
)
|
||||
|
||||
val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let {
|
||||
val signedContent = keySigner?.sign(storageKeyContent.canonicalSignable())?.let {
|
||||
storageKeyContent.copy(
|
||||
signatures = it
|
||||
)
|
||||
|
@ -97,7 +98,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
callback.onSuccess(SsssKeyCreationInfo(
|
||||
keyId = keyId,
|
||||
content = storageKeyContent,
|
||||
recoveryKey = computeRecoveryKey(key.privateKey)
|
||||
recoveryKey = computeRecoveryKey(key)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
@ -114,19 +115,9 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener)
|
||||
|
||||
val pubKey = try {
|
||||
withOlmDecryption { olmPkDecryption ->
|
||||
olmPkDecryption.setPrivateKey(privatePart.privateKey)
|
||||
}
|
||||
} catch (failure: Throwable) {
|
||||
callback.onFailure(failure)
|
||||
return@launch
|
||||
}
|
||||
|
||||
val storageKeyContent = SecretStorageKeyContent(
|
||||
algorithm = SSSS_ALGORITHM_CURVE25519_AES_SHA2,
|
||||
passphrase = SsssPassphrase(algorithm = "m.pbkdf2", iterations = privatePart.iterations, salt = privatePart.salt),
|
||||
publicKey = pubKey
|
||||
algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2,
|
||||
passphrase = SsssPassphrase(algorithm = "m.pbkdf2", iterations = privatePart.iterations, salt = privatePart.salt)
|
||||
)
|
||||
|
||||
val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let {
|
||||
|
@ -189,24 +180,19 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
return getKey(keyId)
|
||||
}
|
||||
|
||||
override fun storeSecret(name: String, secretBase64: String, keys: List<String>?, callback: MatrixCallback<Unit>) {
|
||||
override fun storeSecret(name: String, secretBase64: String, keys: List<Pair<String?, SsssKeySpec?>>, callback: MatrixCallback<Unit>) {
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
val encryptedContents = HashMap<String, EncryptedSecretContent>()
|
||||
try {
|
||||
if (keys.isNullOrEmpty()) {
|
||||
// use default key
|
||||
when (val key = getDefaultKey()) {
|
||||
keys.forEach {
|
||||
val keyId = it.first
|
||||
// encrypt the content
|
||||
when (val key = keyId?.let { getKey(keyId) } ?: getDefaultKey()) {
|
||||
is KeyInfoResult.Success -> {
|
||||
if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_CURVE25519_AES_SHA2) {
|
||||
val encryptedResult = withOlmEncryption { olmEncrypt ->
|
||||
olmEncrypt.setRecipientKey(key.keyInfo.content.publicKey)
|
||||
olmEncrypt.encrypt(secretBase64)
|
||||
if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_AES_HMAC_SHA2) {
|
||||
encryptAesHmacSha2(it.second!!, name, secretBase64).let {
|
||||
encryptedContents[key.keyInfo.id] = it
|
||||
}
|
||||
encryptedContents[key.keyInfo.id] = EncryptedSecretContent(
|
||||
ciphertext = encryptedResult.mCipherText,
|
||||
ephemeral = encryptedResult.mEphemeralKey,
|
||||
mac = encryptedResult.mMac
|
||||
)
|
||||
} else {
|
||||
// Unknown algorithm
|
||||
callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: ""))
|
||||
|
@ -218,34 +204,6 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
return@launch
|
||||
}
|
||||
}
|
||||
} else {
|
||||
keys.forEach {
|
||||
val keyId = it
|
||||
// encrypt the content
|
||||
when (val key = getKey(keyId)) {
|
||||
is KeyInfoResult.Success -> {
|
||||
if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_CURVE25519_AES_SHA2) {
|
||||
val encryptedResult = withOlmEncryption { olmEncrypt ->
|
||||
olmEncrypt.setRecipientKey(key.keyInfo.content.publicKey)
|
||||
olmEncrypt.encrypt(secretBase64)
|
||||
}
|
||||
encryptedContents[keyId] = EncryptedSecretContent(
|
||||
ciphertext = encryptedResult.mCipherText,
|
||||
ephemeral = encryptedResult.mEphemeralKey,
|
||||
mac = encryptedResult.mMac
|
||||
)
|
||||
} else {
|
||||
// Unknown algorithm
|
||||
callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: ""))
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
is KeyInfoResult.Error -> {
|
||||
callback.onFailure(key.error)
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
accountDataService.updateAccountData(
|
||||
|
@ -259,8 +217,107 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
callback.onFailure(failure)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Add default key
|
||||
/**
|
||||
* Encrytion algorithm m.secret_storage.v1.aes-hmac-sha2
|
||||
* Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. The data is encrypted and MACed as follows:
|
||||
*
|
||||
* Given the secret storage key, generate 64 bytes by performing an HKDF with SHA-256 as the hash, a salt of 32 bytes of 0, and with the secret name as the info.
|
||||
*
|
||||
* The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
|
||||
*
|
||||
* Generate 16 random bytes, set bit 63 to 0 (in order to work around differences in AES-CTR implementations), and use this as the AES initialization vector.
|
||||
* This becomes the iv property, encoded using base64.
|
||||
*
|
||||
* Encrypt the data using AES-CTR-256 using the AES key generated above.
|
||||
*
|
||||
* This encrypted data, encoded using base64, becomes the ciphertext property.
|
||||
*
|
||||
* Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above.
|
||||
* The resulting MAC is base64-encoded and becomes the mac property.
|
||||
* (We use AES-CTR to match file encryption and key exports.)
|
||||
*/
|
||||
@Throws
|
||||
private fun encryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, clearDataBase64: String): EncryptedSecretContent {
|
||||
secretKey as RawBytesKeySpec
|
||||
val pseudoRandomKey = HkdfSha256.deriveSecret(
|
||||
secretKey.privateKey,
|
||||
ByteArray(32) { 0.toByte() },
|
||||
secretName.toByteArray(),
|
||||
64)
|
||||
|
||||
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
|
||||
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
|
||||
val macKey = pseudoRandomKey.copyOfRange(32, 64)
|
||||
|
||||
val secureRandom = SecureRandom()
|
||||
val iv = ByteArray(16)
|
||||
secureRandom.nextBytes(iv)
|
||||
|
||||
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
|
||||
// (which would mean we wouldn't be able to decrypt on Android). The loss
|
||||
// of a single bit of salt is a price we have to pay.
|
||||
iv[9] = iv[9] and 0x7f
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||
|
||||
val secretKeySpec = SecretKeySpec(aesKey, "AES")
|
||||
val ivParameterSpec = IvParameterSpec(iv)
|
||||
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||
// secret are not that big, just do Final
|
||||
val cipherBytes = cipher.doFinal(clearDataBase64.fromBase64NoPadding())
|
||||
require(cipherBytes.isNotEmpty())
|
||||
|
||||
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
||||
val mac = Mac.getInstance("HmacSHA256")
|
||||
mac.init(macKeySpec)
|
||||
val digest = mac.doFinal(cipherBytes)
|
||||
|
||||
return EncryptedSecretContent(
|
||||
ciphertext = cipherBytes.toBase64NoPadding(),
|
||||
initializationVector = iv.toBase64NoPadding(),
|
||||
mac = digest.toBase64NoPadding()
|
||||
)
|
||||
}
|
||||
|
||||
private fun decryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, cipherContent: EncryptedSecretContent): String {
|
||||
secretKey as RawBytesKeySpec
|
||||
val pseudoRandomKey = HkdfSha256.deriveSecret(
|
||||
secretKey.privateKey,
|
||||
ByteArray(32) { 0.toByte() },
|
||||
secretName.toByteArray(),
|
||||
64)
|
||||
|
||||
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
|
||||
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
|
||||
val macKey = pseudoRandomKey.copyOfRange(32, 64)
|
||||
|
||||
val iv = cipherContent.initializationVector?.fromBase64NoPadding() ?: ByteArray(16)
|
||||
|
||||
val cipherRawBytes = cipherContent.ciphertext!!.fromBase64NoPadding()
|
||||
|
||||
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
|
||||
|
||||
val secretKeySpec = SecretKeySpec(aesKey, "AES")
|
||||
val ivParameterSpec = IvParameterSpec(iv)
|
||||
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
|
||||
// secret are not that big, just do Final
|
||||
val decryptedSecret = cipher.doFinal(cipherRawBytes)
|
||||
|
||||
require(decryptedSecret.isNotEmpty())
|
||||
|
||||
// Check Signature
|
||||
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
|
||||
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
|
||||
val digest = mac.doFinal(cipherRawBytes)
|
||||
|
||||
if (!cipherContent.mac?.fromBase64NoPadding()?.contentEquals(digest).orFalse()) {
|
||||
throw SharedSecretStorageError.BadMac
|
||||
} else {
|
||||
// we are good
|
||||
return decryptedSecret.toBase64NoPadding()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
|
||||
|
@ -300,7 +357,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
|
||||
val algorithm = key.keyInfo.content
|
||||
if (SSSS_ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) {
|
||||
val keySpec = secretKey as? Curve25519AesSha2KeySpec ?: return Unit.also {
|
||||
val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also {
|
||||
callback.onFailure(SharedSecretStorageError.BadKeyFormat)
|
||||
}
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
|
@ -318,6 +375,15 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
}
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
} else if (SSSS_ALGORITHM_AES_HMAC_SHA2 == algorithm.algorithm) {
|
||||
val keySpec = secretKey as? RawBytesKeySpec ?: return Unit.also {
|
||||
callback.onFailure(SharedSecretStorageError.BadKeyFormat)
|
||||
}
|
||||
cryptoCoroutineScope.launch(coroutineDispatchers.main) {
|
||||
kotlin.runCatching {
|
||||
decryptAesHmacSha2(keySpec, name, secretContent)
|
||||
}.foldToCallback(callback)
|
||||
}
|
||||
} else {
|
||||
callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: ""))
|
||||
}
|
||||
|
@ -343,7 +409,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
|
|||
val keyInfo = (keyInfoResult as? KeyInfoResult.Success)?.keyInfo
|
||||
?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: ""))
|
||||
|
||||
if (keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) {
|
||||
if (keyInfo.content.algorithm != SSSS_ALGORITHM_AES_HMAC_SHA2
|
||||
|| keyInfo.content.algorithm != SSSS_ALGORITHM_CURVE25519_AES_SHA2) {
|
||||
// Unsupported algorithm
|
||||
return IntegrityResult.Error(
|
||||
SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "")
|
||||
|
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright (c) 2020 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
/*
|
||||
* Copyright (C) 2015 Square, Inc.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package im.vector.matrix.android.internal.crypto.tools
|
||||
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.nio.ByteBuffer
|
||||
import javax.crypto.Mac
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import kotlin.math.ceil
|
||||
|
||||
/**
|
||||
* HMAC-based Extract-and-Expand Key Derivation Function (HkdfSha256)
|
||||
* [RFC-5869] https://tools.ietf.org/html/rfc5869
|
||||
*/
|
||||
object HkdfSha256 {
|
||||
|
||||
public fun deriveSecret(inputKeyMaterial: ByteArray, salt: ByteArray?, info: ByteArray, outputLength: Int): ByteArray {
|
||||
return expand(extract(salt, inputKeyMaterial), info, outputLength)
|
||||
}
|
||||
|
||||
/**
|
||||
* HkdfSha256-Extract(salt, IKM) -> PRK
|
||||
*
|
||||
* @param salt optional salt value (a non-secret random value);
|
||||
* if not provided, it is set to a string of HashLen (size in octets) zeros.
|
||||
* @param ikm input keying material
|
||||
*/
|
||||
private fun extract(salt: ByteArray?, ikm: ByteArray): ByteArray {
|
||||
val mac = initMac(salt ?: ByteArray(HASH_LEN) { 0.toByte() })
|
||||
return mac.doFinal(ikm)
|
||||
}
|
||||
|
||||
/**
|
||||
* HkdfSha256-Expand(PRK, info, L) -> OKM
|
||||
*
|
||||
* @param prk a pseudorandom key of at least HashLen bytes (usually, the output from the extract step)
|
||||
* @param info optional context and application specific information (can be empty)
|
||||
* @param outputLength length of output keying material in bytes (<= 255*HashLen)
|
||||
* @return OKM output keying material
|
||||
*/
|
||||
private fun expand(prk: ByteArray, info: ByteArray = ByteArray(0), outputLength: Int): ByteArray {
|
||||
require(outputLength <= 255 * HASH_LEN) { "outputLength must be less than or equal to 255*HashLen" }
|
||||
|
||||
/*
|
||||
The output OKM is calculated as follows:
|
||||
Notation | -> When the message is composed of several elements we use concatenation (denoted |) in the second argument;
|
||||
|
||||
|
||||
N = ceil(L/HashLen)
|
||||
T = T(1) | T(2) | T(3) | ... | T(N)
|
||||
OKM = first L octets of T
|
||||
|
||||
where:
|
||||
T(0) = empty string (zero length)
|
||||
T(1) = HMAC-Hash(PRK, T(0) | info | 0x01)
|
||||
T(2) = HMAC-Hash(PRK, T(1) | info | 0x02)
|
||||
T(3) = HMAC-Hash(PRK, T(2) | info | 0x03)
|
||||
...
|
||||
*/
|
||||
val n = ceil(outputLength.toDouble() / HASH_LEN.toDouble()).toInt()
|
||||
|
||||
var stepHash = ByteArray(0) // T(0) empty string (zero length)
|
||||
|
||||
val generatedBytes = ByteArrayOutputStream() // ByteBuffer.allocate(Math.multiplyExact(n, HASH_LEN))
|
||||
val mac = initMac(prk)
|
||||
for (roundNum in 1..n) {
|
||||
mac.reset()
|
||||
val t = ByteBuffer.allocate(stepHash.size + info.size + 1).apply {
|
||||
put(stepHash)
|
||||
put(info)
|
||||
put(roundNum.toByte())
|
||||
}
|
||||
stepHash = mac.doFinal(t.array())
|
||||
generatedBytes.write(stepHash)
|
||||
}
|
||||
|
||||
return generatedBytes.toByteArray().sliceArray(0 until outputLength)
|
||||
}
|
||||
|
||||
private fun initMac(secret: ByteArray): Mac {
|
||||
val mac = Mac.getInstance(HASH_ALG)
|
||||
mac.init(SecretKeySpec(secret, HASH_ALG))
|
||||
return mac
|
||||
}
|
||||
|
||||
private const val HASH_LEN = 32
|
||||
private const val HASH_ALG = "HmacSHA256"
|
||||
}
|
|
@ -24,7 +24,7 @@ import com.squareup.inject.assisted.Assisted
|
|||
import com.squareup.inject.assisted.AssistedInject
|
||||
import im.vector.matrix.android.api.listeners.ProgressListener
|
||||
import im.vector.matrix.android.api.session.Session
|
||||
import im.vector.matrix.android.api.session.securestorage.Curve25519AesSha2KeySpec
|
||||
import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
|
||||
import im.vector.matrix.android.api.session.securestorage.IntegrityResult
|
||||
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
|
||||
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
|
||||
|
@ -99,7 +99,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
|
|||
isIndeterminate = true
|
||||
)
|
||||
))
|
||||
val keySpec = Curve25519AesSha2KeySpec.fromPassphrase(
|
||||
val keySpec = RawBytesKeySpec.fromPassphrase(
|
||||
passphrase,
|
||||
keyInfo.content.passphrase?.salt ?: "",
|
||||
keyInfo.content.passphrase?.iterations ?: 0,
|
||||
|
|
Loading…
Add table
Reference in a new issue