Skip to content

Commit 1d26207

Browse files
ganfraBillCarsonFr
authored andcommitted
Extract AesHmacSha2 encrypt/decrypt
1 parent 92faf3b commit 1d26207

File tree

2 files changed

+165
-78
lines changed

2 files changed

+165
-78
lines changed

matrix-sdk-android/src/main/java/org/matrix/android/sdk/internal/crypto/secrets/DefaultSharedSecretStorageService.kt

Lines changed: 13 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import kotlinx.coroutines.withContext
2121
import org.matrix.android.sdk.api.MatrixCoroutineDispatchers
2222
import org.matrix.android.sdk.api.crypto.SSSS_ALGORITHM_AES_HMAC_SHA2
2323
import org.matrix.android.sdk.api.crypto.SSSS_ALGORITHM_CURVE25519_AES_SHA2
24-
import org.matrix.android.sdk.api.extensions.orFalse
2524
import org.matrix.android.sdk.api.listeners.ProgressListener
2625
import org.matrix.android.sdk.api.session.accountdata.SessionAccountDataService
2726
import org.matrix.android.sdk.api.session.crypto.keysbackup.computeRecoveryKey
@@ -43,17 +42,12 @@ import org.matrix.android.sdk.api.util.fromBase64
4342
import org.matrix.android.sdk.api.util.toBase64NoPadding
4443
import org.matrix.android.sdk.internal.crypto.SecretShareManager
4544
import org.matrix.android.sdk.internal.crypto.keysbackup.generatePrivateKeyWithPassword
46-
import org.matrix.android.sdk.internal.crypto.tools.HkdfSha256
45+
import org.matrix.android.sdk.internal.crypto.tools.AesHmacSha2
4746
import org.matrix.android.sdk.internal.crypto.tools.withOlmDecryption
4847
import org.matrix.android.sdk.internal.di.UserId
4948
import org.matrix.olm.OlmPkMessage
5049
import java.security.SecureRandom
51-
import javax.crypto.Cipher
52-
import javax.crypto.Mac
53-
import javax.crypto.spec.IvParameterSpec
54-
import javax.crypto.spec.SecretKeySpec
5550
import javax.inject.Inject
56-
import kotlin.experimental.and
5751

5852
internal class DefaultSharedSecretStorageService @Inject constructor(
5953
@UserId private val userId: String,
@@ -214,84 +208,25 @@ internal class DefaultSharedSecretStorageService @Inject constructor(
214208
@Throws
215209
private fun encryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, clearDataBase64: String): EncryptedSecretContent {
216210
secretKey as RawBytesKeySpec
217-
val pseudoRandomKey = HkdfSha256.deriveSecret(
218-
secretKey.privateKey,
219-
ByteArray(32) { 0.toByte() },
220-
secretName.toByteArray(),
221-
64
222-
)
223-
224-
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
225-
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
226-
val macKey = pseudoRandomKey.copyOfRange(32, 64)
227-
228-
val secureRandom = SecureRandom()
229-
val iv = ByteArray(16)
230-
secureRandom.nextBytes(iv)
231-
232-
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
233-
// (which would mean we wouldn't be able to decrypt on Android). The loss
234-
// of a single bit of salt is a price we have to pay.
235-
iv[9] = iv[9] and 0x7f
236-
237-
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
238-
239-
val secretKeySpec = SecretKeySpec(aesKey, "AES")
240-
val ivParameterSpec = IvParameterSpec(iv)
241-
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
242-
// secret are not that big, just do Final
243-
val cipherBytes = cipher.doFinal(clearDataBase64.toByteArray())
244-
require(cipherBytes.isNotEmpty())
245-
246-
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
247-
val mac = Mac.getInstance("HmacSHA256")
248-
mac.init(macKeySpec)
249-
val digest = mac.doFinal(cipherBytes)
250-
211+
val result = AesHmacSha2.encrypt(secretKey.privateKey, secretName, clearDataBase64)
251212
return EncryptedSecretContent(
252-
ciphertext = cipherBytes.toBase64NoPadding(),
253-
initializationVector = iv.toBase64NoPadding(),
254-
mac = digest.toBase64NoPadding()
213+
ciphertext = result.cipherRawBytes.toBase64NoPadding(),
214+
initializationVector = result.initializationVector.toBase64NoPadding(),
215+
mac = result.initializationVector.toBase64NoPadding()
255216
)
256217
}
257218

258219
private fun decryptAesHmacSha2(secretKey: SsssKeySpec, secretName: String, cipherContent: EncryptedSecretContent): String {
259220
secretKey as RawBytesKeySpec
260-
val pseudoRandomKey = HkdfSha256.deriveSecret(
261-
secretKey.privateKey,
262-
ByteArray(32) { 0.toByte() },
263-
secretName.toByteArray(),
264-
64
265-
)
266-
267-
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
268-
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
269-
val macKey = pseudoRandomKey.copyOfRange(32, 64)
270-
271221
val iv = cipherContent.initializationVector?.fromBase64() ?: ByteArray(16)
272-
273-
val cipherRawBytes = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText
274-
275-
// Check Signature
276-
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
277-
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
278-
val digest = mac.doFinal(cipherRawBytes)
279-
280-
if (!cipherContent.mac?.fromBase64()?.contentEquals(digest).orFalse()) {
281-
throw SharedSecretStorageError.BadMac
282-
}
283-
284-
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
285-
286-
val secretKeySpec = SecretKeySpec(aesKey, "AES")
287-
val ivParameterSpec = IvParameterSpec(iv)
288-
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
289-
// secret are not that big, just do Final
290-
val decryptedSecret = cipher.doFinal(cipherRawBytes)
291-
292-
require(decryptedSecret.isNotEmpty())
293-
294-
return String(decryptedSecret, Charsets.UTF_8)
222+
val cipher = cipherContent.ciphertext?.fromBase64() ?: throw SharedSecretStorageError.BadCipherText
223+
val mac = cipherContent.mac?.fromBase64() ?: throw SharedSecretStorageError.BadMac
224+
val encryptedResult = AesHmacSha2.Result(
225+
cipherRawBytes = cipher,
226+
initializationVector = iv,
227+
mac = mac
228+
)
229+
return AesHmacSha2.decrypt(secretKey.privateKey, secretName, encryptedResult)
295230
}
296231

297232
override fun getAlgorithmsForSecret(name: String): List<KeyInfoResult> {
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/*
2+
* Copyright (c) 2022 New Vector Ltd
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.matrix.android.sdk.internal.crypto.tools
18+
19+
import org.matrix.android.sdk.api.session.securestorage.SharedSecretStorageError
20+
import org.matrix.android.sdk.api.util.fromBase64
21+
import java.security.SecureRandom
22+
import javax.crypto.Cipher
23+
import javax.crypto.Mac
24+
import javax.crypto.spec.IvParameterSpec
25+
import javax.crypto.spec.SecretKeySpec
26+
import kotlin.experimental.and
27+
28+
internal object AesHmacSha2 {
29+
30+
class Result(
31+
val cipherRawBytes: ByteArray,
32+
val mac: ByteArray,
33+
val initializationVector: ByteArray
34+
)
35+
36+
/**
37+
* Encryption algorithm aes-hmac-sha2
38+
* Secrets are encrypted using AES-CTR-256 and MACed using HMAC-SHA-256. The data is encrypted and MACed as follows:
39+
*
40+
* Given the secret storage key, generate 64 bytes by performing an HKDF with SHA-256 as the hash, a salt of 32 bytes
41+
* of 0, and with the secret name as the info.
42+
*
43+
* The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
44+
*
45+
* Generate 16 random bytes, set bit 63 to 0 (in order to work around differences in AES-CTR implementations), and use
46+
* this as the AES initialization vector.
47+
* This becomes the iv property, encoded using base64.
48+
*
49+
* Encrypt the data using AES-CTR-256 using the AES key generated above.
50+
*
51+
* This encrypted data, encoded using base64, becomes the ciphertext property.
52+
*
53+
* Pass the raw encrypted data (prior to base64 encoding) through HMAC-SHA-256 using the MAC key generated above.
54+
* The resulting MAC is base64-encoded and becomes the mac property.
55+
* (We use AES-CTR to match file encryption and key exports.)
56+
*/
57+
@Throws
58+
fun encrypt(privateKey: ByteArray, secretName: String, clearDataBase64: String, ivString: String? = null): Result {
59+
val pseudoRandomKey = HkdfSha256.deriveSecret(
60+
privateKey,
61+
ByteArray(32) { 0.toByte() },
62+
secretName.toByteArray(),
63+
64
64+
)
65+
66+
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
67+
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
68+
val macKey = pseudoRandomKey.copyOfRange(32, 64)
69+
70+
val iv = if (ivString != null) {
71+
ivString.fromBase64()
72+
} else {
73+
val secureRandom = SecureRandom()
74+
ByteArray(16).apply {
75+
secureRandom.nextBytes(this)
76+
}
77+
}
78+
// clear bit 63 of the salt to stop us hitting the 64-bit counter boundary
79+
// (which would mean we wouldn't be able to decrypt on Android). The loss
80+
// of a single bit of salt is a price we have to pay.
81+
iv[9] = iv[9] and 0x7f
82+
83+
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
84+
85+
val secretKeySpec = SecretKeySpec(aesKey, "AES")
86+
val ivParameterSpec = IvParameterSpec(iv)
87+
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, ivParameterSpec)
88+
// secret are not that big, just do Final
89+
val cipherBytes = cipher.doFinal(clearDataBase64.toByteArray())
90+
require(cipherBytes.isNotEmpty())
91+
92+
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
93+
val mac = Mac.getInstance("HmacSHA256")
94+
mac.init(macKeySpec)
95+
val digest = mac.doFinal(cipherBytes)
96+
97+
return Result(
98+
cipherRawBytes = cipherBytes,
99+
initializationVector = iv,
100+
mac = digest
101+
)
102+
}
103+
104+
fun decrypt(privateKey: ByteArray, secretName: String, aesHmacSha2Result: Result): String {
105+
val pseudoRandomKey = HkdfSha256.deriveSecret(
106+
privateKey,
107+
zeroByteArray(32),
108+
secretName.toByteArray(),
109+
64
110+
)
111+
112+
// The first 32 bytes are used as the AES key, and the next 32 bytes are used as the MAC key
113+
val aesKey = pseudoRandomKey.copyOfRange(0, 32)
114+
val macKey = pseudoRandomKey.copyOfRange(32, 64)
115+
116+
val iv = aesHmacSha2Result.initializationVector
117+
val cipherRawBytes = aesHmacSha2Result.cipherRawBytes
118+
119+
val macKeySpec = SecretKeySpec(macKey, "HmacSHA256")
120+
val mac = Mac.getInstance("HmacSHA256").apply { init(macKeySpec) }
121+
val digest = mac.doFinal(cipherRawBytes)
122+
if (!aesHmacSha2Result.mac.contentEquals(digest)) {
123+
throw SharedSecretStorageError.BadMac
124+
}
125+
val cipher = Cipher.getInstance("AES/CTR/NoPadding")
126+
127+
val secretKeySpec = SecretKeySpec(aesKey, "AES")
128+
val ivParameterSpec = IvParameterSpec(iv)
129+
cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, ivParameterSpec)
130+
// secret are not that big, just do Final
131+
val decryptedSecret = cipher.doFinal(cipherRawBytes)
132+
133+
require(decryptedSecret.isNotEmpty())
134+
135+
return String(decryptedSecret, Charsets.UTF_8)
136+
}
137+
138+
/** Calculate the MAC for checking the key.
139+
*
140+
* @param {ByteArray} [key] the key to use
141+
* @param {string} [iv] The initialization vector as a base64-encoded string.
142+
* If omitted, a random initialization vector will be created.
143+
* @return [Result] An object that contains, `mac` and `iv` properties.
144+
*/
145+
fun calculateKeyCheck(key: ByteArray, iv: String?): Result {
146+
val zerosStr = String(zeroByteArray(32))
147+
return encrypt(key, "", zerosStr, iv)
148+
}
149+
150+
private fun zeroByteArray(size: Int): ByteArray = ByteArray(size) { 0.toByte() }
151+
152+
}

0 commit comments

Comments
 (0)