Skip to content

Commit b16b8b7

Browse files
committed
CryptoKit AES.GCM implementation
1 parent 6a11bd0 commit b16b8b7

File tree

6 files changed

+202
-11
lines changed

6 files changed

+202
-11
lines changed

cryptography-providers-tests-api/src/commonMain/kotlin/support.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
package dev.whyoleg.cryptography.providers.tests.api
66

77
import dev.whyoleg.cryptography.*
8+
import dev.whyoleg.cryptography.BinarySize.Companion.bytes
89
import dev.whyoleg.cryptography.algorithms.*
910
import dev.whyoleg.cryptography.materials.key.*
1011
import dev.whyoleg.cryptography.serialization.asn1.*
@@ -52,6 +53,14 @@ fun AlgorithmTestScope<AES.CBC>.supportsPadding(padding: Boolean): Boolean = sup
5253
}
5354
}
5455

56+
// CryptoKit supports only the default tag size
57+
fun AlgorithmTestScope<AES.GCM>.supportsTagSize(tagSize: BinarySize): Boolean = supports {
58+
when {
59+
provider.isCryptoKit && tagSize != 16.bytes -> "non-default tag size"
60+
else -> null
61+
}
62+
}
63+
5564
// WebCrypto BROWSER(or only chromium) doesn't support 192bits
5665
// https://bugs.chromium.org/p/chromium/issues/detail?id=533699
5766
fun AlgorithmTestScope<out AES<*>>.supportsKeySize(keySizeBits: Int): Boolean = supports {

cryptography-providers-tests/src/commonMain/kotlin/compatibility/AesGcmCompatibilityTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023-2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright (c) 2023-2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package dev.whyoleg.cryptography.providers.tests.compatibility
@@ -43,6 +43,8 @@ abstract class AesGcmCompatibilityTest(provider: CryptographyProvider) :
4343

4444
val parametersList = buildList {
4545
tagSizes.forEach { tagSize ->
46+
if (!supportsTagSize(tagSize.bits)) return@forEach
47+
4648
// size of IV = 12
4749
(List(ivIterations) { ByteString(CryptographyRandom.nextBytes(12)) } + listOf(null)).forEach { iv ->
4850
val parameters = CipherParameters(tagSize, iv)
@@ -94,6 +96,7 @@ abstract class AesGcmCompatibilityTest(provider: CryptographyProvider) :
9496
val keys = validateKeys()
9597

9698
api.ciphers.getParameters<CipherParameters> { (tagSize, iv), parametersId, _ ->
99+
if (!supportsTagSize(tagSize.bits)) return@getParameters
97100
api.ciphers.getData<AuthenticatedCipherData>(parametersId) { (keyReference, associatedData, plaintext, ciphertext), _, _ ->
98101
keys[keyReference]?.forEach { key ->
99102
val cipher = key.cipher(tagSize.bits)

cryptography-providers-tests/src/commonMain/kotlin/default/AesGcmTest.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2023-2024 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
2+
* Copyright (c) 2023-2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
33
*/
44

55
package dev.whyoleg.cryptography.providers.tests.default
@@ -21,6 +21,7 @@ abstract class AesGcmTest(provider: CryptographyProvider) : AesBasedTest<AES.GCM
2121
assertEquals(keySize.inBytes, key.encodeToByteString(AES.Key.Format.RAW).size)
2222

2323
listOf(96, 104, 112, 120, 128).forEach { tagSizeBits ->
24+
if (!supportsTagSize(tagSizeBits.bits)) return@forEach
2425
val tagSize = tagSizeBits.bits.inBytes
2526
key.cipher(tagSizeBits.bits).run {
2627
listOf(0, 15, 16, 17, 319, 320, 321).forEach { inputSize ->
@@ -60,6 +61,7 @@ abstract class AesGcmTest(provider: CryptographyProvider) : AesBasedTest<AES.GCM
6061

6162
val key = algorithm.keyGenerator(keySize).generateKey()
6263
listOf(96, 104, 112, 120, 128).forEach { tagSizeBits ->
64+
if (!supportsTagSize(tagSizeBits.bits)) return@forEach
6365
val cipher = key.cipher(tagSizeBits.bits)
6466
repeat(100) {
6567
val size = CryptographyRandom.nextInt(20000)
@@ -75,6 +77,7 @@ abstract class AesGcmTest(provider: CryptographyProvider) : AesBasedTest<AES.GCM
7577

7678
val key = algorithm.keyGenerator(keySize).generateKey()
7779
listOf(96, 104, 112, 120, 128).forEach { tagSizeBits ->
80+
if (!supportsTagSize(tagSizeBits.bits)) return@forEach
7881
val cipher = key.cipher(tagSizeBits.bits)
7982
repeat(100) {
8083
val size = CryptographyRandom.nextInt(20000)

cryptography-providers/cryptokit/src/commonMain/kotlin/CryptoKitCryptographyProvider.kt

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,17 +20,17 @@ internal object CryptoKitCryptographyProvider : CryptographyProvider() {
2020

2121
@Suppress("UNCHECKED_CAST")
2222
override fun <A : CryptographyAlgorithm> getOrNull(identifier: CryptographyAlgorithmId<A>): A? = when (identifier) {
23-
MD5 -> CryptoKitDigest(MD5, SwiftHash::md5, SwiftHashAlgorithmMd5)
24-
SHA1 -> CryptoKitDigest(SHA1, SwiftHash::sha1, SwiftHashAlgorithmSha1)
25-
SHA256 -> CryptoKitDigest(SHA256, SwiftHash::sha256, SwiftHashAlgorithmSha256)
26-
SHA384 -> CryptoKitDigest(SHA384, SwiftHash::sha384, SwiftHashAlgorithmSha384)
27-
SHA512 -> CryptoKitDigest(SHA512, SwiftHash::sha512, SwiftHashAlgorithmSha512)
28-
HMAC -> CryptoKitHmac
29-
HKDF -> CryptoKitHkdf
30-
// AES.GCM ->
23+
MD5 -> CryptoKitDigest(MD5, SwiftHash::md5, SwiftHashAlgorithmMd5)
24+
SHA1 -> CryptoKitDigest(SHA1, SwiftHash::sha1, SwiftHashAlgorithmSha1)
25+
SHA256 -> CryptoKitDigest(SHA256, SwiftHash::sha256, SwiftHashAlgorithmSha256)
26+
SHA384 -> CryptoKitDigest(SHA384, SwiftHash::sha384, SwiftHashAlgorithmSha384)
27+
SHA512 -> CryptoKitDigest(SHA512, SwiftHash::sha512, SwiftHashAlgorithmSha512)
28+
HMAC -> CryptoKitHmac
29+
HKDF -> CryptoKitHkdf
30+
AES.GCM -> CryptoKitAesGcm
3131
// ECDSA ->
3232
// ECDH ->
33-
else -> null
33+
else -> null
3434
} as A?
3535
}
3636

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
/*
2+
* Copyright (c) 2024-2025 Oleg Yukhnevich. Use of this source code is governed by the Apache 2.0 license.
3+
*/
4+
5+
package dev.whyoleg.cryptography.providers.cryptokit.algorithms
6+
7+
import dev.whyoleg.cryptography.*
8+
import dev.whyoleg.cryptography.BinarySize.Companion.bytes
9+
import dev.whyoleg.cryptography.algorithms.*
10+
import dev.whyoleg.cryptography.materials.key.*
11+
import dev.whyoleg.cryptography.providers.base.*
12+
import dev.whyoleg.cryptography.providers.base.algorithms.*
13+
import dev.whyoleg.cryptography.providers.base.operations.*
14+
import dev.whyoleg.cryptography.providers.cryptokit.internal.swiftinterop.*
15+
import dev.whyoleg.cryptography.random.*
16+
import kotlinx.cinterop.*
17+
import platform.Foundation.*
18+
19+
internal object CryptoKitAesGcm : AES.GCM {
20+
override fun keyDecoder(): KeyDecoder<AES.Key.Format, AES.GCM.Key> = AesKeyDecoder()
21+
22+
override fun keyGenerator(keySize: BinarySize): KeyGenerator<AES.GCM.Key> = AesGcmKeyGenerator(keySize.inBytes)
23+
}
24+
25+
private class AesKeyDecoder : KeyDecoder<AES.Key.Format, AES.GCM.Key> {
26+
override fun decodeFromByteArrayBlocking(format: AES.Key.Format, bytes: ByteArray): AES.GCM.Key = when (format) {
27+
AES.Key.Format.RAW -> {
28+
require(bytes.size == 16 || bytes.size == 24 || bytes.size == 32) {
29+
"AES key size must be 128, 192 or 256 bits"
30+
}
31+
AesGcmKey(bytes.copyOf())
32+
}
33+
AES.Key.Format.JWK -> error("JWK is not supported")
34+
}
35+
}
36+
37+
private class AesGcmKeyGenerator(private val keySizeBytes: Int) : KeyGenerator<AES.GCM.Key> {
38+
override fun generateKeyBlocking(): AES.GCM.Key {
39+
val key = CryptographyRandom.nextBytes(keySizeBytes)
40+
return AesGcmKey(key)
41+
}
42+
}
43+
44+
private class AesGcmKey(private val key: ByteArray) : AES.GCM.Key {
45+
override fun cipher(tagSize: BinarySize): AES.IvAuthenticatedCipher {
46+
require(tagSize == 16.bytes) { "GCM tag size must be 16 bytes, but was $tagSize" }
47+
return AesGcmCipher(key, tagSize.inBytes)
48+
}
49+
50+
override fun encodeToByteArrayBlocking(format: AES.Key.Format): ByteArray = when (format) {
51+
AES.Key.Format.RAW -> key.copyOf()
52+
AES.Key.Format.JWK -> error("JWK is not supported")
53+
}
54+
}
55+
56+
private class AesGcmCipher(
57+
private val key: ByteArray,
58+
private val tagSize: Int,
59+
) : BaseAesIvAuthenticatedCipher {
60+
private val ivSize: Int get() = 12
61+
62+
override fun createEncryptFunction(associatedData: ByteArray?): CipherFunction {
63+
val iv = CryptographyRandom.nextBytes(ivSize)
64+
return BaseAesImplicitIvEncryptFunction(iv, createEncryptFunctionWithIv(iv, associatedData))
65+
}
66+
67+
override fun createDecryptFunction(associatedData: ByteArray?): CipherFunction {
68+
return BaseAesImplicitIvDecryptFunction(ivSize) { iv, startIndex ->
69+
createDecryptFunctionWithIv(iv, startIndex, associatedData)
70+
}
71+
}
72+
73+
override fun createEncryptFunctionWithIv(iv: ByteArray, associatedData: ByteArray?): CipherFunction {
74+
require(iv.size == ivSize) { "IV size is wrong" }
75+
76+
return AccumulatingCipherFunction { plaintext ->
77+
plaintext.useNSData { plaintextData ->
78+
iv.useNSData { ivData ->
79+
key.useNSData { keyData ->
80+
(associatedData ?: EmptyByteArray).useNSData { adData ->
81+
swiftTry { error ->
82+
SwiftAesGcm.encryptWithKey(
83+
key = keyData,
84+
nonce = ivData,
85+
plaintext = plaintextData,
86+
authenticatedData = adData,
87+
error = error
88+
)
89+
}.toByteArray().let {
90+
it.copyOfRange(ivSize, it.size)
91+
}
92+
}
93+
}
94+
}
95+
}
96+
}
97+
}
98+
99+
private fun createDecryptFunctionWithIv(iv: ByteArray, startIndex: Int, associatedData: ByteArray?): CipherFunction {
100+
require(iv.size - startIndex >= ivSize) { "IV size is wrong" }
101+
102+
return AccumulatingCipherFunction { ciphertext ->
103+
ciphertext.useNSData(endIndex = ciphertext.size - tagSize) { ciphertextData ->
104+
ciphertext.useNSData(startIndex = ciphertext.size - tagSize) { tagData ->
105+
iv.useNSData(startIndex, startIndex + ivSize) { ivData ->
106+
key.useNSData { keyData ->
107+
(associatedData ?: EmptyByteArray).useNSData { adData ->
108+
swiftTry { error ->
109+
SwiftAesGcm.decryptWithKey(
110+
key = keyData,
111+
nonce = ivData,
112+
ciphertext = ciphertextData,
113+
tag = tagData,
114+
authenticatedData = adData,
115+
error = error
116+
)
117+
}.toByteArray()
118+
}
119+
}
120+
}
121+
}
122+
}
123+
}
124+
}
125+
126+
override fun createDecryptFunctionWithIv(iv: ByteArray, associatedData: ByteArray?): CipherFunction {
127+
return createDecryptFunctionWithIv(iv, 0, associatedData)
128+
}
129+
}
130+
131+
@OptIn(BetaInteropApi::class)
132+
private fun <T : Any> swiftTry(
133+
block: (error: CPointer<ObjCObjectVar<NSError?>>) -> T?,
134+
): T = memScoped {
135+
val errorH = alloc<ObjCObjectVar<NSError?>>()
136+
when (val result = block(errorH.ptr)) {
137+
null -> error("Swift call failed: ${errorH.value?.localizedDescription ?: "unknown error"}")
138+
else -> result
139+
}
140+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import CryptoKit
2+
import Foundation
3+
4+
@objc public class SwiftAesGcm: NSObject {
5+
@objc public static func encrypt(
6+
key: NSData,
7+
nonce: NSData,
8+
plaintext: NSData,
9+
authenticatedData: NSData
10+
) throws -> Data {
11+
return try AES.GCM.seal(
12+
plaintext as Data,
13+
using: SymmetricKey(data: key as Data),
14+
nonce: try AES.GCM.Nonce(data: nonce as Data),
15+
authenticating: authenticatedData
16+
).combined!
17+
}
18+
19+
@objc public static func decrypt(
20+
key: NSData,
21+
nonce: NSData,
22+
ciphertext: NSData,
23+
tag: NSData,
24+
authenticatedData: NSData
25+
) throws -> Data {
26+
return try AES.GCM.open(
27+
try AES.GCM.SealedBox(
28+
nonce: try AES.GCM.Nonce(data: nonce),
29+
ciphertext: ciphertext,
30+
tag: tag
31+
),
32+
using: SymmetricKey(data: key as Data),
33+
authenticating: authenticatedData
34+
)
35+
}
36+
}

0 commit comments

Comments
 (0)