Changed Encryption algorithm of 4S

This commit is contained in:
Valere 2020-02-20 18:31:05 +01:00 committed by Benoit Marty
parent e2e4ddf5ba
commit 0064934db9
9 changed files with 297 additions and 113 deletions

View File

@ -16,26 +16,25 @@
package im.vector.matrix.android.internal.crypto.ssss package im.vector.matrix.android.internal.crypto.ssss
import android.util.Base64
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import im.vector.matrix.android.InstrumentedTest import im.vector.matrix.android.InstrumentedTest
import im.vector.matrix.android.api.MatrixCallback import im.vector.matrix.android.api.MatrixCallback
import im.vector.matrix.android.api.session.Session 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.EncryptedSecretContent
import im.vector.matrix.android.api.session.securestorage.KeySigner 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.SecretStorageKeyContent
import im.vector.matrix.android.api.session.securestorage.SsssKeyCreationInfo 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.api.util.Optional
import im.vector.matrix.android.common.CommonTestHelper import im.vector.matrix.android.common.CommonTestHelper
import im.vector.matrix.android.common.SessionTestParams import im.vector.matrix.android.common.SessionTestParams
import im.vector.matrix.android.common.TestConstants import im.vector.matrix.android.common.TestConstants
import im.vector.matrix.android.common.TestMatrixCallback 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.crosssigning.toBase64NoPadding
import im.vector.matrix.android.internal.crypto.secrets.DefaultSharedSecretStorageService 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 im.vector.matrix.android.internal.session.sync.model.accountdata.UserAccountDataEvent
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -71,7 +70,7 @@ class QuadSTests : InstrumentedTest {
val TEST_KEY_ID = "my.test.Key" val TEST_KEY_ID = "my.test.Key"
val ssssKeyCreationInfo = mTestHelper.doSync<SsssKeyCreationInfo> { mTestHelper.doSync<SsssKeyCreationInfo> {
quadS.generateKey(TEST_KEY_ID, "Test Key", emptyKeySigner, it) 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) assertNotNull("Key should be stored in account data", accountData)
val parsed = SecretStorageKeyContent.fromJson(accountData!!.content) val parsed = SecretStorageKeyContent.fromJson(accountData!!.content)
assertNotNull("Key Content cannot be parsed", parsed) 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) assertEquals("Unexpected key name", "Test Key", parsed.name)
assertNull("Key was not generated from passphrase", parsed.passphrase) 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 // Set as default key
quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback<Unit> {}) quadS.setDefaultKey(TEST_KEY_ID, object : MatrixCallback<Unit> {})
@ -137,13 +129,15 @@ class QuadSTests : InstrumentedTest {
val keyId = "My.Key" val keyId = "My.Key"
val info = generatedSecret(aliceSession, keyId, true) val info = generatedSecret(aliceSession, keyId, true)
val keySpec = RawBytesKeySpec.fromRecoveryKey(info.recoveryKey)
// Store a secret // Store a secret
val clearSecret = Base64.encodeToString("42".toByteArray(), Base64.NO_PADDING or Base64.NO_WRAP) val clearSecret = "42".toByteArray().toBase64NoPadding()
mTestHelper.doSync<Unit> { mTestHelper.doSync<Unit> {
aliceSession.sharedSecretStorageService.storeSecret( aliceSession.sharedSecretStorageService.storeSecret(
"secret.of.life", "secret.of.life",
clearSecret, clearSecret,
null, // default key listOf(Pair<String?, SsssKeySpec?>(null, keySpec)), // default key
it it
) )
} }
@ -157,14 +151,13 @@ class QuadSTests : InstrumentedTest {
val secret = EncryptedSecretContent.fromJson(encryptedContent?.get(keyId)) val secret = EncryptedSecretContent.fromJson(encryptedContent?.get(keyId))
assertNotNull(secret?.ciphertext) assertNotNull(secret?.ciphertext)
assertNotNull(secret?.mac) assertNotNull(secret?.mac)
assertNotNull(secret?.ephemeral) assertNotNull(secret?.initializationVector)
// Try to decrypt?? // Try to decrypt??
val keySpec = Curve25519AesSha2KeySpec.fromRecoveryKey(info.recoveryKey)
val decryptedSecret = mTestHelper.doSync<String> { val decryptedSecret = mTestHelper.doSync<String> {
aliceSession.sharedSecretStorageService.getSecret("secret.of.life", aliceSession.sharedSecretStorageService.getSecret(
"secret.of.life",
null, // default key null, // default key
keySpec!!, keySpec!!,
it it
@ -209,7 +202,10 @@ class QuadSTests : InstrumentedTest {
aliceSession.sharedSecretStorageService.storeSecret( aliceSession.sharedSecretStorageService.storeSecret(
"my.secret", "my.secret",
mySecretText.toByteArray().toBase64NoPadding(), mySecretText.toByteArray().toBase64NoPadding(),
listOf(keyId1, keyId2), listOf(
keyId1 to RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey),
keyId2 to RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)
),
it it
) )
} }
@ -226,7 +222,7 @@ class QuadSTests : InstrumentedTest {
mTestHelper.doSync<String> { mTestHelper.doSync<String> {
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1, keyId1,
Curve25519AesSha2KeySpec.fromRecoveryKey(key1Info.recoveryKey)!!, RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)!!,
it it
) )
} }
@ -234,7 +230,7 @@ class QuadSTests : InstrumentedTest {
mTestHelper.doSync<String> { mTestHelper.doSync<String> {
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId2, keyId2,
Curve25519AesSha2KeySpec.fromRecoveryKey(key2Info.recoveryKey)!!, RawBytesKeySpec.fromRecoveryKey(key2Info.recoveryKey)!!,
it it
) )
} }
@ -255,7 +251,7 @@ class QuadSTests : InstrumentedTest {
aliceSession.sharedSecretStorageService.storeSecret( aliceSession.sharedSecretStorageService.storeSecret(
"my.secret", "my.secret",
mySecretText.toByteArray().toBase64NoPadding(), mySecretText.toByteArray().toBase64NoPadding(),
listOf(keyId1), listOf(keyId1 to RawBytesKeySpec.fromRecoveryKey(key1Info.recoveryKey)),
it it
) )
} }
@ -264,7 +260,7 @@ class QuadSTests : InstrumentedTest {
var error = false var error = false
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1, keyId1,
Curve25519AesSha2KeySpec.fromPassphrase( RawBytesKeySpec.fromPassphrase(
"A bad passphrase", "A bad passphrase",
key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.salt ?: "",
key1Info.content?.passphrase?.iterations ?: 0, key1Info.content?.passphrase?.iterations ?: 0,
@ -289,7 +285,7 @@ class QuadSTests : InstrumentedTest {
mTestHelper.doSync<String> { mTestHelper.doSync<String> {
aliceSession.sharedSecretStorageService.getSecret("my.secret", aliceSession.sharedSecretStorageService.getSecret("my.secret",
keyId1, keyId1,
Curve25519AesSha2KeySpec.fromPassphrase( RawBytesKeySpec.fromPassphrase(
passphrase, passphrase,
key1Info.content?.passphrase?.salt ?: "", key1Info.content?.passphrase?.salt ?: "",
key1Info.content?.passphrase?.iterations ?: 0, key1Info.content?.passphrase?.iterations ?: 0,

View File

@ -32,7 +32,8 @@ data class EncryptedSecretContent(
/** unpadded base64-encoded ciphertext */ /** unpadded base64-encoded ciphertext */
@Json(name = "ciphertext") val ciphertext: String? = null, @Json(name = "ciphertext") val ciphertext: String? = null,
@Json(name = "mac") val mac: 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 { ) : AccountDataContent {
companion object { companion object {
/** /**

View File

@ -27,5 +27,6 @@ sealed class SharedSecretStorageError(message: String?) : Throwable(message) {
object BadKeyFormat : SharedSecretStorageError("Bad Key Format") object BadKeyFormat : SharedSecretStorageError("Bad Key Format")
object ParsingError : SharedSecretStorageError("parsing Error") object ParsingError : SharedSecretStorageError("parsing Error")
object BadMac : SharedSecretStorageError("Bad mac")
data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage) data class OtherError(val reason: Throwable) : SharedSecretStorageError(reason.localizedMessage)
} }

View File

@ -42,7 +42,7 @@ interface SharedSecretStorageService {
*/ */
fun generateKey(keyId: String, fun generateKey(keyId: String,
keyName: String, keyName: String,
keySigner: KeySigner, keySigner: KeySigner?,
callback: MatrixCallback<SsssKeyCreationInfo>) callback: MatrixCallback<SsssKeyCreationInfo>)
/** /**
@ -92,7 +92,7 @@ interface SharedSecretStorageService {
* @param secret The secret contents. * @param secret The secret contents.
* @param keys The list of (ID,privateKey) of the keys to use to encrypt the secret. * @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 * 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 name The name of the secret
* @param keyId The id of the key that should be used to decrypt (null for default key) * @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>) fun getSecret(name: String, keyId: String?, secretKey: SsssKeySpec, callback: MatrixCallback<String>)

View File

@ -23,14 +23,14 @@ import im.vector.matrix.android.internal.crypto.keysbackup.util.extractCurveKeyF
/** Tag class */ /** Tag class */
interface SsssKeySpec interface SsssKeySpec
data class Curve25519AesSha2KeySpec( data class RawBytesKeySpec(
val privateKey: ByteArray val privateKey: ByteArray
) : SsssKeySpec { ) : SsssKeySpec {
companion object { companion object {
fun fromPassphrase(passphrase: String, salt: String, iterations: Int, progressListener: ProgressListener?): Curve25519AesSha2KeySpec { fun fromPassphrase(passphrase: String, salt: String, iterations: Int, progressListener: ProgressListener?): RawBytesKeySpec {
return Curve25519AesSha2KeySpec( return RawBytesKeySpec(
privateKey = deriveKey( privateKey = deriveKey(
passphrase, passphrase,
salt, salt,
@ -40,9 +40,9 @@ data class Curve25519AesSha2KeySpec(
) )
} }
fun fromRecoveryKey(recoveryKey: String): Curve25519AesSha2KeySpec? { fun fromRecoveryKey(recoveryKey: String): RawBytesKeySpec? {
return extractCurveKeyFromRecoveryKey(recoveryKey)?.let { return extractCurveKeyFromRecoveryKey(recoveryKey)?.let {
Curve25519AesSha2KeySpec( RawBytesKeySpec(
privateKey = it privateKey = it
) )
} }
@ -53,7 +53,7 @@ data class Curve25519AesSha2KeySpec(
if (this === other) return true if (this === other) return true
if (javaClass != other?.javaClass) return false if (javaClass != other?.javaClass) return false
other as Curve25519AesSha2KeySpec other as RawBytesKeySpec
if (!privateKey.contentEquals(other.privateKey)) return false if (!privateKey.contentEquals(other.privateKey)) return false

View File

@ -35,6 +35,8 @@ const val MXCRYPTO_ALGORITHM_MEGOLM_BACKUP = "m.megolm_backup.v1.curve25519-aes-
* Secured Shared Storage algorithm constant * Secured Shared Storage algorithm constant
*/ */
const val SSSS_ALGORITHM_CURVE25519_AES_SHA2 = "m.secret_storage.v1.curve25519-aes-sha2" 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 // TODO Refacto: use this constants everywhere
const val ed25519 = "ed25519" const val ed25519 = "ed25519"

View File

@ -17,38 +17,42 @@
package im.vector.matrix.android.internal.crypto.secrets package im.vector.matrix.android.internal.crypto.secrets
import im.vector.matrix.android.api.MatrixCallback 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.listeners.ProgressListener
import im.vector.matrix.android.api.session.accountdata.AccountDataService 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.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.EncryptedSecretContent
import im.vector.matrix.android.api.session.securestorage.IntegrityResult 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.KeyInfo
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult 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.KeySigner
import im.vector.matrix.android.api.session.securestorage.SsssKeySpec import im.vector.matrix.android.api.session.securestorage.RawBytesKeySpec
import im.vector.matrix.android.api.session.securestorage.SsssPassphrase
import im.vector.matrix.android.api.session.securestorage.SecretStorageKeyContent 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.SharedSecretStorageError
import im.vector.matrix.android.api.session.securestorage.SharedSecretStorageService 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.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.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.generatePrivateKeyWithPassword
import im.vector.matrix.android.internal.crypto.keysbackup.util.computeRecoveryKey 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.withOlmDecryption
import im.vector.matrix.android.internal.crypto.tools.withOlmEncryption
import im.vector.matrix.android.internal.extensions.foldToCallback import im.vector.matrix.android.internal.extensions.foldToCallback
import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers import im.vector.matrix.android.internal.util.MatrixCoroutineDispatchers
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.matrix.olm.OlmPkMessage 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 import javax.inject.Inject
import kotlin.experimental.and
private data class Key(
val publicKey: String,
@Suppress("ArrayInDataClass")
val privateKey: ByteArray
)
internal class DefaultSharedSecretStorageService @Inject constructor( internal class DefaultSharedSecretStorageService @Inject constructor(
private val accountDataService: AccountDataService, private val accountDataService: AccountDataService,
@ -58,14 +62,12 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
override fun generateKey(keyId: String, override fun generateKey(keyId: String,
keyName: String, keyName: String,
keySigner: KeySigner, keySigner: KeySigner?,
callback: MatrixCallback<SsssKeyCreationInfo>) { callback: MatrixCallback<SsssKeyCreationInfo>) {
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val key = try { val key = try {
withOlmDecryption { olmPkDecryption -> ByteArray(32).also {
val pubKey = olmPkDecryption.generateKey() SecureRandom().nextBytes(it)
val privateKey = olmPkDecryption.privateKey()
Key(pubKey, privateKey)
} }
} catch (failure: Throwable) { } catch (failure: Throwable) {
callback.onFailure(failure) callback.onFailure(failure)
@ -74,12 +76,11 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val storageKeyContent = SecretStorageKeyContent( val storageKeyContent = SecretStorageKeyContent(
name = keyName, name = keyName,
algorithm = SSSS_ALGORITHM_CURVE25519_AES_SHA2, algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2,
passphrase = null, passphrase = null
publicKey = key.publicKey
) )
val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let { val signedContent = keySigner?.sign(storageKeyContent.canonicalSignable())?.let {
storageKeyContent.copy( storageKeyContent.copy(
signatures = it signatures = it
) )
@ -97,7 +98,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
callback.onSuccess(SsssKeyCreationInfo( callback.onSuccess(SsssKeyCreationInfo(
keyId = keyId, keyId = keyId,
content = storageKeyContent, content = storageKeyContent,
recoveryKey = computeRecoveryKey(key.privateKey) recoveryKey = computeRecoveryKey(key)
)) ))
} }
} }
@ -114,19 +115,9 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val privatePart = generatePrivateKeyWithPassword(passphrase, progressListener) 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( val storageKeyContent = SecretStorageKeyContent(
algorithm = SSSS_ALGORITHM_CURVE25519_AES_SHA2, algorithm = SSSS_ALGORITHM_AES_HMAC_SHA2,
passphrase = SsssPassphrase(algorithm = "m.pbkdf2", iterations = privatePart.iterations, salt = privatePart.salt), passphrase = SsssPassphrase(algorithm = "m.pbkdf2", iterations = privatePart.iterations, salt = privatePart.salt)
publicKey = pubKey
) )
val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let { val signedContent = keySigner.sign(storageKeyContent.canonicalSignable())?.let {
@ -189,51 +180,19 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
return getKey(keyId) 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) { cryptoCoroutineScope.launch(coroutineDispatchers.main) {
val encryptedContents = HashMap<String, EncryptedSecretContent>() val encryptedContents = HashMap<String, EncryptedSecretContent>()
try { try {
if (keys.isNullOrEmpty()) {
// use default key
when (val key = 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)
}
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 ?: ""))
return@launch
}
}
is KeyInfoResult.Error -> {
callback.onFailure(key.error)
return@launch
}
}
} else {
keys.forEach { keys.forEach {
val keyId = it val keyId = it.first
// encrypt the content // encrypt the content
when (val key = getKey(keyId)) { when (val key = keyId?.let { getKey(keyId) } ?: getDefaultKey()) {
is KeyInfoResult.Success -> { is KeyInfoResult.Success -> {
if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_CURVE25519_AES_SHA2) { if (key.keyInfo.content.algorithm == SSSS_ALGORITHM_AES_HMAC_SHA2) {
val encryptedResult = withOlmEncryption { olmEncrypt -> encryptAesHmacSha2(it.second!!, name, secretBase64).let {
olmEncrypt.setRecipientKey(key.keyInfo.content.publicKey) encryptedContents[key.keyInfo.id] = it
olmEncrypt.encrypt(secretBase64)
} }
encryptedContents[keyId] = EncryptedSecretContent(
ciphertext = encryptedResult.mCipherText,
ephemeral = encryptedResult.mEphemeralKey,
mac = encryptedResult.mMac
)
} else { } else {
// Unknown algorithm // Unknown algorithm
callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: "")) callback.onFailure(SharedSecretStorageError.UnknownAlgorithm(key.keyInfo.content.algorithm ?: ""))
@ -246,7 +205,6 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
} }
} }
} }
}
accountDataService.updateAccountData( accountDataService.updateAccountData(
type = name, type = name,
@ -259,8 +217,107 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
callback.onFailure(failure) 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> { override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
@ -300,7 +357,7 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val algorithm = key.keyInfo.content val algorithm = key.keyInfo.content
if (SSSS_ALGORITHM_CURVE25519_AES_SHA2 == algorithm.algorithm) { 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) callback.onFailure(SharedSecretStorageError.BadKeyFormat)
} }
cryptoCoroutineScope.launch(coroutineDispatchers.main) { cryptoCoroutineScope.launch(coroutineDispatchers.main) {
@ -318,6 +375,15 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
} }
}.foldToCallback(callback) }.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 { } else {
callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: "")) callback.onFailure(SharedSecretStorageError.UnsupportedAlgorithm(algorithm.algorithm ?: ""))
} }
@ -343,7 +409,8 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
val keyInfo = (keyInfoResult as? KeyInfoResult.Success)?.keyInfo val keyInfo = (keyInfoResult as? KeyInfoResult.Success)?.keyInfo
?: return IntegrityResult.Error(SharedSecretStorageError.UnknownKey(keyId ?: "")) ?: 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 // Unsupported algorithm
return IntegrityResult.Error( return IntegrityResult.Error(
SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "") SharedSecretStorageError.UnsupportedAlgorithm(keyInfo.content.algorithm ?: "")

View File

@ -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"
}

View File

@ -24,7 +24,7 @@ import com.squareup.inject.assisted.Assisted
import com.squareup.inject.assisted.AssistedInject import com.squareup.inject.assisted.AssistedInject
import im.vector.matrix.android.api.listeners.ProgressListener import im.vector.matrix.android.api.listeners.ProgressListener
import im.vector.matrix.android.api.session.Session 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.IntegrityResult
import im.vector.matrix.android.api.session.securestorage.KeyInfoResult import im.vector.matrix.android.api.session.securestorage.KeyInfoResult
import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding import im.vector.matrix.android.internal.crypto.crosssigning.toBase64NoPadding
@ -99,7 +99,7 @@ class SharedSecureStorageViewModel @AssistedInject constructor(
isIndeterminate = true isIndeterminate = true
) )
)) ))
val keySpec = Curve25519AesSha2KeySpec.fromPassphrase( val keySpec = RawBytesKeySpec.fromPassphrase(
passphrase, passphrase,
keyInfo.content.passphrase?.salt ?: "", keyInfo.content.passphrase?.salt ?: "",
keyInfo.content.passphrase?.iterations ?: 0, keyInfo.content.passphrase?.iterations ?: 0,