-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Symmetric Encryption #210
base: development
Are you sure you want to change the base?
Symmetric Encryption #210
Conversation
6830ecb
to
f0ca760
Compare
Well, iOS does not verify padding at all, so we need to manually pkcs#7 pad our input and manually verify, to check for padding errors |
b1e58a1
to
7ccd3e2
Compare
indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Ciphertext.kt
Outdated
Show resolved
Hide resolved
supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/crypt/AESTest.kt
Outdated
Show resolved
Hide resolved
supreme/src/jvmMain/kotlin/at/asitplus/signum/supreme/crypt/Encryptor.jvm.kt
Outdated
Show resolved
Hide resolved
supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/crypt/Test.kt
Outdated
Show resolved
Hide resolved
supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/crypt/Encryptor.ios.kt
Outdated
Show resolved
Hide resolved
I'd love to see the changes in vck, to get a feeling how this is used |
indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MAC.kt
Outdated
Show resolved
Hide resolved
indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MAC.kt
Outdated
Show resolved
Hide resolved
supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/mac/MAC.kt
Outdated
Show resolved
Hide resolved
supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/crypt/Encryptor.kt
Outdated
Show resolved
Hide resolved
indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Ciphertext.kt
Outdated
Show resolved
Hide resolved
indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Ciphertext.kt
Outdated
Show resolved
Hide resolved
indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Ciphertext.kt
Outdated
Show resolved
Hide resolved
indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/Ciphertext.kt
Outdated
Show resolved
Hide resolved
...sable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SymmetricEncryptionAlgorithm.kt
Outdated
Show resolved
Hide resolved
c0359ed
to
7e9701d
Compare
supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/mac/MAC.kt
Outdated
Show resolved
Hide resolved
supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/crypt/Test.kt
Outdated
Show resolved
Hide resolved
7d4430c
to
683652e
Compare
683652e
to
77d13f8
Compare
As for our padding oracle discussion: I think this is a tricky one: internally, you may want to know whether padding or something else was the reason for a failed decryption. To the outside world, you never want to communicate such details, so I'm not convinced It is our job to hide such details. |
I don't think we should know even internally. It's a foot-gun waiting to go off. |
Well, we can't not know on the JVM, because it behaves differently for a padding error than for something else. Now it is at least consistent across platforms. |
b506f4e
to
64f7c16
Compare
...commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricEncryptionAlgorithm.kt
Outdated
Show resolved
Hide resolved
06e1eeb
to
c115066
Compare
d9881d0
to
f7508ae
Compare
bf8bc64
to
ee76fbf
Compare
diff --git a/CHANGELOG.md b/CHANGELOG.md index 130aa1a4..6e4f65a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,9 +3,15 @@ ## 3.0 ### NEXT -* Rename `ObjectIdentifier.parse` -> `ObjectIdentifier.decodeFromAsn1ContentBytes` in accordance with other similar functions -* Add dedicated Android targets (SDK 30 /JDK 1.8) to all modules - +* **Note: We are deprecating and will soon be removing the debug-only serialization for cryptographic datatypes like certificates, public keys, etc.** + * We support robust ASN.1 encoding and mapping from/to JOSE and COSE datatypes and our ASN.1 structures support pretty printing + * -> There is no need for this misleading serialization support for debugging anymore + * `@Serializable` suggests deserialization from JSON, CBOR, etc. works, which was never universally true + * kotlinx-serialization is not the way to go, given how robust Signum's ASN.1 engine has become + * `indispensable-asn1` is only lacking ASN.1 REAL for a proper kotlinx-serialization plugin supporting ASN.1 + * Once there, getting native ASN.1 serialization for kotlinx-serialization becomes a no-brainer + * This note will be prepended to the changelog entries for all point releases leading up to and including + Indispensable 4.0.0 / Supreme 1.0.0 * HMAC Support * Symmetric Encryption * Supported Algorithms @@ -20,6 +26,8 @@ * `ivLength` and `encryptionKeyLength` now return `BitLength` instead of `Int` * `text` is now properly called `identifier` * Moved `HazardousMaterials` annotation from `supreme` to `indispensable` +* Rename `ObjectIdentifier.parse` -> `ObjectIdentifier.decodeFromAsn1ContentBytes` in accordance with other similar functions +* Add dedicated Android targets (SDK 30 /JDK 1.8) to all modules ### 3.14.0 (Supreme 0.7.0)
diff --git a/.github/workflows/test-jvm.yml b/.github/workflows/test-jvm.yml index 8bbf7620..239a9a7b 100644 --- a/.github/workflows/test-jvm.yml +++ b/.github/workflows/test-jvm.yml @@ -19,5 +19,5 @@ jobs: if: success() || failure() with: name: JVM All Tests - path: indispensable-asn1/build/test-results/**/TEST*.xml,indispensable/build/test-results/**/TEST*.xml,indispensable-josef/build/test-results/**/TEST*.xml,indispensable-josef/build/test-results/**/TEST*.xml,supreme/build/test-results/**/TEST*.xml + path: indispensable-asn1/build/test-results/**/TEST*.xml,indispensable/build/test-results/**/TEST*.xml,indispensable-cosef/build/test-results/**/TEST*.xml,indispensable-josef/build/test-results/**/TEST*.xml,supreme/build/test-results/**/TEST*.xml reporter: java-junit diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4f65a0..69f7e813 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,14 @@ * This note will be prepended to the changelog entries for all point releases leading up to and including Indispensable 4.0.0 / Supreme 1.0.0 * HMAC Support + * **Clean up `RSAorHMAC` mess** + * Introduce `DataAuthenticationAlgorithm` encompassing Signatures and MACs + * Rename `RSAorHMAC` to `RSA` and introduce dedicated `MessageAuthenticationCode` and `HMAC` classes + * This caused **some breaking shifts and cleanups** in conversion methods between indispensable, josef, cosef, and JCA types + * There is no direct mapping between `SignatureAlgorithm` and COSE/JOSE algorithms anymore, because JOSE/COSE also incorporate MAC + * Hence, for both `JwsAlgorithm` and `CoseAlgorithm`, the following properties have been introduced on the companions: + * `signatureAlgorithms` + * `messageAuthenticationCodes` * Symmetric Encryption * Supported Algorithms * AES diff --git a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt index 6bff9f32..201ab745 100644 --- a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt +++ b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt @@ -11,7 +11,6 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size @@ -46,28 +45,26 @@ import at.asitplus.signum.indispensable.CryptoSignature import at.asitplus.signum.indispensable.ECCurve import at.asitplus.signum.indispensable.RSAPadding import at.asitplus.signum.indispensable.SignatureAlgorithm -import at.asitplus.signum.indispensable.SpecializedSignatureAlgorithm import at.asitplus.signum.indispensable.X509SignatureAlgorithm import at.asitplus.signum.indispensable.nativeDigest +import at.asitplus.signum.indispensable.pki.X509Certificate import at.asitplus.signum.supreme.dsl.PREFERRED import at.asitplus.signum.supreme.sign.Signer import at.asitplus.signum.supreme.sign.makeVerifier import at.asitplus.signum.supreme.sign.verify import at.asitplus.cryptotest.theme.AppTheme import at.asitplus.cryptotest.theme.LocalThemeIsDark -import at.asitplus.signum.indispensable.CryptoPublicKey -import at.asitplus.signum.indispensable.KeyAgreementPrivateValue import at.asitplus.signum.indispensable.jsonEncoded -import at.asitplus.signum.supreme.SecretExposure -import at.asitplus.signum.supreme.agree.Ephemeral import at.asitplus.signum.supreme.asKmmResult import at.asitplus.signum.supreme.os.PlatformSignerConfigurationBase +import at.asitplus.signum.supreme.os.PlatformSigningKeyConfigurationBase +import at.asitplus.signum.supreme.os.PlatformSigningProvider import at.asitplus.signum.supreme.os.SignerConfiguration import at.asitplus.signum.supreme.os.SigningProvider import at.asitplus.signum.supreme.sign.Verifier import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier -import io.ktor.util.encodeBase64 +import io.ktor.util.decodeBase64Bytes import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch @@ -100,10 +97,7 @@ private class getter<T>(private val fn: () -> T) { operator fun getValue(nothing: Nothing?, property: KProperty<*>): T = fn() } -@OptIn( - ExperimentalCoroutinesApi::class, - SecretExposure::class -) +@OptIn(ExperimentalStdlibApi::class, ExperimentalCoroutinesApi::class) @Composable internal fun App() { @@ -120,7 +114,7 @@ internal fun App() { X509SignatureAlgorithm.RS512 ) var keyAlgorithm by remember { - mutableStateOf<SpecializedSignatureAlgorithm>( + mutableStateOf<X509SignatureAlgorithm>( X509SignatureAlgorithm.ES256 ) } @@ -143,12 +137,6 @@ internal fun App() { } val signingPossible by getter { currentKey?.isSuccess == true } var signatureData by remember { mutableStateOf<KmmResult<CryptoSignature>?>(null) } - var ephemeralKey by remember { - mutableStateOf<KeyAgreementPrivateValue.ECDH?>( - null - ) - } - var agreedKey by remember { mutableStateOf<KmmResult<ByteArray>?>(null) } val signatureDataStr by getter { signatureData?.fold(onSuccess = Any::toString) { Napier.e("Signature failed", it) @@ -169,14 +157,6 @@ internal fun App() { var genTextOverride by remember { mutableStateOf<String?>(null) } val genText by getter { genTextOverride ?: "Generate" } - fun genEphemeralKey(){ - ephemeralKey = if (currentKey?.getOrNull() is CryptoPublicKey.EC) - KeyAgreementPrivateValue.ECDH.Ephemeral((currentKey!!.getOrThrow() as CryptoPublicKey.EC).curve).getOrNull() - else null - agreedKey=null - } - - Column( modifier = Modifier.fillMaxSize().verticalScroll(ScrollState(0), enabled = true) .windowInsetsPadding(WindowInsets.safeDrawing) @@ -327,16 +307,12 @@ internal fun App() { canGenerate = false genTextOverride = "Creating…" currentSigner = Provider.createSigningKey(ALIAS) { - when (val alg = keyAlgorithm.algorithm) { is SignatureAlgorithm.ECDSA -> { [email protected] { - curve = alg.requiredCurve - ?: ECCurve.entries.find { it.nativeDigest == alg.digest }!! + curve = alg.requiredCurve ?: + ECCurve.entries.find { it.nativeDigest == alg.digest }!! digests = setOf(alg.digest) - purposes { - keyAgreement = true - } } } @@ -351,27 +327,29 @@ internal fun App() { else -> error("unreachable") } - signer(SIGNER_CONFIG) + if (this is PlatformSigningKeyConfigurationBase) { + signer(SIGNER_CONFIG) - val timeout = runCatching { - biometricAuth.substringBefore("s").trim().toInt() - }.getOrNull() + val timeout = runCatching { + biometricAuth.substringBefore("s").trim().toInt() + }.getOrNull() - if (attestation || timeout != null) { - hardware { - backing = PREFERRED - if (attestation) { - attestation { - challenge = Random.nextBytes(16) + if (attestation || timeout != null) { + hardware { + backing = PREFERRED + if (attestation) { + attestation { + challenge = Random.nextBytes(16) + } } - } - if (timeout != null) { - protection { - this.timeout = timeout.seconds - factors { - biometry = true - deviceLock = true + if (timeout != null) { + protection { + this.timeout = timeout.seconds + factors { + biometry = true + deviceLock = true + } } } } @@ -384,8 +362,6 @@ internal fun App() { Napier.w { "Signing possible: ${currentKey?.isSuccess}" } canGenerate = true genTextOverride = null - - genEphemeralKey() } }, modifier = Modifier.padding(start = 16.dp) @@ -409,7 +385,6 @@ internal fun App() { //loadPubKey().let { Napier.w { "PubKey retrieved from native: $it" } } canGenerate = true genTextOverride = null - genEphemeralKey() } }, modifier = Modifier.padding(start = 16.dp, end = 16.dp) @@ -457,6 +432,7 @@ internal fun App() { Button( onClick = { + Napier.w { "input: $inputData" } Napier.w { "signingKey: $currentKey" } CoroutineScope(context).launch { @@ -465,61 +441,13 @@ internal fun App() { .transform { it.sign(data).asKmmResult() } .also { signatureData = it; verifyState = null } } + }, - modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(), + + modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), enabled = signingPossible - ) { Text("Sign") } - - if (signingPossible && (currentKey?.getOrNull() is CryptoPublicKey.EC)) { - Napier.i { "Ephemeral key: $ephemeralKey" } - Spacer(Modifier.height(8.dp)) - ephemeralKey?.let { ephemeralKey -> - OutlinedTextField(value = ephemeralKey.publicValue.toString(), - modifier = Modifier.fillMaxWidth().padding(16.dp), - minLines = 1, - textStyle = TextStyle.Default.copy(fontSize = 10.sp), - readOnly = true, - onValueChange = {}, - label = { Text("Random faux-external key for ECDH") }) - Button( - onClick = { - Napier.w { "input: $inputData" } - Napier.w { "signingKey: $currentKey" } - CoroutineScope(context).launch { - - agreedKey = - (currentSigner!!.getOrThrow() as Signer.ECDSA).keyAgreement( - ephemeralKey.publicValue - ) - Napier.i { - "ECDH Key of ext_piv + hardware-backed_pub: ${ - agreedKey?.getOrNull()?.encodeBase64() - }" - } - - Napier.i { - "ECDH Key of ext_pub + hardware-backed_priv: ${ - agreedKey?.getOrNull()?.encodeBase64() - }" - } - } - - }, - modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(), - ) { Text("Perform ECDH key agreement") } - } - - if (agreedKey != null) { - OutlinedTextField(value = - "Computed from ext_pub + hardware-backed_priv:\n" + agreedKey?.map { it.encodeBase64() } - ?.getOrElse { it.message ?: it::class.simpleName ?: "" }, - modifier = Modifier.fillMaxWidth().padding(16.dp), - minLines = 1, - textStyle = TextStyle.Default.copy(fontSize = 10.sp), - readOnly = true, - onValueChange = {}, - label = { Text("Agreed-upon secret") }) - } + ) { + Text("Sign") } if (signatureData != null) { diff --git a/docs/docs/indispensable.md b/docs/docs/indispensable.md index 4376b599..d9c3aa60 100644 --- a/docs/docs/indispensable.md +++ b/docs/docs/indispensable.md @@ -104,9 +104,9 @@ The following functions provide interop functionality with platform types. ### JVM/Android * `SignatureAlgorithm.getJCASignatureInstance()` gets a pre-configured JCA instance for this algorithm -* `SpecializedSignatureAlgorithm.getJCASignatureInstance()` gets a pre-configured JCA instance for this algorithm +* `X509SignatureAlgorithm.getJCASignatureInstance()` gets a pre-configured JCA instance for this algorithm * `SignatureAlgorithm.getJCASignatureInstancePreHashed()` gets a pre-configured JCA instance for pre-hashed data for this algorithm -* `SpecializedSignatureAlgorithm.getJCASignatureInstancePreHashed()` gets a pre-configured JCA instance for pre-hashed data for this algorithm +* `X509SignatureAlgorithm.getJCASignatureInstancePreHashed()` gets a pre-configured JCA instance for pre-hashed data for this algorithm <br> @@ -148,7 +148,7 @@ The following functions provide interop functionality with platform types. * `CryptoSignature.jcaSignatureBytes` returns the JCA-native encoded representation of a signature * `CryptoSignature.parseFromJca()` returns a signature object form a JCA-native encoded representation of a signature * `CryptoSignature.EC.parseFromJca()` returns an EC signature object form a JCA-native encoded representation of a signature -* `CryptoSignature.RSAorHMAC.parseFromJca()` returns an RSA signature object form a JCA-native encoded representation of a signature +* `CryptoSignature.RSA.parseFromJca()` returns an RSA signature object form a JCA-native encoded representation of a signature * `CryptoSignature.EC.parseFromJcaP1363` parses a signature produced by the JCA digestWithECDSAinP1363Format algorithm. * `X509Certificate.toJcaCertificate()` converts the certificate to a JCA-native `X509Certificate` * `java.security.cert.X509Certificate.toKmpCertificate()` converts a JCA-native certificate to a Signum `X509Certificate` diff --git a/indispensable-asn1/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt b/indispensable-asn1/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt index b8f2d98b..b6837c26 100644 --- a/indispensable-asn1/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt +++ b/indispensable-asn1/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt @@ -13,7 +13,9 @@ class Asn1TagMismatchException(val expected: Asn1Element.Tag, val actual: Asn1El class Asn1StructuralException(message: String, cause: Throwable? = null) : Asn1Exception(message, cause) -class Asn1OidException(message: String, val oid: ObjectIdentifier) : Asn1Exception(message) +class Asn1OidException(message: String, val oid: ObjectIdentifier) : Asn1Exception(message){ + constructor(oid: ObjectIdentifier):this("Unknown oid $oid", oid) +} /** * Runs [block] inside [catching] and encapsulates any thrown exception in an [Asn1Exception] unless it already is one diff --git a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseAlgorithm.kt b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseAlgorithm.kt index 979cb602..94bfe3f1 100644 --- a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseAlgorithm.kt +++ b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseAlgorithm.kt @@ -1,11 +1,10 @@ package at.asitplus.signum.indispensable.cosef +import at.asitplus.KmmResult import at.asitplus.catching -import at.asitplus.signum.indispensable.Digest -import at.asitplus.signum.indispensable.ECCurve -import at.asitplus.signum.indispensable.RSAPadding -import at.asitplus.signum.indispensable.SignatureAlgorithm -import at.asitplus.signum.indispensable.SpecializedSignatureAlgorithm +import at.asitplus.signum.indispensable.* +import at.asitplus.signum.indispensable.mac.HMAC +import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -18,7 +17,7 @@ import kotlinx.serialization.encoding.Encoder * See [COSE Algorithm Registry](https://www.iana.org/assignments/cose/cose.xhtml) */ @Serializable(with = CoseAlgorithmSerializer::class) -enum class CoseAlgorithm(val value: Int): SpecializedSignatureAlgorithm { +enum class CoseAlgorithm(val value: Int) : SpecializedDataIntegrityAlgorithm { // ECDSA with SHA-size ES256(-7), @@ -43,22 +42,52 @@ enum class CoseAlgorithm(val value: Int): SpecializedSignatureAlgorithm { // RSASSA-PKCS1-v1_5 using SHA-1 RS1(-65535); - val digest: Digest get() = when(this) { - RS1 -> Digest.SHA1 - ES256, HS256, PS256, RS256 -> Digest.SHA256 - ES384, HS384, PS384, RS384 -> Digest.SHA384 - ES512, HS512, PS512, RS512 -> Digest.SHA512 + + companion object { + /** + * encompasses only signature algorithms, filtering out MACs + */ + val signatureAlgorithms = listOf( + ES256, + ES384, + ES512, + + PS256, + PS384, + PS512, + + RS256, + RS384, + RS512, + RS1 + ).map { it.algorithm as SignatureAlgorithm } + + /** + * encompasses only MACs, filtering out signature algorithms + */ + val messageAuthenticationCodes = listOf(HS256, HS384, HS512).map { it.algorithm as MessageAuthenticationCode } + + } - override val algorithm: SignatureAlgorithm get() = when (this) { - ES256 -> SignatureAlgorithm.ECDSA(Digest.SHA256, ECCurve.SECP_256_R_1) - ES384 -> SignatureAlgorithm.ECDSA(Digest.SHA384, ECCurve.SECP_384_R_1) - ES512 -> SignatureAlgorithm.ECDSA(Digest.SHA512, ECCurve.SECP_521_R_1) + val digest: Digest + get() = when (this) { + RS1 -> Digest.SHA1 + ES256, HS256, PS256, RS256 -> Digest.SHA256 + ES384, HS384, PS384, RS384 -> Digest.SHA384 + ES512, HS512, PS512, RS512 -> Digest.SHA512 + } - HS256, HS384, HS512 -> SignatureAlgorithm.HMAC(this.digest) - PS256, PS384, PS512 -> SignatureAlgorithm.RSA(this. digest, RSAPadding.PSS) - RS1, RS256, RS384, RS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PKCS1) - } + override val algorithm: DataIntegrityAlgorithm + get() = when (this) { + ES256 -> SignatureAlgorithm.ECDSA(Digest.SHA256, ECCurve.SECP_256_R_1) + ES384 -> SignatureAlgorithm.ECDSA(Digest.SHA384, ECCurve.SECP_384_R_1) + ES512 -> SignatureAlgorithm.ECDSA(Digest.SHA512, ECCurve.SECP_521_R_1) + + HS256, HS384, HS512 -> HMAC.byDigest(digest) + PS256, PS384, PS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PSS) + RS1, RS256, RS384, RS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PKCS1) + } } object CoseAlgorithmSerializer : KSerializer<CoseAlgorithm> { @@ -77,8 +106,9 @@ object CoseAlgorithmSerializer : KSerializer<CoseAlgorithm> { } + /** Tries to find a matching COSE algorithm. Note that COSE imposes curve restrictions on ECDSA based on the digest. */ -fun SignatureAlgorithm.toCoseAlgorithm() = catching { +fun DataIntegrityAlgorithm.toCoseAlgorithm(): KmmResult<CoseAlgorithm> = catching { when (this) { is SignatureAlgorithm.ECDSA -> when (this.digest) { Digest.SHA256 -> CoseAlgorithm.ES256 @@ -86,6 +116,7 @@ fun SignatureAlgorithm.toCoseAlgorithm() = catching { Digest.SHA512 -> CoseAlgorithm.ES512 else -> throw IllegalArgumentException("ECDSA with ${this.digest} is unsupported by COSE") } + is SignatureAlgorithm.RSA -> when (this.padding) { RSAPadding.PKCS1 -> when (this.digest) { Digest.SHA1 -> CoseAlgorithm.RS1 @@ -93,6 +124,7 @@ fun SignatureAlgorithm.toCoseAlgorithm() = catching { Digest.SHA384 -> CoseAlgorithm.RS384 Digest.SHA512 -> CoseAlgorithm.RS512 } + RSAPadding.PSS -> when (this.digest) { Digest.SHA256 -> CoseAlgorithm.PS256 Digest.SHA384 -> CoseAlgorithm.PS384 @@ -100,15 +132,18 @@ fun SignatureAlgorithm.toCoseAlgorithm() = catching { else -> throw IllegalArgumentException("RSA-PSS with ${this.digest} is unsupported by COSE") } } - is SignatureAlgorithm.HMAC -> when (this.digest) { + + is HMAC -> when (this.digest) { Digest.SHA256 -> CoseAlgorithm.HS256 Digest.SHA384 -> CoseAlgorithm.HS384 Digest.SHA512 -> CoseAlgorithm.HS512 else -> throw IllegalArgumentException("HMAC with ${this.digest} is unsupported by COSE") } + + else -> throw IllegalArgumentException("UnsupportedAlgorithm $this") } } /** Tries to find a matching COSE algorithm. Note that COSE imposes curve restrictions on ECDSA based on the digest. */ -fun SpecializedSignatureAlgorithm.toCoseAlgorithm() = +fun SpecializedDataIntegrityAlgorithm.toCoseAlgorithm() = this.algorithm.toCoseAlgorithm() diff --git a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseSignedSerializer.kt b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseSignedSerializer.kt index c7f98eeb..e571328e 100644 --- a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseSignedSerializer.kt +++ b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseSignedSerializer.kt @@ -66,7 +66,7 @@ class CoseSignedSerializer<P : Any?>( ): CryptoSignature.RawByteEncodable = if (protectedHeader.usesEC() ?: unprotectedHeader?.usesEC() ?: (size < 2048)) CryptoSignature.EC.fromRawBytes(this) - else CryptoSignature.RSAorHMAC(this) + else CryptoSignature.RSA(this) private fun ByteArray?.toNullablePayload(): P? = when (this) { null -> null @@ -80,7 +80,7 @@ class CoseSignedSerializer<P : Any?>( } else { runCatching { fromBytes() } .getOrElse { fromByteStringWrapper() } - // if it still fails, the input is not valid + // if it still fails, the input not valid } private fun ByteArray.fromBytes(): P = diff --git a/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseEqualsTest.kt b/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseEqualsTest.kt index dc47dd22..d9d737c7 100644 --- a/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseEqualsTest.kt +++ b/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseEqualsTest.kt @@ -21,14 +21,14 @@ class CoseEqualsTest : FreeSpec({ protectedHeader = CoseHeader(), unprotectedHeader = null, payload = bytes, - signature = CryptoSignature.RSAorHMAC(bytes), + signature = CryptoSignature.RSA(bytes), payloadSerializer = ByteArraySerializer(), ) val bytesSigned2 = CoseSigned.create( protectedHeader = CoseHeader(), unprotectedHeader = null, payload = bytes, - signature = CryptoSignature.RSAorHMAC(bytes), + signature = CryptoSignature.RSA(bytes), payloadSerializer = ByteArraySerializer(), ) @@ -42,14 +42,14 @@ class CoseEqualsTest : FreeSpec({ protectedHeader = CoseHeader(), unprotectedHeader = null, payload = reversed, - signature = CryptoSignature.RSAorHMAC(reversed), + signature = CryptoSignature.RSA(reversed), payloadSerializer = ByteArraySerializer(), ) val reversedSigned2 = CoseSigned.create( protectedHeader = CoseHeader(), unprotectedHeader = null, payload = reversed, - signature = CryptoSignature.RSAorHMAC(reversed), + signature = CryptoSignature.RSA(reversed), payloadSerializer = ByteArraySerializer(), ) @@ -80,14 +80,14 @@ class CoseEqualsTest : FreeSpec({ protectedHeader = CoseHeader(), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSAorHMAC(bytes), + signature = CryptoSignature.RSA(bytes), payloadSerializer = DataClass.serializer(), ) val bytesSigned2 = CoseSigned.create( protectedHeader = CoseHeader(), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSAorHMAC(bytes), + signature = CryptoSignature.RSA(bytes), payloadSerializer = DataClass.serializer(), ) @@ -102,14 +102,14 @@ class CoseEqualsTest : FreeSpec({ protectedHeader = CoseHeader(), unprotectedHeader = null, payload = reversed, - signature = CryptoSignature.RSAorHMAC(bytes), + signature = CryptoSignature.RSA(bytes), payloadSerializer = DataClass.serializer(), ) val reversedSigned2 = CoseSigned.create( protectedHeader = CoseHeader(), unprotectedHeader = null, payload = reversed, - signature = CryptoSignature.RSAorHMAC(bytes), + signature = CryptoSignature.RSA(bytes), payloadSerializer = DataClass.serializer(), ) diff --git a/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseSerializationTest.kt b/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseSerializationTest.kt index bc0c9cb1..dd25e09c 100644 --- a/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseSerializationTest.kt +++ b/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseSerializationTest.kt @@ -30,7 +30,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.ES256), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSAorHMAC(byteArrayOf()), + signature = CryptoSignature.RSA(byteArrayOf()), payloadSerializer = ByteStringWrapper.serializer(String.serializer()) ) } @@ -42,7 +42,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), + signature = CryptoSignature.RSA("bar".encodeToByteArray()), payloadSerializer = ByteArraySerializer(), ) @@ -55,7 +55,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), + signature = CryptoSignature.RSA("bar".encodeToByteArray()), payloadSerializer = DataClass.serializer(), ) @@ -68,7 +68,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), //RSAorHMAC because EC expects tuple + signature = CryptoSignature.RSA("bar".encodeToByteArray()), //RSA because EC expects tuple payloadSerializer = ByteArraySerializer(), ) val serialized = cose.serialize(ByteArraySerializer()) @@ -86,7 +86,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = null, - signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), //RSAorHMAC because EC expects tuple + signature = CryptoSignature.RSA("bar".encodeToByteArray()), //RSA because EC expects tuple payloadSerializer = ByteArraySerializer(), ) val serialized = cose.serialize(ByteArraySerializer()) @@ -129,7 +129,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), //RSAorHMAC because EC expects tuple + signature = CryptoSignature.RSA("bar".encodeToByteArray()), //RSA because EC expects tuple payloadSerializer = DataClass.serializer(), ) val serialized = cose.serialize(DataClass.serializer()) @@ -195,7 +195,7 @@ class CoseSerializationTest : FreeSpec({ val inputObject = CoseSigned.create( protectedHeader = header, payload = payload, - signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), + signature = CryptoSignature.RSA("bar".encodeToByteArray()), payloadSerializer = ByteArraySerializer(), ).prepareCoseSignatureInput(byteArrayOf()) .encodeToString(Base16()) @@ -217,7 +217,7 @@ class CoseSerializationTest : FreeSpec({ val inputObject = CoseSigned.create( protectedHeader = header, payload = payload, - signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), + signature = CryptoSignature.RSA("bar".encodeToByteArray()), payloadSerializer = DataClass.serializer(), ).prepareCoseSignatureInput(byteArrayOf()) .encodeToString(Base16()) diff --git a/indispensable-cosef/src/jvmTest/kotlin/ConversionTests.kt b/indispensable-cosef/src/jvmTest/kotlin/ConversionTests.kt index 02270b0d..54737142 100644 --- a/indispensable-cosef/src/jvmTest/kotlin/ConversionTests.kt +++ b/indispensable-cosef/src/jvmTest/kotlin/ConversionTests.kt @@ -23,9 +23,9 @@ class ConversionTests : FreeSpec({ } } "COSE -> X509 -> COSE is stable" - { - withData(CoseAlgorithm.entries) { + withData(CoseAlgorithm.signatureAlgorithms/*here we filter by using only sig alg*/) { it.toX509SignatureAlgorithm().getOrNull()?.let { x509 -> - x509.toCoseAlgorithm() shouldSucceedWith it + x509.toCoseAlgorithm().onSuccess { it.algorithm } shouldSucceedWith it.toCoseAlgorithm().getOrThrow() //here we go back to cose, after initially filtering, because cose does not discriminate between MAC and sig } } } diff --git a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsAlgorithm.kt b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsAlgorithm.kt index 2f8ecd28..9ec4e38e 100644 --- a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsAlgorithm.kt +++ b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsAlgorithm.kt @@ -1,11 +1,10 @@ package at.asitplus.signum.indispensable.josef +import at.asitplus.KmmResult import at.asitplus.catching -import at.asitplus.signum.indispensable.Digest -import at.asitplus.signum.indispensable.ECCurve -import at.asitplus.signum.indispensable.RSAPadding -import at.asitplus.signum.indispensable.SignatureAlgorithm -import at.asitplus.signum.indispensable.SpecializedSignatureAlgorithm +import at.asitplus.signum.indispensable.* +import at.asitplus.signum.indispensable.mac.HMAC +import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -19,7 +18,7 @@ import kotlinx.serialization.encoding.Encoder * Since we support only JWS algorithms (with one exception), this class is called what it's called. */ @Serializable(with = JwsAlgorithmSerializer::class) -enum class JwsAlgorithm(override val identifier: String) : JsonWebAlgorithm, SpecializedSignatureAlgorithm { +enum class JwsAlgorithm(override val identifier: String) : JsonWebAlgorithm, SpecializedDataIntegrityAlgorithm { ES256("ES256"), ES384("ES384"), @@ -42,28 +41,58 @@ enum class JwsAlgorithm(override val identifier: String) : JsonWebAlgorithm, Spe */ NON_JWS_SHA1_WITH_RSA("RS1"); - val digest: Digest get() = when(this) { - NON_JWS_SHA1_WITH_RSA -> Digest.SHA1 - ES256, HS256, PS256, RS256 -> Digest.SHA256 - ES384, HS384, PS384, RS384 -> Digest.SHA384 - ES512, HS512, PS512, RS512 -> Digest.SHA512 + + companion object { + /** + * encompasses only signature algorithms, filtering out MACs + */ + val signatureAlgorithms = listOf( + ES256, + ES384, + ES512, + + PS256, + PS384, + PS512, + + RS256, + RS384, + RS512, + NON_JWS_SHA1_WITH_RSA + ).map { it.algorithm as SignatureAlgorithm } + + /** + * Encompasses only MACs, filtering our signature algorithms + */ + val messageAuthenticationCodes = listOf(HS256, HS384, HS512).map { it.algorithm as MessageAuthenticationCode } + } - override val algorithm: SignatureAlgorithm get() = when (this) { - ES256, ES384, ES512 -> SignatureAlgorithm.ECDSA(this.digest, this.ecCurve!!) - HS256, HS384, HS512 -> SignatureAlgorithm.HMAC(this.digest) - PS256, PS384, PS512 -> SignatureAlgorithm.RSA(this. digest, RSAPadding.PSS) - NON_JWS_SHA1_WITH_RSA, RS256, RS384, RS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PKCS1) - } + val digest: Digest + get() = when (this) { + NON_JWS_SHA1_WITH_RSA -> Digest.SHA1 + ES256, HS256, PS256, RS256 -> Digest.SHA256 + ES384, HS384, PS384, RS384 -> Digest.SHA384 + ES512, HS512, PS512, RS512 -> Digest.SHA512 + } + + override val algorithm: DataIntegrityAlgorithm + get() = when (this) { + ES256, ES384, ES512 -> SignatureAlgorithm.ECDSA(this.digest, this.ecCurve!!) + HS256, HS384, HS512 -> HMAC.byDigest(this.digest) + PS256, PS384, PS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PSS) + NON_JWS_SHA1_WITH_RSA, RS256, RS384, RS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PKCS1) + } /** The curve to create signatures on. * This is fixed by RFC7518, as opposed to X.509 where other combinations are possible. */ - val ecCurve: ECCurve? get() = when (this) { - ES256 -> ECCurve.SECP_256_R_1 - ES384 -> ECCurve.SECP_384_R_1 - ES512 -> ECCurve.SECP_521_R_1 - else -> null - } + val ecCurve: ECCurve? + get() = when (this) { + ES256 -> ECCurve.SECP_256_R_1 + ES384 -> ECCurve.SECP_384_R_1 + ES512 -> ECCurve.SECP_521_R_1 + else -> null + } } object JwsAlgorithmSerializer : KSerializer<JwsAlgorithm> { @@ -80,8 +109,12 @@ object JwsAlgorithmSerializer : KSerializer<JwsAlgorithm> { } } + +fun SpecializedDataIntegrityAlgorithm.toJwsAlgorithm(): KmmResult<JwsAlgorithm> = + algorithm.toJwsAlgorithm() + /** Tries to find a matching JWS algorithm. Note that JWS imposes curve restrictions on ECDSA based on the digest. */ -fun SignatureAlgorithm.toJwsAlgorithm() = catching { +fun DataIntegrityAlgorithm.toJwsAlgorithm(): KmmResult<JwsAlgorithm> = catching { when (this) { is SignatureAlgorithm.ECDSA -> when (this.digest) { Digest.SHA256 -> JwsAlgorithm.ES256 @@ -89,6 +122,7 @@ fun SignatureAlgorithm.toJwsAlgorithm() = catching { Digest.SHA512 -> JwsAlgorithm.ES512 else -> throw IllegalArgumentException("ECDSA with ${this.digest} is unsupported by JWS") } + is SignatureAlgorithm.RSA -> when (this.padding) { RSAPadding.PKCS1 -> when (this.digest) { Digest.SHA1 -> JwsAlgorithm.NON_JWS_SHA1_WITH_RSA @@ -96,24 +130,26 @@ fun SignatureAlgorithm.toJwsAlgorithm() = catching { Digest.SHA384 -> JwsAlgorithm.RS384 Digest.SHA512 -> JwsAlgorithm.RS512 } + RSAPadding.PSS -> when (this.digest) { Digest.SHA256 -> JwsAlgorithm.PS256 Digest.SHA384 -> JwsAlgorithm.PS384 Digest.SHA512 -> JwsAlgorithm.PS512 else -> throw IllegalArgumentException("RSA-PSS with ${this.digest} is unsupported by JWS") } + } - is SignatureAlgorithm.HMAC -> when (this.digest) { + + is HMAC -> when (this.digest) { Digest.SHA256 -> JwsAlgorithm.HS256 Digest.SHA384 -> JwsAlgorithm.HS384 Digest.SHA512 -> JwsAlgorithm.HS512 else -> throw IllegalArgumentException("HMAC with ${this.digest} is unsupported by JWS") } + + else -> throw IllegalArgumentException("UnsupportedAlgorithm $this") + } } -/** Tries to find a matching JWS algorithm. Note that JWS imposes curve restrictions on ECDSA based on the digest. */ -fun SpecializedSignatureAlgorithm.toJwsAlgorithm() = - this.algorithm.toJwsAlgorithm() - diff --git a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt index 0bc1838e..6c31b6e5 100644 --- a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt +++ b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt @@ -74,7 +74,7 @@ data class JwsSigned<out P : Any>( val payload = inputParts[1] val signature = with(inputParts[2]) { when (val curve = header.algorithm.ecCurve) { - null -> CryptoSignature.RSAorHMAC(this) + null -> CryptoSignature.RSA(this) else -> CryptoSignature.EC.fromRawBytes(curve, this) } } diff --git a/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/ConversionTest.kt b/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/ConversionTest.kt index 69b0c60f..d8f2610c 100644 --- a/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/ConversionTest.kt +++ b/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/ConversionTest.kt @@ -17,9 +17,9 @@ class ConversionTest : FreeSpec({ } } "JWS -> X509 -> JWS is stable" - { - withData(JwsAlgorithm.entries) { + withData(JwsAlgorithm.signatureAlgorithms /*here we filter s.t. only sig alg are fed into the test*/) { it.toX509SignatureAlgorithm().getOrNull()?.let { x509 -> - x509.toJwsAlgorithm() shouldSucceedWith it + x509.toJwsAlgorithm() shouldSucceedWith it.toJwsAlgorithm().getOrThrow() /* and here we convert the sig algs back to JWS alg*/ } } } diff --git a/indispensable/src/androidJvmMain/kotlin/at/asitplus/signum/indispensable/JcaExtensions.kt b/indispensable/src/androidJvmMain/kotlin/at/asitplus/signum/indispensable/JcaExtensions.kt index 90e17c91..273e91aa 100644 --- a/indispensable/src/androidJvmMain/kotlin/at/asitplus/signum/indispensable/JcaExtensions.kt +++ b/indispensable/src/androidJvmMain/kotlin/at/asitplus/signum/indispensable/JcaExtensions.kt @@ -4,6 +4,7 @@ import at.asitplus.KmmResult import at.asitplus.catching import at.asitplus.signum.indispensable.asn1.toAsn1Integer import at.asitplus.signum.indispensable.asn1.toJavaBigInteger +import at.asitplus.signum.indispensable.mac.HMAC import at.asitplus.signum.internals.isAndroid import at.asitplus.signum.indispensable.pki.X509Certificate import com.ionspin.kotlin.bignum.integer.base63.toJavaBigInteger @@ -186,7 +187,7 @@ fun CryptoPublicKey.Companion.fromJcaPublicKey(publicKey: PublicKey): KmmResult< val CryptoSignature.jcaSignatureBytes: ByteArray get() = when (this) { is CryptoSignature.EC -> encodeToDer() - is CryptoSignature.RSAorHMAC -> rawByteArray + is CryptoSignature.RSA -> rawByteArray } /** @@ -199,7 +200,7 @@ fun CryptoSignature.Companion.parseFromJca( if (algorithm is SignatureAlgorithm.ECDSA) CryptoSignature.EC.parseFromJca(input) else - CryptoSignature.RSAorHMAC.parseFromJca(input) + CryptoSignature.RSA.parseFromJca(input) fun CryptoSignature.Companion.parseFromJca( input: ByteArray, @@ -218,8 +219,8 @@ fun CryptoSignature.EC.Companion.parseFromJca(input: ByteArray) = fun CryptoSignature.EC.Companion.parseFromJcaP1363(input: ByteArray) = CryptoSignature.EC.fromRawBytes(input) -fun CryptoSignature.RSAorHMAC.Companion.parseFromJca(input: ByteArray) = - CryptoSignature.RSAorHMAC(input) +fun CryptoSignature.RSA.Companion.parseFromJca(input: ByteArray) = + CryptoSignature.RSA(input) /** * Converts this [X509Certificate] to a [java.security.cert.X509Certificate]. diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt index d8f59e9d..b5033a80 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt @@ -217,7 +217,7 @@ sealed interface CryptoSignature : Asn1Encodable<Asn1Element> { } - class RSAorHMAC private constructor (rawBytes: ByteArray?, x509Element: Asn1Primitive?) : CryptoSignature, RawByteEncodable { + class RSA private constructor (rawBytes: ByteArray?, x509Element: Asn1Primitive?) : CryptoSignature, RawByteEncodable { constructor(rawBytes: ByteArray) : this(rawBytes, null) constructor(x509Element: Asn1Primitive) : this(null, x509Element) @@ -241,16 +241,16 @@ sealed interface CryptoSignature : Asn1Encodable<Asn1Element> { if (this === other) return true if (other == null || this::class != other::class) return false - other as RSAorHMAC + other as RSA return signature == other.signature } - companion object : Asn1Decodable<Asn1Element, RSAorHMAC> { + companion object : Asn1Decodable<Asn1Element, RSA> { @Throws(Asn1Exception::class) - override fun doDecode(src: Asn1Element): RSAorHMAC { + override fun doDecode(src: Asn1Element): RSA { src as Asn1Primitive - return RSAorHMAC(src) + return RSA(src) } } } @@ -259,7 +259,7 @@ sealed interface CryptoSignature : Asn1Encodable<Asn1Element> { @Throws(Asn1Exception::class) override fun doDecode(src: Asn1Element): CryptoSignature = runRethrowing { when (src.tag) { - Asn1Element.Tag.BIT_STRING -> RSAorHMAC.decodeFromTlv(src) + Asn1Element.Tag.BIT_STRING -> RSA.decodeFromTlv(src) Asn1Element.Tag.SEQUENCE -> EC.decodeFromTlv(src) else -> throw Asn1Exception("Unknown Signature Format") diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/DataIntegrityAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/DataIntegrityAlgorithm.kt new file mode 100644 index 00000000..d85fb548 --- /dev/null +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/DataIntegrityAlgorithm.kt @@ -0,0 +1,14 @@ +package at.asitplus.signum.indispensable + +import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode + +/** + * Umbrella interface encompassing _data integrity algorithms_: + * * Message Authentication Codes ([MessageAuthenticationCode]) + * * Digital Signatures ([SignatureAlgorithm]) + */ +interface DataIntegrityAlgorithm{ + companion object { + val entries: Iterable<DataIntegrityAlgorithm> = MessageAuthenticationCode.entries + SignatureAlgorithm.entries + } +} \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt index 1659a602..84739935 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt @@ -5,11 +5,7 @@ enum class RSAPadding { PSS; } -sealed interface SignatureAlgorithm { - data class HMAC( - /** The digest to use */ - val digest: Digest - ) : SignatureAlgorithm +sealed interface SignatureAlgorithm : DataIntegrityAlgorithm { data class ECDSA( /** The digest to apply to the data, or `null` to directly process the raw data. */ @@ -38,15 +34,22 @@ sealed interface SignatureAlgorithm { val RSAwithSHA384andPSSPadding = RSA(Digest.SHA384, RSAPadding.PSS) val RSAwithSHA512andPSSPadding = RSA(Digest.SHA512, RSAPadding.PSS) - @Deprecated("Not yet implemented", level = DeprecationLevel.ERROR) - val HMACwithSHA256 = HMAC(Digest.SHA256) - @Deprecated("Not yet implemented", level = DeprecationLevel.ERROR) - val HMACwithSHA384 = HMAC(Digest.SHA384) - @Deprecated("Not yet implemented", level = DeprecationLevel.ERROR) - val HMACwithSHA512 = HMAC(Digest.SHA512) + val entries: Iterable<SignatureAlgorithm> = listOf( + ECDSAwithSHA256, + ECDSAwithSHA384, + ECDSAwithSHA512, + + RSAwithSHA256andPSSPadding, + RSAwithSHA384andPSSPadding, + RSAwithSHA512andPSSPadding, + + RSAwithSHA256andPKCS1Padding, + RSAwithSHA384andPKCS1Padding, + RSAwithSHA512andPKCS1Padding + ) } } -interface SpecializedSignatureAlgorithm { - val algorithm: SignatureAlgorithm +interface SpecializedDataIntegrityAlgorithm { + val algorithm: DataIntegrityAlgorithm } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt index 43acfc3d..e3c4d9f5 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt @@ -18,18 +18,13 @@ import kotlinx.serialization.encoding.Encoder enum class X509SignatureAlgorithm( override val oid: ObjectIdentifier, val isEc: Boolean = false -) : Asn1Encodable<Asn1Sequence>, Identifiable, SpecializedSignatureAlgorithm { +) : Asn1Encodable<Asn1Sequence>, Identifiable, SpecializedDataIntegrityAlgorithm { // ECDSA with SHA-size ES256(KnownOIDs.ecdsaWithSHA256, true), ES384(KnownOIDs.ecdsaWithSHA384, true), ES512(KnownOIDs.ecdsaWithSHA512, true), - // HMAC-size with SHA-size - HS256(KnownOIDs.hmacWithSHA256), - HS384(KnownOIDs.hmacWithSHA384), - HS512(KnownOIDs.hmacWithSHA512), - // RSASSA-PSS with SHA-size PS256(KnownOIDs.rsaPSS), PS384(KnownOIDs.rsaPSS), @@ -84,7 +79,6 @@ enum class X509SignatureAlgorithm( PS512 -> encodePSSParams(512) - HS256, HS384, HS512, RS256, RS384, RS512, RS1 -> Asn1.Sequence { +oid +Null() @@ -93,15 +87,14 @@ enum class X509SignatureAlgorithm( val digest: Digest get() = when(this) { RS1 -> Digest.SHA1 - ES256, HS256, PS256, RS256 -> Digest.SHA256 - ES384, HS384, PS384, RS384 -> Digest.SHA384 - ES512, HS512, PS512, RS512 -> Digest.SHA512 + ES256, PS256, RS256 -> Digest.SHA256 + ES384, PS384, RS384 -> Digest.SHA384 + ES512, PS512, RS512 -> Digest.SHA512 } override val algorithm: SignatureAlgorithm get() = when(this) { ES256, ES384, ES512 -> SignatureAlgorithm.ECDSA(this.digest, null) - HS256, HS384, HS512 -> SignatureAlgorithm.HMAC(this.digest) PS256, PS384, PS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PSS) RS1, RS256, RS384, RS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PKCS1) } @@ -119,8 +112,7 @@ enum class X509SignatureAlgorithm( ES512.oid, ES384.oid, ES256.oid -> fromOid(oid) RS1.oid -> RS1 - RS256.oid, RS384.oid, RS512.oid, - HS256.oid, HS384.oid, HS512.oid -> fromOid(oid).also { + RS256.oid, RS384.oid, RS512.oid-> fromOid(oid).also { val tag = src.nextChild().tag if (tag != Asn1Element.Tag.NULL) throw Asn1TagMismatchException(Asn1Element.Tag.NULL, tag, "RSA Params not allowed.") @@ -192,17 +184,8 @@ fun SignatureAlgorithm.toX509SignatureAlgorithm() = catching { else -> throw IllegalArgumentException("Digest ${this.digest} is unsupported by X.509 RSA-PSS") } } - is SignatureAlgorithm.HMAC -> when (this.digest) { - Digest.SHA256 -> X509SignatureAlgorithm.HS256 - Digest.SHA384 -> X509SignatureAlgorithm.HS384 - Digest.SHA512 -> X509SignatureAlgorithm.HS512 - else -> throw IllegalArgumentException("Digest ${this.digest} is unsupported by X.509 HMAC") - } } } -/** Finds a X.509 signature algorithm matching this algorithm. Curve restrictions are not preserved. */ -fun SpecializedSignatureAlgorithm.toX509SignatureAlgorithm() = - this.algorithm.toX509SignatureAlgorithm() object X509SignatureAlgorithmSerializer : KSerializer<X509SignatureAlgorithm> { diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MAC.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MAC.kt deleted file mode 100644 index 6c7810b8..00000000 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MAC.kt +++ /dev/null @@ -1,40 +0,0 @@ -package at.asitplus.signum.indispensable.mac - -import at.asitplus.signum.indispensable.Digest -import at.asitplus.signum.indispensable.asn1.Identifiable -import at.asitplus.signum.indispensable.asn1.KnownOIDs -import at.asitplus.signum.indispensable.asn1.ObjectIdentifier -import at.asitplus.signum.indispensable.misc.BitLength - -sealed interface MAC { - /** output size of MAC */ - val outputLength: BitLength - - companion object { - val entries: Iterable<MAC> = HMAC.entries - } -} - -/** - * RFC 2104 HMAC - */ -enum class HMAC(val digest: Digest, override val oid: ObjectIdentifier) : MAC, Identifiable { - SHA1(Digest.SHA1, KnownOIDs.hmacWithSHA1), - SHA256(Digest.SHA256, KnownOIDs.hmacWithSHA256), - SHA384(Digest.SHA384, KnownOIDs.hmacWithSHA384), - SHA512(Digest.SHA512, KnownOIDs.hmacWithSHA512), - ; - - override fun toString() = "HMAC-$digest" - - companion object { - operator fun invoke(digest: Digest) = when (digest) { - Digest.SHA1 -> SHA1 - Digest.SHA256 -> SHA256 - Digest.SHA384 -> SHA384 - Digest.SHA512 -> SHA512 - } - } - - override val outputLength: BitLength get() = digest.outputLength -} diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt new file mode 100644 index 00000000..cc507420 --- /dev/null +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt @@ -0,0 +1,61 @@ +package at.asitplus.signum.indispensable.mac + +import at.asitplus.signum.indispensable.DataIntegrityAlgorithm +import at.asitplus.signum.indispensable.Digest +import at.asitplus.signum.indispensable.asn1.* +import at.asitplus.signum.indispensable.asn1.encoding.Asn1 +import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Null +import at.asitplus.signum.indispensable.asn1.encoding.readNull +import at.asitplus.signum.indispensable.misc.BitLength + +sealed interface MessageAuthenticationCode : DataIntegrityAlgorithm { + /** output size of MAC */ + val outputLength: BitLength + + companion object { + val entries: Iterable<MessageAuthenticationCode> = HMAC.entries + } +} + +/** + * RFC 2104 HMAC + */ +enum class HMAC(val digest: Digest, override val oid: ObjectIdentifier) : MessageAuthenticationCode, Identifiable, + Asn1Encodable<Asn1Sequence> { + SHA1(Digest.SHA1, KnownOIDs.hmacWithSHA1), + SHA256(Digest.SHA256, KnownOIDs.hmacWithSHA256), + SHA384(Digest.SHA384, KnownOIDs.hmacWithSHA384), + SHA512(Digest.SHA512, KnownOIDs.hmacWithSHA512), + ; + + override fun toString() = "HMAC-$digest" + + override fun encodeToTlv(): Asn1Sequence = Asn1.Sequence { + +oid + +Null() + } + + + companion object : Asn1Decodable<Asn1Sequence, HMAC> { + + fun byOID(oid: ObjectIdentifier): HMAC? = entries.find { it.oid == oid } + + fun byDigest(digest: Digest): HMAC = entries.find { it.digest == digest }!! + + operator fun invoke(digest: Digest) = when (digest) { + Digest.SHA1 -> SHA1 + Digest.SHA256 -> SHA256 + Digest.SHA384 -> SHA384 + Digest.SHA512 -> SHA512 + } + + override fun doDecode(src: Asn1Sequence): HMAC { + val oid = src.nextChild().asPrimitive().readOid() + src.nextChild().asPrimitive().readNull() + require(!src.hasMoreChildren()) { "Superfluous ANS.1 data in HMAC" } + return byOID(oid) ?: throw Asn1OidException(oid) + } + } + + override val outputLength: BitLength get() = digest.outputLength +} diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt index e9f38b49..147e9c9a 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt @@ -224,7 +224,7 @@ constructor( val CryptoSignature.x509Encoded get() = when (this) { is CryptoSignature.EC -> encodeToDer().encodeToAsn1BitStringPrimitive() - is CryptoSignature.RSAorHMAC -> encodeToTlv() + is CryptoSignature.RSA -> encodeToTlv() } /** @@ -235,7 +235,7 @@ val CryptoSignature.x509Encoded fun CryptoSignature.Companion.fromX509Encoded(alg: X509SignatureAlgorithm, it: Asn1Primitive) = when (alg.isEc) { true -> CryptoSignature.EC.decodeFromDer(it.asAsn1BitString().rawBytes) - false -> CryptoSignature.RSAorHMAC.decodeFromTlv(it) + false -> CryptoSignature.RSA.decodeFromTlv(it) } /** diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricEncryptionAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricEncryptionAlgorithm.kt index 2ba35669..b7314d76 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricEncryptionAlgorithm.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricEncryptionAlgorithm.kt @@ -7,7 +7,7 @@ import at.asitplus.signum.indispensable.asn1.KnownOIDs import at.asitplus.signum.indispensable.asn1.ObjectIdentifier import at.asitplus.signum.indispensable.asn1.encoding.encodeTo8Bytes import at.asitplus.signum.indispensable.mac.HMAC -import at.asitplus.signum.indispensable.mac.MAC +import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode import at.asitplus.signum.indispensable.misc.BitLength import at.asitplus.signum.indispensable.misc.bit import kotlin.contracts.ExperimentalContracts @@ -110,7 +110,7 @@ sealed interface SymmetricEncryptionAlgorithm<out A : AuthCapability<out K>, out interface Integrated<I : NonceTrait> : Authenticated<AuthCapability.Authenticated.Integrated, I, KeyType.Integrated> - interface WithDedicatedMac<M : MAC, I : NonceTrait> : + interface WithDedicatedMac<M : MessageAuthenticationCode, I : NonceTrait> : Authenticated<AuthCapability.Authenticated.WithDedicatedMac<M, I>, I, KeyType.WithDedicatedMacKey> } @@ -311,7 +311,7 @@ sealed interface AuthCapability<K : KeyType> { * An authenticated cipher construction based on an unauthenticated cipher with a dedicated MAC function, requiring a dedicated MAC key. * _Encrypt-then-MAC_ */ - class WithDedicatedMac<M : MAC, I : NonceTrait>( + class WithDedicatedMac<M : MessageAuthenticationCode, I : NonceTrait>( /** * The inner unauthenticated cipher */ @@ -406,14 +406,14 @@ val DefaultDedicatedMacAuthTagTransformation: DedicatedMacAuthTagTransformation /** * Typealias defining the signature of the lambda for defining a custom MAC input calculation scheme. */ -typealias DedicatedMacInputCalculation = MAC.(ciphertext: ByteArray, nonce: ByteArray, aad: ByteArray) -> ByteArray +typealias DedicatedMacInputCalculation = MessageAuthenticationCode.(ciphertext: ByteArray, nonce: ByteArray, aad: ByteArray) -> ByteArray /** * The default dedicated mac input calculation as per [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518#section-5.2.2.1), authenticating all inputs: * `AAD || IV || Ciphertext || AAD Length`, where AAD_length is a 64 bit big-endian representation of the aad length in bits */ val DefaultDedicatedMacInputCalculation: DedicatedMacInputCalculation = - fun MAC.(ciphertext: ByteArray, iv: ByteArray, aad: ByteArray): ByteArray = + fun MessageAuthenticationCode.(ciphertext: ByteArray, iv: ByteArray, aad: ByteArray): ByteArray = aad + iv + ciphertext + (aad.size.toLong()*8L).encodeTo8Bytes() /** diff --git a/indispensable/src/commonTest/kotlin/CryptoSignatureTest.kt b/indispensable/src/commonTest/kotlin/CryptoSignatureTest.kt index a17391bb..0b3c51f5 100644 --- a/indispensable/src/commonTest/kotlin/CryptoSignatureTest.kt +++ b/indispensable/src/commonTest/kotlin/CryptoSignatureTest.kt @@ -20,9 +20,9 @@ class CryptoSignatureTest : FreeSpec({ val ec1 = CryptoSignature.EC.fromRS(first.toBigInteger(), second.toBigInteger()) val ec2 = CryptoSignature.EC.fromRS(first.toBigInteger(), second.toBigInteger()) val ec3 = CryptoSignature.EC.fromRS(second.toBigInteger(), first.toBigInteger()) - val rsa1 = CryptoSignature.RSAorHMAC(first.toTwosComplementByteArray()) - val rsa2 = CryptoSignature.RSAorHMAC(first.toTwosComplementByteArray()) - val rsa3 = CryptoSignature.RSAorHMAC(second.toTwosComplementByteArray()) + val rsa1 = CryptoSignature.RSA(first.toTwosComplementByteArray()) + val rsa2 = CryptoSignature.RSA(first.toTwosComplementByteArray()) + val rsa3 = CryptoSignature.RSA(second.toTwosComplementByteArray()) ec1 shouldBe ec1 ec1 shouldBe ec2 diff --git a/indispensable/src/iosMain/kotlin/CommonCryptoExtensions.kt b/indispensable/src/iosMain/kotlin/CommonCryptoExtensions.kt index b1806733..f5bbcab4 100644 --- a/indispensable/src/iosMain/kotlin/CommonCryptoExtensions.kt +++ b/indispensable/src/iosMain/kotlin/CommonCryptoExtensions.kt @@ -40,13 +40,8 @@ val SignatureAlgorithm.secKeyAlgorithm: SecKeyAlgorithm } } - is SignatureAlgorithm.HMAC -> TODO("HMAC is unsupported") }!! -val SpecializedSignatureAlgorithm.secKeyAlgorithm - get() = - this.algorithm.secKeyAlgorithm - val SignatureAlgorithm.secKeyAlgorithmPreHashed: SecKeyAlgorithm get() = when (this) { is SignatureAlgorithm.ECDSA -> { @@ -77,17 +72,16 @@ val SignatureAlgorithm.secKeyAlgorithmPreHashed: SecKeyAlgorithm } } - is SignatureAlgorithm.HMAC -> TODO("HMAC is unsupported") }!! -val SpecializedSignatureAlgorithm.secKeyAlgorithmPreHashed +val X509SignatureAlgorithm.secKeyAlgorithmPreHashed get() = this.algorithm.secKeyAlgorithmPreHashed val CryptoSignature.iosEncoded get() = when (this) { is CryptoSignature.EC -> this.encodeToDer() - is CryptoSignature.RSAorHMAC -> this.rawByteArray + is CryptoSignature.RSA -> this.rawByteArray } fun CryptoPublicKey.toSecKey() = catching { diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/EnumConsistencyTests.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/EnumConsistencyTests.kt index fb045341..4aa6e241 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/EnumConsistencyTests.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/EnumConsistencyTests.kt @@ -1,6 +1,6 @@ package at.asitplus.signum.indispensable -import at.asitplus.signum.indispensable.mac.MAC +import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe @@ -40,5 +40,5 @@ inline fun<reified T: Any> FreeSpec.enumConsistencyTest() { } class EnumConsistencyTests : FreeSpec({ - enumConsistencyTest<MAC>() + enumConsistencyTest<MessageAuthenticationCode>() }) \ No newline at end of file diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/SignatureCodecTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/SignatureCodecTest.kt index d018fa5f..9923e11b 100644 --- a/indispensable/src/jvmTest/kot…
This reverts commit 4cb60b8f8e28a1faec8c13d8456b117c1b6d2b15. diff --git a/.github/workflows/test-jvm.yml b/.github/workflows/test-jvm.yml index 239a9a7b..8bbf7620 100644 --- a/.github/workflows/test-jvm.yml +++ b/.github/workflows/test-jvm.yml @@ -19,5 +19,5 @@ jobs: if: success() || failure() with: name: JVM All Tests - path: indispensable-asn1/build/test-results/**/TEST*.xml,indispensable/build/test-results/**/TEST*.xml,indispensable-cosef/build/test-results/**/TEST*.xml,indispensable-josef/build/test-results/**/TEST*.xml,supreme/build/test-results/**/TEST*.xml + path: indispensable-asn1/build/test-results/**/TEST*.xml,indispensable/build/test-results/**/TEST*.xml,indispensable-josef/build/test-results/**/TEST*.xml,indispensable-josef/build/test-results/**/TEST*.xml,supreme/build/test-results/**/TEST*.xml reporter: java-junit diff --git a/CHANGELOG.md b/CHANGELOG.md index 69f7e813..6e4f65a0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,14 +13,6 @@ * This note will be prepended to the changelog entries for all point releases leading up to and including Indispensable 4.0.0 / Supreme 1.0.0 * HMAC Support - * **Clean up `RSAorHMAC` mess** - * Introduce `DataAuthenticationAlgorithm` encompassing Signatures and MACs - * Rename `RSAorHMAC` to `RSA` and introduce dedicated `MessageAuthenticationCode` and `HMAC` classes - * This caused **some breaking shifts and cleanups** in conversion methods between indispensable, josef, cosef, and JCA types - * There is no direct mapping between `SignatureAlgorithm` and COSE/JOSE algorithms anymore, because JOSE/COSE also incorporate MAC - * Hence, for both `JwsAlgorithm` and `CoseAlgorithm`, the following properties have been introduced on the companions: - * `signatureAlgorithms` - * `messageAuthenticationCodes` * Symmetric Encryption * Supported Algorithms * AES diff --git a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt index 201ab745..6bff9f32 100644 --- a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt +++ b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.safeDrawing import androidx.compose.foundation.layout.size @@ -45,26 +46,28 @@ import at.asitplus.signum.indispensable.CryptoSignature import at.asitplus.signum.indispensable.ECCurve import at.asitplus.signum.indispensable.RSAPadding import at.asitplus.signum.indispensable.SignatureAlgorithm +import at.asitplus.signum.indispensable.SpecializedSignatureAlgorithm import at.asitplus.signum.indispensable.X509SignatureAlgorithm import at.asitplus.signum.indispensable.nativeDigest -import at.asitplus.signum.indispensable.pki.X509Certificate import at.asitplus.signum.supreme.dsl.PREFERRED import at.asitplus.signum.supreme.sign.Signer import at.asitplus.signum.supreme.sign.makeVerifier import at.asitplus.signum.supreme.sign.verify import at.asitplus.cryptotest.theme.AppTheme import at.asitplus.cryptotest.theme.LocalThemeIsDark +import at.asitplus.signum.indispensable.CryptoPublicKey +import at.asitplus.signum.indispensable.KeyAgreementPrivateValue import at.asitplus.signum.indispensable.jsonEncoded +import at.asitplus.signum.supreme.SecretExposure +import at.asitplus.signum.supreme.agree.Ephemeral import at.asitplus.signum.supreme.asKmmResult import at.asitplus.signum.supreme.os.PlatformSignerConfigurationBase -import at.asitplus.signum.supreme.os.PlatformSigningKeyConfigurationBase -import at.asitplus.signum.supreme.os.PlatformSigningProvider import at.asitplus.signum.supreme.os.SignerConfiguration import at.asitplus.signum.supreme.os.SigningProvider import at.asitplus.signum.supreme.sign.Verifier import io.github.aakira.napier.DebugAntilog import io.github.aakira.napier.Napier -import io.ktor.util.decodeBase64Bytes +import io.ktor.util.encodeBase64 import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.launch @@ -97,7 +100,10 @@ private class getter<T>(private val fn: () -> T) { operator fun getValue(nothing: Nothing?, property: KProperty<*>): T = fn() } -@OptIn(ExperimentalStdlibApi::class, ExperimentalCoroutinesApi::class) +@OptIn( + ExperimentalCoroutinesApi::class, + SecretExposure::class +) @Composable internal fun App() { @@ -114,7 +120,7 @@ internal fun App() { X509SignatureAlgorithm.RS512 ) var keyAlgorithm by remember { - mutableStateOf<X509SignatureAlgorithm>( + mutableStateOf<SpecializedSignatureAlgorithm>( X509SignatureAlgorithm.ES256 ) } @@ -137,6 +143,12 @@ internal fun App() { } val signingPossible by getter { currentKey?.isSuccess == true } var signatureData by remember { mutableStateOf<KmmResult<CryptoSignature>?>(null) } + var ephemeralKey by remember { + mutableStateOf<KeyAgreementPrivateValue.ECDH?>( + null + ) + } + var agreedKey by remember { mutableStateOf<KmmResult<ByteArray>?>(null) } val signatureDataStr by getter { signatureData?.fold(onSuccess = Any::toString) { Napier.e("Signature failed", it) @@ -157,6 +169,14 @@ internal fun App() { var genTextOverride by remember { mutableStateOf<String?>(null) } val genText by getter { genTextOverride ?: "Generate" } + fun genEphemeralKey(){ + ephemeralKey = if (currentKey?.getOrNull() is CryptoPublicKey.EC) + KeyAgreementPrivateValue.ECDH.Ephemeral((currentKey!!.getOrThrow() as CryptoPublicKey.EC).curve).getOrNull() + else null + agreedKey=null + } + + Column( modifier = Modifier.fillMaxSize().verticalScroll(ScrollState(0), enabled = true) .windowInsetsPadding(WindowInsets.safeDrawing) @@ -307,12 +327,16 @@ internal fun App() { canGenerate = false genTextOverride = "Creating…" currentSigner = Provider.createSigningKey(ALIAS) { + when (val alg = keyAlgorithm.algorithm) { is SignatureAlgorithm.ECDSA -> { [email protected] { - curve = alg.requiredCurve ?: - ECCurve.entries.find { it.nativeDigest == alg.digest }!! + curve = alg.requiredCurve + ?: ECCurve.entries.find { it.nativeDigest == alg.digest }!! digests = setOf(alg.digest) + purposes { + keyAgreement = true + } } } @@ -327,29 +351,27 @@ internal fun App() { else -> error("unreachable") } - if (this is PlatformSigningKeyConfigurationBase) { - signer(SIGNER_CONFIG) + signer(SIGNER_CONFIG) - val timeout = runCatching { - biometricAuth.substringBefore("s").trim().toInt() - }.getOrNull() + val timeout = runCatching { + biometricAuth.substringBefore("s").trim().toInt() + }.getOrNull() - if (attestation || timeout != null) { - hardware { - backing = PREFERRED - if (attestation) { - attestation { - challenge = Random.nextBytes(16) - } + if (attestation || timeout != null) { + hardware { + backing = PREFERRED + if (attestation) { + attestation { + challenge = Random.nextBytes(16) } + } - if (timeout != null) { - protection { - this.timeout = timeout.seconds - factors { - biometry = true - deviceLock = true - } + if (timeout != null) { + protection { + this.timeout = timeout.seconds + factors { + biometry = true + deviceLock = true } } } @@ -362,6 +384,8 @@ internal fun App() { Napier.w { "Signing possible: ${currentKey?.isSuccess}" } canGenerate = true genTextOverride = null + + genEphemeralKey() } }, modifier = Modifier.padding(start = 16.dp) @@ -385,6 +409,7 @@ internal fun App() { //loadPubKey().let { Napier.w { "PubKey retrieved from native: $it" } } canGenerate = true genTextOverride = null + genEphemeralKey() } }, modifier = Modifier.padding(start = 16.dp, end = 16.dp) @@ -432,7 +457,6 @@ internal fun App() { Button( onClick = { - Napier.w { "input: $inputData" } Napier.w { "signingKey: $currentKey" } CoroutineScope(context).launch { @@ -441,13 +465,61 @@ internal fun App() { .transform { it.sign(data).asKmmResult() } .also { signatureData = it; verifyState = null } } - }, - - modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp), + modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(), enabled = signingPossible - ) { - Text("Sign") + ) { Text("Sign") } + + if (signingPossible && (currentKey?.getOrNull() is CryptoPublicKey.EC)) { + Napier.i { "Ephemeral key: $ephemeralKey" } + Spacer(Modifier.height(8.dp)) + ephemeralKey?.let { ephemeralKey -> + OutlinedTextField(value = ephemeralKey.publicValue.toString(), + modifier = Modifier.fillMaxWidth().padding(16.dp), + minLines = 1, + textStyle = TextStyle.Default.copy(fontSize = 10.sp), + readOnly = true, + onValueChange = {}, + label = { Text("Random faux-external key for ECDH") }) + Button( + onClick = { + Napier.w { "input: $inputData" } + Napier.w { "signingKey: $currentKey" } + CoroutineScope(context).launch { + + agreedKey = + (currentSigner!!.getOrThrow() as Signer.ECDSA).keyAgreement( + ephemeralKey.publicValue + ) + Napier.i { + "ECDH Key of ext_piv + hardware-backed_pub: ${ + agreedKey?.getOrNull()?.encodeBase64() + }" + } + + Napier.i { + "ECDH Key of ext_pub + hardware-backed_priv: ${ + agreedKey?.getOrNull()?.encodeBase64() + }" + } + } + + }, + modifier = Modifier.padding(horizontal = 16.dp).fillMaxWidth(), + ) { Text("Perform ECDH key agreement") } + } + + if (agreedKey != null) { + OutlinedTextField(value = + "Computed from ext_pub + hardware-backed_priv:\n" + agreedKey?.map { it.encodeBase64() } + ?.getOrElse { it.message ?: it::class.simpleName ?: "" }, + modifier = Modifier.fillMaxWidth().padding(16.dp), + minLines = 1, + textStyle = TextStyle.Default.copy(fontSize = 10.sp), + readOnly = true, + onValueChange = {}, + label = { Text("Agreed-upon secret") }) + } } if (signatureData != null) { diff --git a/docs/docs/indispensable.md b/docs/docs/indispensable.md index d9c3aa60..4376b599 100644 --- a/docs/docs/indispensable.md +++ b/docs/docs/indispensable.md @@ -104,9 +104,9 @@ The following functions provide interop functionality with platform types. ### JVM/Android * `SignatureAlgorithm.getJCASignatureInstance()` gets a pre-configured JCA instance for this algorithm -* `X509SignatureAlgorithm.getJCASignatureInstance()` gets a pre-configured JCA instance for this algorithm +* `SpecializedSignatureAlgorithm.getJCASignatureInstance()` gets a pre-configured JCA instance for this algorithm * `SignatureAlgorithm.getJCASignatureInstancePreHashed()` gets a pre-configured JCA instance for pre-hashed data for this algorithm -* `X509SignatureAlgorithm.getJCASignatureInstancePreHashed()` gets a pre-configured JCA instance for pre-hashed data for this algorithm +* `SpecializedSignatureAlgorithm.getJCASignatureInstancePreHashed()` gets a pre-configured JCA instance for pre-hashed data for this algorithm <br> @@ -148,7 +148,7 @@ The following functions provide interop functionality with platform types. * `CryptoSignature.jcaSignatureBytes` returns the JCA-native encoded representation of a signature * `CryptoSignature.parseFromJca()` returns a signature object form a JCA-native encoded representation of a signature * `CryptoSignature.EC.parseFromJca()` returns an EC signature object form a JCA-native encoded representation of a signature -* `CryptoSignature.RSA.parseFromJca()` returns an RSA signature object form a JCA-native encoded representation of a signature +* `CryptoSignature.RSAorHMAC.parseFromJca()` returns an RSA signature object form a JCA-native encoded representation of a signature * `CryptoSignature.EC.parseFromJcaP1363` parses a signature produced by the JCA digestWithECDSAinP1363Format algorithm. * `X509Certificate.toJcaCertificate()` converts the certificate to a JCA-native `X509Certificate` * `java.security.cert.X509Certificate.toKmpCertificate()` converts a JCA-native certificate to a Signum `X509Certificate` diff --git a/indispensable-asn1/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt b/indispensable-asn1/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt index b6837c26..b8f2d98b 100644 --- a/indispensable-asn1/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt +++ b/indispensable-asn1/src/commonMain/kotlin/at/asitplus/signum/indispensable/asn1/Asn1Exception.kt @@ -13,9 +13,7 @@ class Asn1TagMismatchException(val expected: Asn1Element.Tag, val actual: Asn1El class Asn1StructuralException(message: String, cause: Throwable? = null) : Asn1Exception(message, cause) -class Asn1OidException(message: String, val oid: ObjectIdentifier) : Asn1Exception(message){ - constructor(oid: ObjectIdentifier):this("Unknown oid $oid", oid) -} +class Asn1OidException(message: String, val oid: ObjectIdentifier) : Asn1Exception(message) /** * Runs [block] inside [catching] and encapsulates any thrown exception in an [Asn1Exception] unless it already is one diff --git a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseAlgorithm.kt b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseAlgorithm.kt index 94bfe3f1..979cb602 100644 --- a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseAlgorithm.kt +++ b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseAlgorithm.kt @@ -1,10 +1,11 @@ package at.asitplus.signum.indispensable.cosef -import at.asitplus.KmmResult import at.asitplus.catching -import at.asitplus.signum.indispensable.* -import at.asitplus.signum.indispensable.mac.HMAC -import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode +import at.asitplus.signum.indispensable.Digest +import at.asitplus.signum.indispensable.ECCurve +import at.asitplus.signum.indispensable.RSAPadding +import at.asitplus.signum.indispensable.SignatureAlgorithm +import at.asitplus.signum.indispensable.SpecializedSignatureAlgorithm import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -17,7 +18,7 @@ import kotlinx.serialization.encoding.Encoder * See [COSE Algorithm Registry](https://www.iana.org/assignments/cose/cose.xhtml) */ @Serializable(with = CoseAlgorithmSerializer::class) -enum class CoseAlgorithm(val value: Int) : SpecializedDataIntegrityAlgorithm { +enum class CoseAlgorithm(val value: Int): SpecializedSignatureAlgorithm { // ECDSA with SHA-size ES256(-7), @@ -42,52 +43,22 @@ enum class CoseAlgorithm(val value: Int) : SpecializedDataIntegrityAlgorithm { // RSASSA-PKCS1-v1_5 using SHA-1 RS1(-65535); - - companion object { - /** - * encompasses only signature algorithms, filtering out MACs - */ - val signatureAlgorithms = listOf( - ES256, - ES384, - ES512, - - PS256, - PS384, - PS512, - - RS256, - RS384, - RS512, - RS1 - ).map { it.algorithm as SignatureAlgorithm } - - /** - * encompasses only MACs, filtering out signature algorithms - */ - val messageAuthenticationCodes = listOf(HS256, HS384, HS512).map { it.algorithm as MessageAuthenticationCode } - - + val digest: Digest get() = when(this) { + RS1 -> Digest.SHA1 + ES256, HS256, PS256, RS256 -> Digest.SHA256 + ES384, HS384, PS384, RS384 -> Digest.SHA384 + ES512, HS512, PS512, RS512 -> Digest.SHA512 } - val digest: Digest - get() = when (this) { - RS1 -> Digest.SHA1 - ES256, HS256, PS256, RS256 -> Digest.SHA256 - ES384, HS384, PS384, RS384 -> Digest.SHA384 - ES512, HS512, PS512, RS512 -> Digest.SHA512 - } + override val algorithm: SignatureAlgorithm get() = when (this) { + ES256 -> SignatureAlgorithm.ECDSA(Digest.SHA256, ECCurve.SECP_256_R_1) + ES384 -> SignatureAlgorithm.ECDSA(Digest.SHA384, ECCurve.SECP_384_R_1) + ES512 -> SignatureAlgorithm.ECDSA(Digest.SHA512, ECCurve.SECP_521_R_1) - override val algorithm: DataIntegrityAlgorithm - get() = when (this) { - ES256 -> SignatureAlgorithm.ECDSA(Digest.SHA256, ECCurve.SECP_256_R_1) - ES384 -> SignatureAlgorithm.ECDSA(Digest.SHA384, ECCurve.SECP_384_R_1) - ES512 -> SignatureAlgorithm.ECDSA(Digest.SHA512, ECCurve.SECP_521_R_1) - - HS256, HS384, HS512 -> HMAC.byDigest(digest) - PS256, PS384, PS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PSS) - RS1, RS256, RS384, RS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PKCS1) - } + HS256, HS384, HS512 -> SignatureAlgorithm.HMAC(this.digest) + PS256, PS384, PS512 -> SignatureAlgorithm.RSA(this. digest, RSAPadding.PSS) + RS1, RS256, RS384, RS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PKCS1) + } } object CoseAlgorithmSerializer : KSerializer<CoseAlgorithm> { @@ -106,9 +77,8 @@ object CoseAlgorithmSerializer : KSerializer<CoseAlgorithm> { } - /** Tries to find a matching COSE algorithm. Note that COSE imposes curve restrictions on ECDSA based on the digest. */ -fun DataIntegrityAlgorithm.toCoseAlgorithm(): KmmResult<CoseAlgorithm> = catching { +fun SignatureAlgorithm.toCoseAlgorithm() = catching { when (this) { is SignatureAlgorithm.ECDSA -> when (this.digest) { Digest.SHA256 -> CoseAlgorithm.ES256 @@ -116,7 +86,6 @@ fun DataIntegrityAlgorithm.toCoseAlgorithm(): KmmResult<CoseAlgorithm> = catchin Digest.SHA512 -> CoseAlgorithm.ES512 else -> throw IllegalArgumentException("ECDSA with ${this.digest} is unsupported by COSE") } - is SignatureAlgorithm.RSA -> when (this.padding) { RSAPadding.PKCS1 -> when (this.digest) { Digest.SHA1 -> CoseAlgorithm.RS1 @@ -124,7 +93,6 @@ fun DataIntegrityAlgorithm.toCoseAlgorithm(): KmmResult<CoseAlgorithm> = catchin Digest.SHA384 -> CoseAlgorithm.RS384 Digest.SHA512 -> CoseAlgorithm.RS512 } - RSAPadding.PSS -> when (this.digest) { Digest.SHA256 -> CoseAlgorithm.PS256 Digest.SHA384 -> CoseAlgorithm.PS384 @@ -132,18 +100,15 @@ fun DataIntegrityAlgorithm.toCoseAlgorithm(): KmmResult<CoseAlgorithm> = catchin else -> throw IllegalArgumentException("RSA-PSS with ${this.digest} is unsupported by COSE") } } - - is HMAC -> when (this.digest) { + is SignatureAlgorithm.HMAC -> when (this.digest) { Digest.SHA256 -> CoseAlgorithm.HS256 Digest.SHA384 -> CoseAlgorithm.HS384 Digest.SHA512 -> CoseAlgorithm.HS512 else -> throw IllegalArgumentException("HMAC with ${this.digest} is unsupported by COSE") } - - else -> throw IllegalArgumentException("UnsupportedAlgorithm $this") } } /** Tries to find a matching COSE algorithm. Note that COSE imposes curve restrictions on ECDSA based on the digest. */ -fun SpecializedDataIntegrityAlgorithm.toCoseAlgorithm() = +fun SpecializedSignatureAlgorithm.toCoseAlgorithm() = this.algorithm.toCoseAlgorithm() diff --git a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseSignedSerializer.kt b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseSignedSerializer.kt index e571328e..c7f98eeb 100644 --- a/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseSignedSerializer.kt +++ b/indispensable-cosef/src/commonMain/kotlin/at/asitplus/signum/indispensable/cosef/CoseSignedSerializer.kt @@ -66,7 +66,7 @@ class CoseSignedSerializer<P : Any?>( ): CryptoSignature.RawByteEncodable = if (protectedHeader.usesEC() ?: unprotectedHeader?.usesEC() ?: (size < 2048)) CryptoSignature.EC.fromRawBytes(this) - else CryptoSignature.RSA(this) + else CryptoSignature.RSAorHMAC(this) private fun ByteArray?.toNullablePayload(): P? = when (this) { null -> null @@ -80,7 +80,7 @@ class CoseSignedSerializer<P : Any?>( } else { runCatching { fromBytes() } .getOrElse { fromByteStringWrapper() } - // if it still fails, the input not valid + // if it still fails, the input is not valid } private fun ByteArray.fromBytes(): P = diff --git a/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseEqualsTest.kt b/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseEqualsTest.kt index d9d737c7..dc47dd22 100644 --- a/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseEqualsTest.kt +++ b/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseEqualsTest.kt @@ -21,14 +21,14 @@ class CoseEqualsTest : FreeSpec({ protectedHeader = CoseHeader(), unprotectedHeader = null, payload = bytes, - signature = CryptoSignature.RSA(bytes), + signature = CryptoSignature.RSAorHMAC(bytes), payloadSerializer = ByteArraySerializer(), ) val bytesSigned2 = CoseSigned.create( protectedHeader = CoseHeader(), unprotectedHeader = null, payload = bytes, - signature = CryptoSignature.RSA(bytes), + signature = CryptoSignature.RSAorHMAC(bytes), payloadSerializer = ByteArraySerializer(), ) @@ -42,14 +42,14 @@ class CoseEqualsTest : FreeSpec({ protectedHeader = CoseHeader(), unprotectedHeader = null, payload = reversed, - signature = CryptoSignature.RSA(reversed), + signature = CryptoSignature.RSAorHMAC(reversed), payloadSerializer = ByteArraySerializer(), ) val reversedSigned2 = CoseSigned.create( protectedHeader = CoseHeader(), unprotectedHeader = null, payload = reversed, - signature = CryptoSignature.RSA(reversed), + signature = CryptoSignature.RSAorHMAC(reversed), payloadSerializer = ByteArraySerializer(), ) @@ -80,14 +80,14 @@ class CoseEqualsTest : FreeSpec({ protectedHeader = CoseHeader(), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSA(bytes), + signature = CryptoSignature.RSAorHMAC(bytes), payloadSerializer = DataClass.serializer(), ) val bytesSigned2 = CoseSigned.create( protectedHeader = CoseHeader(), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSA(bytes), + signature = CryptoSignature.RSAorHMAC(bytes), payloadSerializer = DataClass.serializer(), ) @@ -102,14 +102,14 @@ class CoseEqualsTest : FreeSpec({ protectedHeader = CoseHeader(), unprotectedHeader = null, payload = reversed, - signature = CryptoSignature.RSA(bytes), + signature = CryptoSignature.RSAorHMAC(bytes), payloadSerializer = DataClass.serializer(), ) val reversedSigned2 = CoseSigned.create( protectedHeader = CoseHeader(), unprotectedHeader = null, payload = reversed, - signature = CryptoSignature.RSA(bytes), + signature = CryptoSignature.RSAorHMAC(bytes), payloadSerializer = DataClass.serializer(), ) diff --git a/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseSerializationTest.kt b/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseSerializationTest.kt index dd25e09c..bc0c9cb1 100644 --- a/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseSerializationTest.kt +++ b/indispensable-cosef/src/commonTest/kotlin/at/asitplus/signum/indispensable/cosef/CoseSerializationTest.kt @@ -30,7 +30,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.ES256), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSA(byteArrayOf()), + signature = CryptoSignature.RSAorHMAC(byteArrayOf()), payloadSerializer = ByteStringWrapper.serializer(String.serializer()) ) } @@ -42,7 +42,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSA("bar".encodeToByteArray()), + signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), payloadSerializer = ByteArraySerializer(), ) @@ -55,7 +55,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSA("bar".encodeToByteArray()), + signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), payloadSerializer = DataClass.serializer(), ) @@ -68,7 +68,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSA("bar".encodeToByteArray()), //RSA because EC expects tuple + signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), //RSAorHMAC because EC expects tuple payloadSerializer = ByteArraySerializer(), ) val serialized = cose.serialize(ByteArraySerializer()) @@ -86,7 +86,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = null, - signature = CryptoSignature.RSA("bar".encodeToByteArray()), //RSA because EC expects tuple + signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), //RSAorHMAC because EC expects tuple payloadSerializer = ByteArraySerializer(), ) val serialized = cose.serialize(ByteArraySerializer()) @@ -129,7 +129,7 @@ class CoseSerializationTest : FreeSpec({ protectedHeader = CoseHeader(algorithm = CoseAlgorithm.RS256), unprotectedHeader = null, payload = payload, - signature = CryptoSignature.RSA("bar".encodeToByteArray()), //RSA because EC expects tuple + signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), //RSAorHMAC because EC expects tuple payloadSerializer = DataClass.serializer(), ) val serialized = cose.serialize(DataClass.serializer()) @@ -195,7 +195,7 @@ class CoseSerializationTest : FreeSpec({ val inputObject = CoseSigned.create( protectedHeader = header, payload = payload, - signature = CryptoSignature.RSA("bar".encodeToByteArray()), + signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), payloadSerializer = ByteArraySerializer(), ).prepareCoseSignatureInput(byteArrayOf()) .encodeToString(Base16()) @@ -217,7 +217,7 @@ class CoseSerializationTest : FreeSpec({ val inputObject = CoseSigned.create( protectedHeader = header, payload = payload, - signature = CryptoSignature.RSA("bar".encodeToByteArray()), + signature = CryptoSignature.RSAorHMAC("bar".encodeToByteArray()), payloadSerializer = DataClass.serializer(), ).prepareCoseSignatureInput(byteArrayOf()) .encodeToString(Base16()) diff --git a/indispensable-cosef/src/jvmTest/kotlin/ConversionTests.kt b/indispensable-cosef/src/jvmTest/kotlin/ConversionTests.kt index 54737142..02270b0d 100644 --- a/indispensable-cosef/src/jvmTest/kotlin/ConversionTests.kt +++ b/indispensable-cosef/src/jvmTest/kotlin/ConversionTests.kt @@ -23,9 +23,9 @@ class ConversionTests : FreeSpec({ } } "COSE -> X509 -> COSE is stable" - { - withData(CoseAlgorithm.signatureAlgorithms/*here we filter by using only sig alg*/) { + withData(CoseAlgorithm.entries) { it.toX509SignatureAlgorithm().getOrNull()?.let { x509 -> - x509.toCoseAlgorithm().onSuccess { it.algorithm } shouldSucceedWith it.toCoseAlgorithm().getOrThrow() //here we go back to cose, after initially filtering, because cose does not discriminate between MAC and sig + x509.toCoseAlgorithm() shouldSucceedWith it } } } diff --git a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsAlgorithm.kt b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsAlgorithm.kt index 9ec4e38e..2f8ecd28 100644 --- a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsAlgorithm.kt +++ b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsAlgorithm.kt @@ -1,10 +1,11 @@ package at.asitplus.signum.indispensable.josef -import at.asitplus.KmmResult import at.asitplus.catching -import at.asitplus.signum.indispensable.* -import at.asitplus.signum.indispensable.mac.HMAC -import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode +import at.asitplus.signum.indispensable.Digest +import at.asitplus.signum.indispensable.ECCurve +import at.asitplus.signum.indispensable.RSAPadding +import at.asitplus.signum.indispensable.SignatureAlgorithm +import at.asitplus.signum.indispensable.SpecializedSignatureAlgorithm import kotlinx.serialization.KSerializer import kotlinx.serialization.Serializable import kotlinx.serialization.descriptors.PrimitiveKind @@ -18,7 +19,7 @@ import kotlinx.serialization.encoding.Encoder * Since we support only JWS algorithms (with one exception), this class is called what it's called. */ @Serializable(with = JwsAlgorithmSerializer::class) -enum class JwsAlgorithm(override val identifier: String) : JsonWebAlgorithm, SpecializedDataIntegrityAlgorithm { +enum class JwsAlgorithm(override val identifier: String) : JsonWebAlgorithm, SpecializedSignatureAlgorithm { ES256("ES256"), ES384("ES384"), @@ -41,58 +42,28 @@ enum class JwsAlgorithm(override val identifier: String) : JsonWebAlgorithm, Spe */ NON_JWS_SHA1_WITH_RSA("RS1"); - - companion object { - /** - * encompasses only signature algorithms, filtering out MACs - */ - val signatureAlgorithms = listOf( - ES256, - ES384, - ES512, - - PS256, - PS384, - PS512, - - RS256, - RS384, - RS512, - NON_JWS_SHA1_WITH_RSA - ).map { it.algorithm as SignatureAlgorithm } - - /** - * Encompasses only MACs, filtering our signature algorithms - */ - val messageAuthenticationCodes = listOf(HS256, HS384, HS512).map { it.algorithm as MessageAuthenticationCode } - + val digest: Digest get() = when(this) { + NON_JWS_SHA1_WITH_RSA -> Digest.SHA1 + ES256, HS256, PS256, RS256 -> Digest.SHA256 + ES384, HS384, PS384, RS384 -> Digest.SHA384 + ES512, HS512, PS512, RS512 -> Digest.SHA512 } - val digest: Digest - get() = when (this) { - NON_JWS_SHA1_WITH_RSA -> Digest.SHA1 - ES256, HS256, PS256, RS256 -> Digest.SHA256 - ES384, HS384, PS384, RS384 -> Digest.SHA384 - ES512, HS512, PS512, RS512 -> Digest.SHA512 - } - - override val algorithm: DataIntegrityAlgorithm - get() = when (this) { - ES256, ES384, ES512 -> SignatureAlgorithm.ECDSA(this.digest, this.ecCurve!!) - HS256, HS384, HS512 -> HMAC.byDigest(this.digest) - PS256, PS384, PS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PSS) - NON_JWS_SHA1_WITH_RSA, RS256, RS384, RS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PKCS1) - } + override val algorithm: SignatureAlgorithm get() = when (this) { + ES256, ES384, ES512 -> SignatureAlgorithm.ECDSA(this.digest, this.ecCurve!!) + HS256, HS384, HS512 -> SignatureAlgorithm.HMAC(this.digest) + PS256, PS384, PS512 -> SignatureAlgorithm.RSA(this. digest, RSAPadding.PSS) + NON_JWS_SHA1_WITH_RSA, RS256, RS384, RS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PKCS1) + } /** The curve to create signatures on. * This is fixed by RFC7518, as opposed to X.509 where other combinations are possible. */ - val ecCurve: ECCurve? - get() = when (this) { - ES256 -> ECCurve.SECP_256_R_1 - ES384 -> ECCurve.SECP_384_R_1 - ES512 -> ECCurve.SECP_521_R_1 - else -> null - } + val ecCurve: ECCurve? get() = when (this) { + ES256 -> ECCurve.SECP_256_R_1 + ES384 -> ECCurve.SECP_384_R_1 + ES512 -> ECCurve.SECP_521_R_1 + else -> null + } } object JwsAlgorithmSerializer : KSerializer<JwsAlgorithm> { @@ -109,12 +80,8 @@ object JwsAlgorithmSerializer : KSerializer<JwsAlgorithm> { } } - -fun SpecializedDataIntegrityAlgorithm.toJwsAlgorithm(): KmmResult<JwsAlgorithm> = - algorithm.toJwsAlgorithm() - /** Tries to find a matching JWS algorithm. Note that JWS imposes curve restrictions on ECDSA based on the digest. */ -fun DataIntegrityAlgorithm.toJwsAlgorithm(): KmmResult<JwsAlgorithm> = catching { +fun SignatureAlgorithm.toJwsAlgorithm() = catching { when (this) { is SignatureAlgorithm.ECDSA -> when (this.digest) { Digest.SHA256 -> JwsAlgorithm.ES256 @@ -122,7 +89,6 @@ fun DataIntegrityAlgorithm.toJwsAlgorithm(): KmmResult<JwsAlgorithm> = catching Digest.SHA512 -> JwsAlgorithm.ES512 else -> throw IllegalArgumentException("ECDSA with ${this.digest} is unsupported by JWS") } - is SignatureAlgorithm.RSA -> when (this.padding) { RSAPadding.PKCS1 -> when (this.digest) { Digest.SHA1 -> JwsAlgorithm.NON_JWS_SHA1_WITH_RSA @@ -130,26 +96,24 @@ fun DataIntegrityAlgorithm.toJwsAlgorithm(): KmmResult<JwsAlgorithm> = catching Digest.SHA384 -> JwsAlgorithm.RS384 Digest.SHA512 -> JwsAlgorithm.RS512 } - RSAPadding.PSS -> when (this.digest) { Digest.SHA256 -> JwsAlgorithm.PS256 Digest.SHA384 -> JwsAlgorithm.PS384 Digest.SHA512 -> JwsAlgorithm.PS512 else -> throw IllegalArgumentException("RSA-PSS with ${this.digest} is unsupported by JWS") } - } - - is HMAC -> when (this.digest) { + is SignatureAlgorithm.HMAC -> when (this.digest) { Digest.SHA256 -> JwsAlgorithm.HS256 Digest.SHA384 -> JwsAlgorithm.HS384 Digest.SHA512 -> JwsAlgorithm.HS512 else -> throw IllegalArgumentException("HMAC with ${this.digest} is unsupported by JWS") } - - else -> throw IllegalArgumentException("UnsupportedAlgorithm $this") - } } +/** Tries to find a matching JWS algorithm. Note that JWS imposes curve restrictions on ECDSA based on the digest. */ +fun SpecializedSignatureAlgorithm.toJwsAlgorithm() = + this.algorithm.toJwsAlgorithm() + diff --git a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt index 6c31b6e5..0bc1838e 100644 --- a/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt +++ b/indispensable-josef/src/commonMain/kotlin/at/asitplus/signum/indispensable/josef/JwsSigned.kt @@ -74,7 +74,7 @@ data class JwsSigned<out P : Any>( val payload = inputParts[1] val signature = with(inputParts[2]) { when (val curve = header.algorithm.ecCurve) { - null -> CryptoSignature.RSA(this) + null -> CryptoSignature.RSAorHMAC(this) else -> CryptoSignature.EC.fromRawBytes(curve, this) } } diff --git a/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/ConversionTest.kt b/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/ConversionTest.kt index d8f2610c..69b0c60f 100644 --- a/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/ConversionTest.kt +++ b/indispensable-josef/src/jvmTest/kotlin/at/asitplus/signum/indispensable/josef/ConversionTest.kt @@ -17,9 +17,9 @@ class ConversionTest : FreeSpec({ } } "JWS -> X509 -> JWS is stable" - { - withData(JwsAlgorithm.signatureAlgorithms /*here we filter s.t. only sig alg are fed into the test*/) { + withData(JwsAlgorithm.entries) { it.toX509SignatureAlgorithm().getOrNull()?.let { x509 -> - x509.toJwsAlgorithm() shouldSucceedWith it.toJwsAlgorithm().getOrThrow() /* and here we convert the sig algs back to JWS alg*/ + x509.toJwsAlgorithm() shouldSucceedWith it } } } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt index b5033a80..d8f59e9d 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/CryptoSignature.kt @@ -217,7 +217,7 @@ sealed interface CryptoSignature : Asn1Encodable<Asn1Element> { } - class RSA private constructor (rawBytes: ByteArray?, x509Element: Asn1Primitive?) : CryptoSignature, RawByteEncodable { + class RSAorHMAC private constructor (rawBytes: ByteArray?, x509Element: Asn1Primitive?) : CryptoSignature, RawByteEncodable { constructor(rawBytes: ByteArray) : this(rawBytes, null) constructor(x509Element: Asn1Primitive) : this(null, x509Element) @@ -241,16 +241,16 @@ sealed interface CryptoSignature : Asn1Encodable<Asn1Element> { if (this === other) return true if (other == null || this::class != other::class) return false - other as RSA + other as RSAorHMAC return signature == other.signature } - companion object : Asn1Decodable<Asn1Element, RSA> { + companion object : Asn1Decodable<Asn1Element, RSAorHMAC> { @Throws(Asn1Exception::class) - override fun doDecode(src: Asn1Element): RSA { + override fun doDecode(src: Asn1Element): RSAorHMAC { src as Asn1Primitive - return RSA(src) + return RSAorHMAC(src) } } } @@ -259,7 +259,7 @@ sealed interface CryptoSignature : Asn1Encodable<Asn1Element> { @Throws(Asn1Exception::class) override fun doDecode(src: Asn1Element): CryptoSignature = runRethrowing { when (src.tag) { - Asn1Element.Tag.BIT_STRING -> RSA.decodeFromTlv(src) + Asn1Element.Tag.BIT_STRING -> RSAorHMAC.decodeFromTlv(src) Asn1Element.Tag.SEQUENCE -> EC.decodeFromTlv(src) else -> throw Asn1Exception("Unknown Signature Format") diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/DataIntegrityAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/DataIntegrityAlgorithm.kt deleted file mode 100644 index d85fb548..00000000 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/DataIntegrityAlgorithm.kt +++ /dev/null @@ -1,14 +0,0 @@ -package at.asitplus.signum.indispensable - -import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode - -/** - * Umbrella interface encompassing _data integrity algorithms_: - * * Message Authentication Codes ([MessageAuthenticationCode]) - * * Digital Signatures ([SignatureAlgorithm]) - */ -interface DataIntegrityAlgorithm{ - companion object { - val entries: Iterable<DataIntegrityAlgorithm> = MessageAuthenticationCode.entries + SignatureAlgorithm.entries - } -} \ No newline at end of file diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt index 84739935..1659a602 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SignatureAlgorithm.kt @@ -5,7 +5,11 @@ enum class RSAPadding { PSS; } -sealed interface SignatureAlgorithm : DataIntegrityAlgorithm { +sealed interface SignatureAlgorithm { + data class HMAC( + /** The digest to use */ + val digest: Digest + ) : SignatureAlgorithm data class ECDSA( /** The digest to apply to the data, or `null` to directly process the raw data. */ @@ -34,22 +38,15 @@ sealed interface SignatureAlgorithm : DataIntegrityAlgorithm { val RSAwithSHA384andPSSPadding = RSA(Digest.SHA384, RSAPadding.PSS) val RSAwithSHA512andPSSPadding = RSA(Digest.SHA512, RSAPadding.PSS) - val entries: Iterable<SignatureAlgorithm> = listOf( - ECDSAwithSHA256, - ECDSAwithSHA384, - ECDSAwithSHA512, - - RSAwithSHA256andPSSPadding, - RSAwithSHA384andPSSPadding, - RSAwithSHA512andPSSPadding, - - RSAwithSHA256andPKCS1Padding, - RSAwithSHA384andPKCS1Padding, - RSAwithSHA512andPKCS1Padding - ) + @Deprecated("Not yet implemented", level = DeprecationLevel.ERROR) + val HMACwithSHA256 = HMAC(Digest.SHA256) + @Deprecated("Not yet implemented", level = DeprecationLevel.ERROR) + val HMACwithSHA384 = HMAC(Digest.SHA384) + @Deprecated("Not yet implemented", level = DeprecationLevel.ERROR) + val HMACwithSHA512 = HMAC(Digest.SHA512) } } -interface SpecializedDataIntegrityAlgorithm { - val algorithm: DataIntegrityAlgorithm +interface SpecializedSignatureAlgorithm { + val algorithm: SignatureAlgorithm } diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt index e3c4d9f5..43acfc3d 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/X509SignatureAlgorithm.kt @@ -18,13 +18,18 @@ import kotlinx.serialization.encoding.Encoder enum class X509SignatureAlgorithm( override val oid: ObjectIdentifier, val isEc: Boolean = false -) : Asn1Encodable<Asn1Sequence>, Identifiable, SpecializedDataIntegrityAlgorithm { +) : Asn1Encodable<Asn1Sequence>, Identifiable, SpecializedSignatureAlgorithm { // ECDSA with SHA-size ES256(KnownOIDs.ecdsaWithSHA256, true), ES384(KnownOIDs.ecdsaWithSHA384, true), ES512(KnownOIDs.ecdsaWithSHA512, true), + // HMAC-size with SHA-size + HS256(KnownOIDs.hmacWithSHA256), + HS384(KnownOIDs.hmacWithSHA384), + HS512(KnownOIDs.hmacWithSHA512), + // RSASSA-PSS with SHA-size PS256(KnownOIDs.rsaPSS), PS384(KnownOIDs.rsaPSS), @@ -79,6 +84,7 @@ enum class X509SignatureAlgorithm( PS512 -> encodePSSParams(512) + HS256, HS384, HS512, RS256, RS384, RS512, RS1 -> Asn1.Sequence { +oid +Null() @@ -87,14 +93,15 @@ enum class X509SignatureAlgorithm( val digest: Digest get() = when(this) { RS1 -> Digest.SHA1 - ES256, PS256, RS256 -> Digest.SHA256 - ES384, PS384, RS384 -> Digest.SHA384 - ES512, PS512, RS512 -> Digest.SHA512 + ES256, HS256, PS256, RS256 -> Digest.SHA256 + ES384, HS384, PS384, RS384 -> Digest.SHA384 + ES512, HS512, PS512, RS512 -> Digest.SHA512 } override val algorithm: SignatureAlgorithm get() = when(this) { ES256, ES384, ES512 -> SignatureAlgorithm.ECDSA(this.digest, null) + HS256, HS384, HS512 -> SignatureAlgorithm.HMAC(this.digest) PS256, PS384, PS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PSS) RS1, RS256, RS384, RS512 -> SignatureAlgorithm.RSA(this.digest, RSAPadding.PKCS1) } @@ -112,7 +119,8 @@ enum class X509SignatureAlgorithm( ES512.oid, ES384.oid, ES256.oid -> fromOid(oid) RS1.oid -> RS1 - RS256.oid, RS384.oid, RS512.oid-> fromOid(oid).also { + RS256.oid, RS384.oid, RS512.oid, + HS256.oid, HS384.oid, HS512.oid -> fromOid(oid).also { val tag = src.nextChild().tag if (tag != Asn1Element.Tag.NULL) throw Asn1TagMismatchException(Asn1Element.Tag.NULL, tag, "RSA Params not allowed.") @@ -184,8 +192,17 @@ fun SignatureAlgorithm.toX509SignatureAlgorithm() = catching { else -> throw IllegalArgumentException("Digest ${this.digest} is unsupported by X.509 RSA-PSS") } } + is SignatureAlgorithm.HMAC -> when (this.digest) { + Digest.SHA256 -> X509SignatureAlgorithm.HS256 + Digest.SHA384 -> X509SignatureAlgorithm.HS384 + Digest.SHA512 -> X509SignatureAlgorithm.HS512 + else -> throw IllegalArgumentException("Digest ${this.digest} is unsupported by X.509 HMAC") + } } } +/** Finds a X.509 signature algorithm matching this algorithm. Curve restrictions are not preserved. */ +fun SpecializedSignatureAlgorithm.toX509SignatureAlgorithm() = + this.algorithm.toX509SignatureAlgorithm() object X509SignatureAlgorithmSerializer : KSerializer<X509SignatureAlgorithm> { diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MAC.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MAC.kt new file mode 100644 index 00000000..6c7810b8 --- /dev/null +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MAC.kt @@ -0,0 +1,40 @@ +package at.asitplus.signum.indispensable.mac + +import at.asitplus.signum.indispensable.Digest +import at.asitplus.signum.indispensable.asn1.Identifiable +import at.asitplus.signum.indispensable.asn1.KnownOIDs +import at.asitplus.signum.indispensable.asn1.ObjectIdentifier +import at.asitplus.signum.indispensable.misc.BitLength + +sealed interface MAC { + /** output size of MAC */ + val outputLength: BitLength + + companion object { + val entries: Iterable<MAC> = HMAC.entries + } +} + +/** + * RFC 2104 HMAC + */ +enum class HMAC(val digest: Digest, override val oid: ObjectIdentifier) : MAC, Identifiable { + SHA1(Digest.SHA1, KnownOIDs.hmacWithSHA1), + SHA256(Digest.SHA256, KnownOIDs.hmacWithSHA256), + SHA384(Digest.SHA384, KnownOIDs.hmacWithSHA384), + SHA512(Digest.SHA512, KnownOIDs.hmacWithSHA512), + ; + + override fun toString() = "HMAC-$digest" + + companion object { + operator fun invoke(digest: Digest) = when (digest) { + Digest.SHA1 -> SHA1 + Digest.SHA256 -> SHA256 + Digest.SHA384 -> SHA384 + Digest.SHA512 -> SHA512 + } + } + + override val outputLength: BitLength get() = digest.outputLength +} diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt deleted file mode 100644 index cc507420..00000000 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt +++ /dev/null @@ -1,61 +0,0 @@ -package at.asitplus.signum.indispensable.mac - -import at.asitplus.signum.indispensable.DataIntegrityAlgorithm -import at.asitplus.signum.indispensable.Digest -import at.asitplus.signum.indispensable.asn1.* -import at.asitplus.signum.indispensable.asn1.encoding.Asn1 -import at.asitplus.signum.indispensable.asn1.encoding.Asn1.Null -import at.asitplus.signum.indispensable.asn1.encoding.readNull -import at.asitplus.signum.indispensable.misc.BitLength - -sealed interface MessageAuthenticationCode : DataIntegrityAlgorithm { - /** output size of MAC */ - val outputLength: BitLength - - companion object { - val entries: Iterable<MessageAuthenticationCode> = HMAC.entries - } -} - -/** - * RFC 2104 HMAC - */ -enum class HMAC(val digest: Digest, override val oid: ObjectIdentifier) : MessageAuthenticationCode, Identifiable, - Asn1Encodable<Asn1Sequence> { - SHA1(Digest.SHA1, KnownOIDs.hmacWithSHA1), - SHA256(Digest.SHA256, KnownOIDs.hmacWithSHA256), - SHA384(Digest.SHA384, KnownOIDs.hmacWithSHA384), - SHA512(Digest.SHA512, KnownOIDs.hmacWithSHA512), - ; - - override fun toString() = "HMAC-$digest" - - override fun encodeToTlv(): Asn1Sequence = Asn1.Sequence { - +oid - +Null() - } - - - companion object : Asn1Decodable<Asn1Sequence, HMAC> { - - fun byOID(oid: ObjectIdentifier): HMAC? = entries.find { it.oid == oid } - - fun byDigest(digest: Digest): HMAC = entries.find { it.digest == digest }!! - - operator fun invoke(digest: Digest) = when (digest) { - Digest.SHA1 -> SHA1 - Digest.SHA256 -> SHA256 - Digest.SHA384 -> SHA384 - Digest.SHA512 -> SHA512 - } - - override fun doDecode(src: Asn1Sequence): HMAC { - val oid = src.nextChild().asPrimitive().readOid() - src.nextChild().asPrimitive().readNull() - require(!src.hasMoreChildren()) { "Superfluous ANS.1 data in HMAC" } - return byOID(oid) ?: throw Asn1OidException(oid) - } - } - - override val outputLength: BitLength get() = digest.outputLength -} diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt index 147e9c9a..e9f38b49 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/pki/X509Certificate.kt @@ -224,7 +224,7 @@ constructor( val CryptoSignature.x509Encoded get() = when (this) { is CryptoSignature.EC -> encodeToDer().encodeToAsn1BitStringPrimitive() - is CryptoSignature.RSA -> encodeToTlv() + is CryptoSignature.RSAorHMAC -> encodeToTlv() } /** @@ -235,7 +235,7 @@ val CryptoSignature.x509Encoded fun CryptoSignature.Companion.fromX509Encoded(alg: X509SignatureAlgorithm, it: Asn1Primitive) = when (alg.isEc) { true -> CryptoSignature.EC.decodeFromDer(it.asAsn1BitString().rawBytes) - false -> CryptoSignature.RSA.decodeFromTlv(it) + false -> CryptoSignature.RSAorHMAC.decodeFromTlv(it) } /** diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricEncryptionAlgorithm.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricEncryptionAlgorithm.kt index b7314d76..2ba35669 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricEncryptionAlgorithm.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricEncryptionAlgorithm.kt @@ -7,7 +7,7 @@ import at.asitplus.signum.indispensable.asn1.KnownOIDs import at.asitplus.signum.indispensable.asn1.ObjectIdentifier import at.asitplus.signum.indispensable.asn1.encoding.encodeTo8Bytes import at.asitplus.signum.indispensable.mac.HMAC -import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode +import at.asitplus.signum.indispensable.mac.MAC import at.asitplus.signum.indispensable.misc.BitLength import at.asitplus.signum.indispensable.misc.bit import kotlin.contracts.ExperimentalContracts @@ -110,7 +110,7 @@ sealed interface SymmetricEncryptionAlgorithm<out A : AuthCapability<out K>, out interface Integrated<I : NonceTrait> : Authenticated<AuthCapability.Authenticated.Integrated, I, KeyType.Integrated> - interface WithDedicatedMac<M : MessageAuthenticationCode, I : NonceTrait> : + interface WithDedicatedMac<M : MAC, I : NonceTrait> : Authenticated<AuthCapability.Authenticated.WithDedicatedMac<M, I>, I, KeyType.WithDedicatedMacKey> } @@ -311,7 +311,7 @@ sealed interface AuthCapability<K : KeyType> { * An authenticated cipher construction based on an unauthenticated cipher with a dedicated MAC function, requiring a dedicated MAC key. * _Encrypt-then-MAC_ */ - class WithDedicatedMac<M : MessageAuthenticationCode, I : NonceTrait>( + class WithDedicatedMac<M : MAC, I : NonceTrait>( /** * The inner unauthenticated cipher */ @@ -406,14 +406,14 @@ val DefaultDedicatedMacAuthTagTransformation: DedicatedMacAuthTagTransformation /** * Typealias defining the signature of the lambda for defining a custom MAC input calculation scheme. */ -typealias DedicatedMacInputCalculation = MessageAuthenticationCode.(ciphertext: ByteArray, nonce: ByteArray, aad: ByteArray) -> ByteArray +typealias DedicatedMacInputCalculation = MAC.(ciphertext: ByteArray, nonce: ByteArray, aad: ByteArray) -> ByteArray /** * The default dedicated mac input calculation as per [RFC 7518](https://datatracker.ietf.org/doc/html/rfc7518#section-5.2.2.1), authenticating all inputs: * `AAD || IV || Ciphertext || AAD Length`, where AAD_length is a 64 bit big-endian representation of the aad length in bits */ val DefaultDedicatedMacInputCalculation: DedicatedMacInputCalculation = - fun MessageAuthenticationCode.(ciphertext: ByteArray, iv: ByteArray, aad: ByteArray): ByteArray = + fun MAC.(ciphertext: ByteArray, iv: ByteArray, aad: ByteArray): ByteArray = aad + iv + ciphertext + (aad.size.toLong()*8L).encodeTo8Bytes() /** diff --git a/indispensable/src/commonTest/kotlin/CryptoSignatureTest.kt b/indispensable/src/commonTest/kotlin/CryptoSignatureTest.kt index 0b3c51f5..a17391bb 100644 --- a/indispensable/src/commonTest/kotlin/CryptoSignatureTest.kt +++ b/indispensable/src/commonTest/kotlin/CryptoSignatureTest.kt @@ -20,9 +20,9 @@ class CryptoSignatureTest : FreeSpec({ val ec1 = CryptoSignature.EC.fromRS(first.toBigInteger(), second.toBigInteger()) val ec2 = CryptoSignature.EC.fromRS(first.toBigInteger(), second.toBigInteger()) val ec3 = CryptoSignature.EC.fromRS(second.toBigInteger(), first.toBigInteger()) - val rsa1 = CryptoSignature.RSA(first.toTwosComplementByteArray()) - val rsa2 = CryptoSignature.RSA(first.toTwosComplementByteArray()) - val rsa3 = CryptoSignature.RSA(second.toTwosComplementByteArray()) + val rsa1 = CryptoSignature.RSAorHMAC(first.toTwosComplementByteArray()) + val rsa2 = CryptoSignature.RSAorHMAC(first.toTwosComplementByteArray()) + val rsa3 = CryptoSignature.RSAorHMAC(second.toTwosComplementByteArray()) ec1 shouldBe ec1 ec1 shouldBe ec2 diff --git a/indispensable/src/iosMain/kotlin/CommonCryptoExtensions.kt b/indispensable/src/iosMain/kotlin/CommonCryptoExtensions.kt index f5bbcab4..b1806733 100644 --- a/indispensable/src/iosMain/kotlin/CommonCryptoExtensions.kt +++ b/indispensable/src/iosMain/kotlin/CommonCryptoExtensions.kt @@ -40,8 +40,13 @@ val SignatureAlgorithm.secKeyAlgorithm: SecKeyAlgorithm } } + is SignatureAlgorithm.HMAC -> TODO("HMAC is unsupported") }!! +val SpecializedSignatureAlgorithm.secKeyAlgorithm + get() = + this.algorithm.secKeyAlgorithm + val SignatureAlgorithm.secKeyAlgorithmPreHashed: SecKeyAlgorithm get() = when (this) { is SignatureAlgorithm.ECDSA -> { @@ -72,16 +77,17 @@ val SignatureAlgorithm.secKeyAlgorithmPreHashed: SecKeyAlgorithm } } + is SignatureAlgorithm.HMAC -> TODO("HMAC is unsupported") }!! -val X509SignatureAlgorithm.secKeyAlgorithmPreHashed +val SpecializedSignatureAlgorithm.secKeyAlgorithmPreHashed get() = this.algorithm.secKeyAlgorithmPreHashed val CryptoSignature.iosEncoded get() = when (this) { is CryptoSignature.EC -> this.encodeToDer() - is CryptoSignature.RSA -> this.rawByteArray + is CryptoSignature.RSAorHMAC -> this.rawByteArray } fun CryptoPublicKey.toSecKey() = catching { diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/EnumConsistencyTests.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/EnumConsistencyTests.kt index 4aa6e241..fb045341 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/EnumConsistencyTests.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/EnumConsistencyTests.kt @@ -1,6 +1,6 @@ package at.asitplus.signum.indispensable -import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode +import at.asitplus.signum.indispensable.mac.MAC import io.kotest.core.spec.style.FreeSpec import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe @@ -40,5 +40,5 @@ inline fun<reified T: Any> FreeSpec.enumConsistencyTest() { } class EnumConsistencyTests : FreeSpec({ - enumConsistencyTest<MessageAuthenticationCode>() + enumConsistencyTest<MAC>() }) \ No newline at end of file diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/SignatureCodecTest.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/SignatureCodecTest.kt index 9923e11b..d018fa5f 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/SignatureCodecTest.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/SignatureCodecTest.kt @@ -72,7 +72,7 @@ class SignatureCodecTest : FreeSpec({ sign() } - CryptoSignature.RSA.parseFromJca(sig).jcaSignatureBytes shouldBe sig + CryptoSignature.RSAorHMAC.parseFromJca(sig).jcaSignatureBytes shouldBe sig CryptoSignature.parseFromJca( sig, SignatureAlgorithm.RSA(Digest.valueOf(digest), RSAPadding.PKCS1) @@ -99,7 +99,7 @@ class SignatureCodecTest : FreeSpec({ val bcSig = (ASN1Sequence.fromByteArray(certificateHolder.encoded) as DLSequence).elementAt(2) .toASN1Primitive().encoded - CryptoSignature.RSA.parseFromJca(certificateHolder.signature).encodeToDer() shouldBe bcSig + CryptoSignature.RSAorHMAC.parseFromJca(certificateHolder.signature).encodeToDer() shouldBe bcSig CryptoSignature.parseFromJca( certificateHolder.signature, SignatureAlgorithm.RSA(Digest.valueOf(digest), RSAPadding.PKCS1) diff --git a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509ConversionTests.kt b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509ConversionTests.kt index 8365699d..f9a2e13d 100644 --- a/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509ConversionTests.kt +++ b/indispensable/src/jvmTest/kotlin/at/asitplus/signum/indispensable/pki/X509ConversionTests.kt @@ -12,6 +12,7 @@ infix fun <T> KmmResult<T>.shouldSucceedWith(b: T) : T = class X509ConversionTests : FreeSpec({ "X509 -> Alg -> X509 i…
Co-authored-by: Jakob Heher <[email protected]>
…nsable/symmetric/SymmetricEncryptionAlgorithm.kt Co-authored-by: Jakob Heher <[email protected]>
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt index 488f30bb..cc91a84c 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt @@ -1,6 +1,5 @@ package at.asitplus.signum.indispensable.mac -import at.asitplus.signum.indispensable.DataIntegrityAlgorithm import at.asitplus.signum.indispensable.Digest import at.asitplus.signum.indispensable.asn1.* import at.asitplus.signum.indispensable.asn1.encoding.Asn1 diff --git a/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/AES.jca.kt b/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/AES.jca.kt index e3634a3f..7761b116 100644 --- a/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/AES.jca.kt +++ b/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/AES.jca.kt @@ -2,7 +2,6 @@ package at.asitplus.signum.supreme.symmetric import at.asitplus.signum.HazardousMaterials import at.asitplus.signum.indispensable.symmetric.AuthCapability -import at.asitplus.signum.indispensable.symmetric.KeyType import at.asitplus.signum.indispensable.symmetric.SymmetricEncryptionAlgorithm import javax.crypto.Cipher import javax.crypto.spec.GCMParameterSpec @@ -12,41 +11,31 @@ import javax.crypto.spec.SecretKeySpec @OptIn(HazardousMaterials::class) internal object AESJCA { fun initCipher( + mode: PlatformCipher.Mode, algorithm: SymmetricEncryptionAlgorithm.AES<*, *, *>, key: ByteArray, nonce: ByteArray?, aad: ByteArray? - ) = - Cipher.getInstance(algorithm.jcaName).apply { - val cipher = algorithm.authCapability - if (cipher is AuthCapability.Authenticated.Integrated) - init( - Cipher.ENCRYPT_MODE, - SecretKeySpec(key, algorithm.jcaKeySpec), - GCMParameterSpec(cipher.tagLength.bits.toInt(), nonce) - ) - else if (algorithm is SymmetricEncryptionAlgorithm.AES.CBC<*, *>) //covers unauthenticated and CBC-HMAC, because CBC will always delegate to here - init( - Cipher.ENCRYPT_MODE, - SecretKeySpec(key, algorithm.jcaKeySpec), - IvParameterSpec(nonce) - ) - else if ((algorithm is SymmetricEncryptionAlgorithm.AES.ECB) || algorithm is SymmetricEncryptionAlgorithm.AES.WRAP.RFC3394) { - init( - Cipher.ENCRYPT_MODE, - SecretKeySpec(key, algorithm.jcaKeySpec), - ) - } - - else TODO("Algorithm $algorithm is unsupported ") - aad?.let { if (algorithm is SymmetricEncryptionAlgorithm.AES.GCM) updateAAD(it) /*CBC-HMAC we do ourselves*/ } - }.let { - @Suppress("UNCHECKED_CAST") - PlatformCipher<Cipher, AuthCapability<KeyType>, KeyType>( - algorithm as SymmetricEncryptionAlgorithm<AuthCapability<KeyType>,*, KeyType>, - it, - nonce, - aad + ) = Cipher.getInstance(algorithm.jcaName).apply { + val cipher = algorithm.authCapability + if (cipher is AuthCapability.Authenticated.Integrated) + init( + mode.jcaCipherMode, + SecretKeySpec(key, algorithm.jcaKeySpec), + GCMParameterSpec(cipher.tagLength.bits.toInt(), nonce) ) - } + else if (algorithm is SymmetricEncryptionAlgorithm.AES.CBC<*, *>) //covers unauthenticated and CBC-HMAC, because CBC will always delegate to here + init( + mode.jcaCipherMode, + SecretKeySpec(key, algorithm.jcaKeySpec), + IvParameterSpec(nonce) + ) + else if ((algorithm is SymmetricEncryptionAlgorithm.AES.ECB) || algorithm is SymmetricEncryptionAlgorithm.AES.WRAP.RFC3394) { + init( + mode.jcaCipherMode, + SecretKeySpec(key, algorithm.jcaKeySpec), + ) + } else TODO("Algorithm $algorithm is unsupported ") + aad?.let { if (algorithm is SymmetricEncryptionAlgorithm.AES.GCM) updateAAD(it) /*CBC-HMAC we do ourselves*/ } + } } diff --git a/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/ChaCha.jca.kt b/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/ChaCha.jca.kt index 0a0c2f5e..20376fb1 100644 --- a/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/ChaCha.jca.kt +++ b/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/ChaCha.jca.kt @@ -1,20 +1,18 @@ package at.asitplus.signum.supreme.symmetric -import at.asitplus.signum.indispensable.symmetric.AuthCapability -import at.asitplus.signum.indispensable.symmetric.KeyType import at.asitplus.signum.indispensable.symmetric.SymmetricEncryptionAlgorithm import javax.crypto.Cipher import javax.crypto.spec.IvParameterSpec import javax.crypto.spec.SecretKeySpec internal object ChaChaJVM { - fun initCipher(key: ByteArray, nonce: ByteArray, aad: ByteArray?): PlatformCipher<Cipher, AuthCapability.Authenticated.Integrated, KeyType.Integrated> = + fun initCipher(mode: PlatformCipher.Mode, key: ByteArray, nonce: ByteArray, aad: ByteArray?): Cipher = Cipher.getInstance(SymmetricEncryptionAlgorithm.ChaCha20Poly1305.jcaName).apply { init( - Cipher.ENCRYPT_MODE, + mode.jcaCipherMode, SecretKeySpec(key, SymmetricEncryptionAlgorithm.ChaCha20Poly1305.jcaKeySpec), IvParameterSpec(nonce) ) - aad?.let { updateAAD(it) } - }.let { PlatformCipher(SymmetricEncryptionAlgorithm.ChaCha20Poly1305, it, nonce, aad) } + aad?.let { updateAAD(it) } + } } \ No newline at end of file diff --git a/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.jca.kt b/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.jca.kt index c39372ab..31f1bcdf 100644 --- a/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.jca.kt +++ b/supreme/src/androidJvmMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.jca.kt @@ -2,75 +2,89 @@ package at.asitplus.signum.supreme.symmetric import at.asitplus.signum.HazardousMaterials import at.asitplus.signum.indispensable.symmetric.* -import at.asitplus.signum.indispensable.symmetric.AuthCapability.Authenticated import javax.crypto.Cipher -import javax.crypto.spec.GCMParameterSpec -import javax.crypto.spec.IvParameterSpec -import javax.crypto.spec.SecretKeySpec -internal actual fun <T, A : AuthCapability<out K>, I : NonceTrait, K : KeyType> initCipher( +internal actual suspend fun <A : AuthCapability<out K>, I : NonceTrait, K : KeyType> initCipher( + mode: PlatformCipher.Mode, algorithm: SymmetricEncryptionAlgorithm<A, I, K>, key: ByteArray, nonce: ByteArray?, aad: ByteArray? -): PlatformCipher<T, A, out K> = when { - algorithm.requiresNonce() -> { - @OptIn(HazardousMaterials::class) - val nonce = nonce ?: algorithm.randomNonce() +): PlatformCipher<A, I, K> = JcaPlatformCipher(mode, algorithm, key, nonce, aad) + +internal class JcaPlatformCipher<A : AuthCapability<out K>, I : NonceTrait, K : KeyType>( + override val mode: PlatformCipher.Mode, + override val algorithm: SymmetricEncryptionAlgorithm<A, I, K>, + override val key: ByteArray, + override val nonce: ByteArray?, + override val aad: ByteArray?, +) : PlatformCipher<A, I, K> { + + + internal val cipher: Cipher = + when { + algorithm.requiresNonce() -> { + + + @Suppress("UNCHECKED_CAST") + when (algorithm) { + is SymmetricEncryptionAlgorithm.ChaCha20Poly1305 -> ChaChaJVM.initCipher(mode, key, nonce!!, aad) + is SymmetricEncryptionAlgorithm.AES<*, *, *> -> AESJCA.initCipher(mode, algorithm, key, nonce, aad) + } + } + + else -> { + @OptIn(HazardousMaterials::class) + if ((algorithm !is SymmetricEncryptionAlgorithm.AES.ECB) && (algorithm !is SymmetricEncryptionAlgorithm.AES.WRAP.RFC3394)) + TODO("$algorithm is UNSUPPORTED") + AESJCA.initCipher(mode, algorithm, key, nonce, aad) + } + } + + override suspend fun doEncrypt(data: ByteArray): SealedBox<A, I, out K> { + require(mode == PlatformCipher.Mode.ENCRYPT) { "Cipher not in ENCRYPT mode!" } + val jcaCiphertext = cipher.doFinal(data) + //JCA simply concatenates ciphertext and authtag, so we need to split + val ciphertext = + if (algorithm.authCapability is AuthCapability.Authenticated<*>) + jcaCiphertext.dropLast(((algorithm.authCapability as AuthCapability.Authenticated<*>).tagLength.bytes.toInt()).toInt()) + .toByteArray() + else jcaCiphertext + val authTag = + if (algorithm.authCapability is AuthCapability.Authenticated<*>) + jcaCiphertext.takeLast(((algorithm.authCapability as AuthCapability.Authenticated<*>).tagLength.bytes.toInt()).toInt()) + .toByteArray() else null @Suppress("UNCHECKED_CAST") - when (algorithm) { - is SymmetricEncryptionAlgorithm.ChaCha20Poly1305 -> ChaChaJVM.initCipher(key, nonce, aad) - is SymmetricEncryptionAlgorithm.AES<*, *, *> -> AESJCA.initCipher(algorithm, key, nonce, aad) - } as PlatformCipher<T, A, K> - } + return when { + algorithm.requiresNonce() -> when { + algorithm.isAuthenticated() -> { + (algorithm as SymmetricEncryptionAlgorithm<AuthCapability.Authenticated<*>, NonceTrait.Required, *>) + algorithm.sealedBoxFrom(nonce!!, ciphertext, authTag!!) + } - else -> { - @OptIn(HazardousMaterials::class) - if ((algorithm !is SymmetricEncryptionAlgorithm.AES.ECB) && (algorithm !is SymmetricEncryptionAlgorithm.AES.WRAP.RFC3394)) - TODO("$algorithm is UNSUPPORTED") - - AESJCA.initCipher(algorithm, key, nonce, aad) as PlatformCipher<T, A, K> - } -} - -internal actual fun <A : AuthCapability<out K>, I : NonceTrait, K : KeyType> PlatformCipher<*, A, out K>.doEncrypt(data: ByteArray): SealedBox<A, I, out K> { - @Suppress("UNCHECKED_CAST") (this as PlatformCipher<Cipher, A, K>) - val jcaCiphertext = platformData.doFinal(data) - - //JCA simply concatenates ciphertext and authtag, so we need to split - val ciphertext = - if (alg.authCapability is AuthCapability.Authenticated<*>) - jcaCiphertext.dropLast(((alg.authCapability as AuthCapability.Authenticated<*>).tagLength.bytes.toInt()).toInt()) - .toByteArray() - else jcaCiphertext - val authTag = - if (alg.authCapability is AuthCapability.Authenticated<*>) - jcaCiphertext.takeLast(((alg.authCapability as AuthCapability.Authenticated<*>).tagLength.bytes.toInt()).toInt()) - .toByteArray() else null - - - @Suppress("UNCHECKED_CAST") - return when { - alg.requiresNonce() -> when { - alg.isAuthenticated() -> { - (alg as SymmetricEncryptionAlgorithm<AuthCapability.Authenticated<*>, NonceTrait.Required, *>) - alg.sealedBoxFrom(nonce!!, ciphertext, authTag!!) + else -> algorithm.sealedBoxFrom(nonce!!, ciphertext) } - else -> alg.sealedBoxFrom(nonce!!, ciphertext) - } + else -> when { + algorithm.isAuthenticated() -> { + (algorithm as SymmetricEncryptionAlgorithm<AuthCapability.Authenticated<*>, NonceTrait.Without, *>) + algorithm.sealedBoxFrom(ciphertext, authTag!!) + } - else -> when { - alg.isAuthenticated() -> { - (alg as SymmetricEncryptionAlgorithm<AuthCapability.Authenticated<*>, NonceTrait.Without, *>) - alg.sealedBoxFrom(ciphertext, authTag!!) + else -> algorithm.sealedBoxFrom(ciphertext) } - else -> alg.sealedBoxFrom(ciphertext) - } + }.getOrThrow() as SealedBox<A, I, out K> + } - }.getOrThrow() as SealedBox<A, I, out K> + override suspend fun doDecrypt(data: ByteArray, authTag: ByteArray?): ByteArray { + require(mode == PlatformCipher.Mode.DECRYPT) { "Cipher not in DECRYPT mode!" } + return when (algorithm.isAuthenticated()) { + true -> cipher.doFinal(data + authTag!!) + false -> cipher.doFinal(data) + } + } } val SymmetricEncryptionAlgorithm<*, *, *>.jcaName: String @@ -91,68 +105,9 @@ val SymmetricEncryptionAlgorithm<*, *, *>.jcaKeySpec: String else -> TODO("$this keyspec is unsupported UNSUPPORTED") } -@JvmName("doDecryptAuthenticated") -internal actual fun SealedBox<Authenticated.Integrated, *, out KeyType.Integrated>.doDecryptAEAD( - secretKey: ByteArray, - authenticatedData: ByteArray -): ByteArray { - if (!this.hasNonce()) TODO("AEAD algorithm $algorithm is UNSUPPORTED") - - if ((algorithm !is SymmetricEncryptionAlgorithm.ChaCha20Poly1305) && (algorithm !is SymmetricEncryptionAlgorithm.AES.GCM)) - TODO("AEAD algorithm $algorithm is UNSUPPORTED") - - return aeadDecrypt( - algorithm, - secretKey, - nonce, - encryptedData, - authTag, - authenticatedData - ) - -} - -internal actual fun SealedBox<AuthCapability.Unauthenticated, *, out KeyType.Integrated>.doDecrypt( - secretKey: ByteArray -): ByteArray { - if (algorithm !is SymmetricEncryptionAlgorithm.AES<*, *, *>) - TODO("unauthenticated algorithm $algorithm is UNSUPPORTED") - - @OptIn(HazardousMaterials::class) - if ((algorithm is SymmetricEncryptionAlgorithm.AES.ECB) || (algorithm is SymmetricEncryptionAlgorithm.AES.WRAP.RFC3394)) { - return Cipher.getInstance(algorithm.jcaName).also { cipher -> - cipher.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(secretKey, algorithm.jcaKeySpec), - ) - }.doFinal(encryptedData) +internal val PlatformCipher.Mode.jcaCipherMode + get() = when (this) { + PlatformCipher.Mode.ENCRYPT -> Cipher.ENCRYPT_MODE + PlatformCipher.Mode.DECRYPT -> Cipher.DECRYPT_MODE } - this as SealedBox.WithNonce - return Cipher.getInstance(algorithm.jcaName).also { cipher -> - cipher.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(secretKey, algorithm.jcaKeySpec), - IvParameterSpec([email protected]) - ) - }.doFinal(encryptedData) -} - -internal fun aeadDecrypt( - algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated<KeyType.Integrated>, NonceTrait.Required, KeyType.Integrated>, - secretKey: ByteArray, - nonce: ByteArray, - encryptedData: ByteArray, - authTag: ByteArray, - aad: ByteArray? -): ByteArray = Cipher.getInstance(algorithm.jcaName).also { cipher -> - cipher.init( - Cipher.DECRYPT_MODE, - SecretKeySpec(secretKey, algorithm.jcaKeySpec), - if (algorithm is SymmetricEncryptionAlgorithm.AES.GCM) - GCMParameterSpec(authTag.size * 8, nonce) - else IvParameterSpec(nonce) - ) - aad?.let { cipher.updateAAD(it) } -}.doFinal(encryptedData + authTag) - diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.kt index 4ec14bf3..1395a2a7 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.kt @@ -1,5 +1,6 @@ package at.asitplus.signum.supreme.symmetric +import at.asitplus.signum.HazardousMaterials import at.asitplus.signum.ImplementationError import at.asitplus.signum.indispensable.symmetric.* import at.asitplus.signum.indispensable.symmetric.AuthCapability.Authenticated @@ -10,88 +11,130 @@ internal class Encryptor<A : AuthCapability<out K>, I : NonceTrait, out K : KeyT private val algorithm: SymmetricEncryptionAlgorithm<A, I, K>, private val key: ByteArray, private val macKey: ByteArray?, - private val iv: ByteArray?, + @OptIn(HazardousMaterials::class) + private val nonce: ByteArray? = if (algorithm.requiresNonce()) algorithm.randomNonce() else null, private val aad: ByteArray?, ) { init { - if (algorithm.nonceTrait is NonceTrait.Required) iv?.let { + + + if (algorithm.nonceTrait is NonceTrait.Required) nonce?.let { require(it.size.toUInt() == (algorithm.nonceTrait as NonceTrait.Required).length.bytes) { "IV must be exactly ${(algorithm.nonceTrait as NonceTrait.Required).length} bits long" } } require(key.size.toUInt() == algorithm.keySize.bytes) { "Key must be exactly ${algorithm.keySize} bits long" } } - private val platformCipher: PlatformCipher<*, A, out K> = initCipher<Any, A, I, K>(algorithm, key, iv, aad) - /** * Encrypts [data] and returns a [at.asitplus.signum.indispensable.symmetric.Ciphertext] matching the algorithm type that was used to create this [Encryptor] object. * E.g., an authenticated encryption algorithm causes this function to return a [at.asitplus.signum.indispensable.symmetric.Ciphertext.Authenticated]. */ - internal fun encrypt(data: ByteArray): SealedBox<A, I, out K> = ( - //Our own, flexible construction to make any unauthenticated cipher into an authenticated cipher - if (algorithm.hasDedicatedMac()) { - val aMac = algorithm.authCapability - aMac.innerCipher - val innerCipher = - initCipher<Any, AuthCapability.Unauthenticated, I, KeyType.Integrated>( - aMac.innerCipher, - key, - iv, - aad - ) + internal suspend fun encrypt(data: ByteArray): SealedBox<A, I, out K> { + //Our own, flexible construction to make any unauthenticated cipher into an authenticated cipher + if (algorithm.hasDedicatedMac()) { + val aMac = algorithm.authCapability + aMac.innerCipher + val innerCipher = + initCipher( + PlatformCipher.Mode.ENCRYPT, + aMac.innerCipher, + key, + nonce, + aad + ) - if (!aMac.innerCipher.requiresNonce()) throw ImplementationError("AES-CBC-HMAC Nonce inconsistency") - if (macKey == null) throw ImplementationError("AES-CBC-HMAC MAC key is null") + if (!aMac.innerCipher.requiresNonce()) throw ImplementationError("AES-CBC-HMAC Nonce inconsistency") + if (macKey == null) throw ImplementationError("AES-CBC-HMAC MAC key is null") - val encrypted = innerCipher.doEncrypt<AuthCapability.Unauthenticated, I, KeyType.Integrated>(data) - val macInputCalculation = aMac.dedicatedMacInputCalculation - val hmacInput: ByteArray = - aMac.mac.macInputCalculation( - encrypted.encryptedData, - innerCipher.nonce ?: byteArrayOf(), - aad ?: byteArrayOf() - ) + val encrypted = innerCipher.doEncrypt(data) + val macInputCalculation = aMac.dedicatedMacInputCalculation + val hmacInput: ByteArray = + aMac.mac.macInputCalculation( + encrypted.encryptedData, + innerCipher.nonce ?: byteArrayOf(), + aad ?: byteArrayOf() + ) - val outputTransform = aMac.dedicatedMacAuthTagTransform - val authTag = aMac.outputTransform(aMac.mac.mac(macKey, hmacInput).getOrThrow()) + val outputTransform = aMac.dedicatedMacAuthTagTransform + val authTag = aMac.outputTransform(aMac.mac.mac(macKey, hmacInput).getOrThrow()) - @Suppress("UNCHECKED_CAST") - (if (algorithm.requiresNonce()) { - (algorithm).sealedBoxFrom( - (encrypted as SealedBox.WithNonce<*, *>).nonce, - encrypted.encryptedData, - authTag - ) - } else (algorithm).sealedBoxFrom( + @Suppress("UNCHECKED_CAST") + return (if (algorithm.requiresNonce()) { + (algorithm).sealedBoxFrom( + (encrypted as SealedBox.WithNonce<*, *>).nonce, encrypted.encryptedData, authTag - )).getOrThrow() as SealedBox<A, I, K> + ) + } else (algorithm).sealedBoxFrom( + encrypted.encryptedData, + authTag + )).getOrThrow() as SealedBox<A, I, K> - } else platformCipher.doEncrypt(data)) + } else @Suppress("UNCHECKED_CAST") return initCipher( + PlatformCipher.Mode.ENCRYPT, + algorithm, + key, + nonce, + aad + ).doEncrypt(data) as SealedBox<A, I, out K> + } } -internal class PlatformCipher<T, A : AuthCapability<out K>, K : KeyType>( - val alg: SymmetricEncryptionAlgorithm<A, *, K>, - val platformData: T, - val nonce: ByteArray?, +/** + * Platform cipher abstraction. + */ +internal interface PlatformCipher<A : AuthCapability<out K>, I : NonceTrait, K : KeyType> { + /** + * We could do away with the encrypt/decrypt state and add subclassses, etc. but then we'd just have double the glue + * code, because every platform cipher works that way. + */ + val mode: Mode + val algorithm: SymmetricEncryptionAlgorithm<A, I, K> + val key: ByteArray + val nonce: ByteArray? val aad: ByteArray? -) + //for later use, we could add val oneshot:Boolean =true here and add update() and doFinal() -internal expect fun SealedBox<Authenticated.Integrated, *, out KeyType.Integrated>.doDecryptAEAD( + abstract suspend fun doDecrypt(data: ByteArray, authTag: ByteArray?): ByteArray + + abstract suspend fun doEncrypt(data: ByteArray): SealedBox<A, I, out K> + + enum class Mode { + ENCRYPT, + DECRYPT, + ; + } +} + + +internal suspend fun SealedBox<Authenticated.Integrated, *, out KeyType.Integrated>.doDecryptAEAD( secretKey: ByteArray, authenticatedData: ByteArray -): ByteArray +): ByteArray { + val authTag = if (isAuthenticated()) authTag else null + val nonce = if (hasNonce()) nonce else null -internal expect fun SealedBox<AuthCapability.Unauthenticated, *, out KeyType.Integrated>.doDecrypt( + return initCipher(PlatformCipher.Mode.DECRYPT, algorithm, secretKey, nonce, authenticatedData).doDecrypt( + encryptedData, + authTag + ) +} + +internal suspend fun SealedBox<AuthCapability.Unauthenticated, *, out KeyType.Integrated>.doDecrypt( secretKey: ByteArray -): ByteArray +): ByteArray { + val nonce = if (hasNonce()) nonce else null + return initCipher(PlatformCipher.Mode.DECRYPT, algorithm, secretKey, nonce, null).doDecrypt(encryptedData, null) +} - -internal expect fun <T, A : AuthCapability<out K>, I : NonceTrait, K : KeyType> initCipher( +internal expect suspend fun <A : AuthCapability<out K>, I : NonceTrait, K : KeyType> initCipher( + mode: PlatformCipher.Mode, algorithm: SymmetricEncryptionAlgorithm<A, I, K>, key: ByteArray, nonce: ByteArray?, aad: ByteArray? -): PlatformCipher<T, A, out K> +): PlatformCipher<A, I, K> -internal expect fun <A : AuthCapability<out K>, I : NonceTrait, K : KeyType> PlatformCipher<*, A, out K>.doEncrypt(data: ByteArray): SealedBox<A, I, out K> + +internal suspend fun SealedBox<*, *, *>.initDecrypt(key: ByteArray, aad: ByteArray?): PlatformCipher<*, *, *> = + initCipher(PlatformCipher.Mode.DECRYPT, algorithm, key, if (hasNonce()) nonce else null, aad) \ No newline at end of file diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/KeyGen.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/KeyGen.kt index 23f7d9ff..f75db909 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/KeyGen.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/KeyGen.kt @@ -15,7 +15,7 @@ private val secureRandom = SecureRandom() * Generates a fresh random key for this algorithm. */ @Suppress("UNCHECKED_CAST") -fun <A : AuthCapability<out K>, I : NonceTrait, K : KeyType> SymmetricEncryptionAlgorithm<A, I, K>.randomKey(): SymmetricKey<A, I, out K> = +suspend fun <A : AuthCapability<out K>, I : NonceTrait, K : KeyType> SymmetricEncryptionAlgorithm<A, I, K>.randomKey(): SymmetricKey<A, I, out K> = keyFromInternal( secureRandom.nextBytesOf(keySize.bytes.toInt()), if (authCapability.keyType is KeyType.WithDedicatedMacKey) secureRandom.nextBytesOf(keySize.bytes.toInt()) @@ -28,7 +28,7 @@ fun <A : AuthCapability<out K>, I : NonceTrait, K : KeyType> SymmetricEncryption */ @JvmName("randomKeyAndMacKey") @Suppress("UNCHECKED_CAST") -fun <I : NonceTrait> SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.WithDedicatedMac<*, I>, I, KeyType.WithDedicatedMacKey>.randomKey( +suspend fun <I : NonceTrait> SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.WithDedicatedMac<*, I>, I, KeyType.WithDedicatedMacKey>.randomKey( macKeyLength: BitLength = preferredMacKeyLength ): SymmetricKey.WithDedicatedMac<I> = keyFromInternal( diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt index 02383926..03597908 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt @@ -18,7 +18,7 @@ import kotlin.jvm.JvmName * [key] and [SealedBox].** */ @JvmName("decryptGeneric") -fun SealedBox<*, *, *>.decrypt(key: SymmetricKey<*, *, *>): KmmResult<ByteArray> = catching { +suspend fun SealedBox<*, *, *>.decrypt(key: SymmetricKey<*, *, *>): KmmResult<ByteArray> = catching { require(algorithm == key.algorithm) { "Algorithm mismatch! expected: $algorithm, actual: ${key.algorithm}" } @Suppress("UNCHECKED_CAST") when (algorithm.authCapability) { @@ -49,7 +49,7 @@ fun SealedBox<*, *, *>.decrypt(key: SymmetricKey<*, *, *>): KmmResult<ByteArray> * [key] and [SealedBox].** */ @JvmName("decryptAuthenticatedIntegrated") -fun <I : NonceTrait, M : MessageAuthenticationCode> SealedBox<AuthCapability.Authenticated.WithDedicatedMac<M, I>, I, KeyType.WithDedicatedMacKey>.decrypt( +suspend fun <I : NonceTrait, M : MessageAuthenticationCode> SealedBox<AuthCapability.Authenticated.WithDedicatedMac<M, I>, I, KeyType.WithDedicatedMacKey>.decrypt( key: SymmetricKey.WithDedicatedMac<*>, authenticatedData: ByteArray = byteArrayOf() ) = catching { @@ -68,7 +68,7 @@ fun <I : NonceTrait, M : MessageAuthenticationCode> SealedBox<AuthCapability.Aut * [key] and [SealedBox].** */ @JvmName("decryptAuthenticatedGeneric") -fun <A : AuthCapability.Authenticated<out K>, K : KeyType> SealedBox<A, *, out K>.decrypt( +suspend fun <A : AuthCapability.Authenticated<out K>, K : KeyType> SealedBox<A, *, out K>.decrypt( key: SymmetricKey<A, *, out K>, authenticatedData: ByteArray = byteArrayOf() ): KmmResult<ByteArray> = catching { @@ -98,7 +98,7 @@ fun <A : AuthCapability.Authenticated<out K>, K : KeyType> SealedBox<A, *, out K * a [AES.CBC] [SealedBox]. * In such cases, this function will immediately return a [KmmResult.failure]. */ -fun <I : NonceTrait> SealedBox<AuthCapability.Unauthenticated, I, KeyType.Integrated>.decrypt( +suspend fun <I : NonceTrait> SealedBox<AuthCapability.Unauthenticated, I, KeyType.Integrated>.decrypt( key: SymmetricKey<AuthCapability.Unauthenticated, I, KeyType.Integrated> ): KmmResult<ByteArray> = catching { require(algorithm == key.algorithm) { "Algorithm mismatch! expected: $algorithm, actual: ${key.algorithm}" } @@ -118,23 +118,23 @@ fun <I : NonceTrait> SealedBox<AuthCapability.Unauthenticated, I, KeyType.Integr * In such cases, this function will immediately return a [KmmResult.failure]. */ @JvmName("decryptRawAuthenticated") -private fun SealedBox<Authenticated.Integrated, *, out KeyType.Integrated>.decryptInternal( +private suspend fun SealedBox<Authenticated.Integrated, *, out KeyType.Integrated>.decryptInternal( secretKey: ByteArray, authenticatedData: ByteArray ): ByteArray { require(secretKey.size.toUInt() == algorithm.keySize.bytes) { "Key must be exactly ${algorithm.keySize} bits long" } - return doDecryptAEAD(secretKey, authenticatedData) + return initDecrypt(secretKey, authenticatedData).doDecrypt(encryptedData, authTag) } @JvmName("decryptRaw") -private fun SealedBox<AuthCapability.Unauthenticated, *, out KeyType.Integrated>.decryptInternal( +private suspend fun SealedBox<AuthCapability.Unauthenticated, *, out KeyType.Integrated>.decryptInternal( secretKey: ByteArray ): ByteArray { require(secretKey.size.toUInt() == algorithm.keySize.bytes) { "Key must be exactly ${algorithm.keySize} bits long" } - return doDecrypt(secretKey) + return initDecrypt(secretKey, null).doDecrypt(encryptedData, null) } -private fun SealedBox<Authenticated.WithDedicatedMac<*, *>, *, out KeyType.WithDedicatedMacKey>.decryptInternal( +private suspend fun SealedBox<Authenticated.WithDedicatedMac<*, *>, *, out KeyType.WithDedicatedMacKey>.decryptInternal( secretKey: ByteArray, macKey: ByteArray = secretKey, authenticatedData: ByteArray @@ -159,7 +159,7 @@ private fun SealedBox<Authenticated.WithDedicatedMac<*, *>, *, out KeyType.WithD ) else (innerCipher as SymmetricEncryptionAlgorithm<AuthCapability.Unauthenticated, NonceTrait.Without, KeyType.Integrated>).sealedBoxFrom( encryptedData )).getOrThrow() as SealedBox<AuthCapability.Unauthenticated, *, KeyType.Integrated> - return box.doDecrypt(secretKey) + return box.initDecrypt(secretKey, null).doDecrypt(box.encryptedData, null) } @@ -170,7 +170,7 @@ private fun SealedBox<Authenticated.WithDedicatedMac<*, *>, *, out KeyType.WithD * Directly decrypts raw [encryptedData], feeding [nonce] into the decryption process. */ @JvmName("decryptRawUnauthedWithNonce") -fun SymmetricKey<AuthCapability.Unauthenticated, NonceTrait.Required, KeyType.Integrated>.decrypt( +suspend fun SymmetricKey<AuthCapability.Unauthenticated, NonceTrait.Required, KeyType.Integrated>.decrypt( nonce: ByteArray, encryptedData: ByteArray ): KmmResult<ByteArray> = algorithm.sealedBoxFrom(nonce, encryptedData).transform { it.decrypt(this) } @@ -180,7 +180,7 @@ fun SymmetricKey<AuthCapability.Unauthenticated, NonceTrait.Required, KeyType.In * Directly decrypts raw [encryptedData]. */ @JvmName("decryptRawUnauthedNoNonce") -fun SymmetricKey<AuthCapability.Unauthenticated, NonceTrait.Without, KeyType.Integrated>.decrypt( +suspend fun SymmetricKey<AuthCapability.Unauthenticated, NonceTrait.Without, KeyType.Integrated>.decrypt( encryptedData: ByteArray ): KmmResult<ByteArray> = algorithm.sealedBoxFrom(encryptedData).transform { it.decrypt(this) } @@ -190,7 +190,7 @@ fun SymmetricKey<AuthCapability.Unauthenticated, NonceTrait.Without, KeyType.Int * @return [at.asitplus.KmmResult.failure] on illegal auth tag length */ @JvmName("decryptRawAuthedWithNonce") -fun <A : AuthCapability.Authenticated<*>> SymmetricKey<A, NonceTrait.Required, *>.decrypt( +suspend fun <A : AuthCapability.Authenticated<*>> SymmetricKey<A, NonceTrait.Required, *>.decrypt( nonce: ByteArray, encryptedData: ByteArray, authTag: ByteArray, @@ -203,7 +203,7 @@ fun <A : AuthCapability.Authenticated<*>> SymmetricKey<A, NonceTrait.Required, * * @return [at.asitplus.KmmResult.failure] on illegal auth tag length */ @JvmName("decryptRawAuthedNoNonce") -fun <A : AuthCapability.Authenticated<*>> SymmetricKey<A, NonceTrait.Without, *>.decrypt( +suspend fun <A : AuthCapability.Authenticated<*>> SymmetricKey<A, NonceTrait.Without, *>.decrypt( encryptedData: ByteArray, authTag: ByteArray, authenticatedData: ByteArray = byteArrayOf() diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/discouraged/encrypt.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/discouraged/encrypt.kt index 873677bc..714b45d2 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/discouraged/encrypt.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/discouraged/encrypt.kt @@ -21,7 +21,7 @@ import kotlin.jvm.JvmName */ @HazardousMaterials("NEVER re-use a nonce/IV! Have them auto-generated instead!") @JvmName("encryptAuthenticatedWithNonce") -fun <K : KeyType, A : AuthCapability.Authenticated<out K>> KeyWithNonceAuthenticating<A, out K>.encrypt( +suspend fun <K : KeyType, A : AuthCapability.Authenticated<out K>> KeyWithNonceAuthenticating<A, out K>.encrypt( data: ByteArray, authenticatedData: ByteArray? = null ): KmmResult<SealedBox<A, NonceTrait.Required, out K>> = catching { @@ -45,7 +45,7 @@ fun <K : KeyType, A : AuthCapability.Authenticated<out K>> KeyWithNonceAuthentic */ @HazardousMaterials("NEVER re-use a nonce/IV! Have them auto-generated instead!") @JvmName("encryptWithNonce") -fun <K : KeyType, A : AuthCapability<out K>> KeyWithNonce<A, out K>.encrypt( +suspend fun <K : KeyType, A : AuthCapability<out K>> KeyWithNonce<A, out K>.encrypt( data: ByteArray ): KmmResult<SealedBox.WithNonce<A, out K>> = catching { Encryptor( diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/encrypt.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/encrypt.kt index c45d3700..2c6a4453 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/encrypt.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/encrypt.kt @@ -12,15 +12,14 @@ import kotlin.jvm.JvmName * invalid parameters (e.g., algorithm mismatch, key length, …) */ @JvmName("encryptWithAutoGenIV") -fun <K : KeyType, A : AuthCapability<out K>, I : NonceTrait> SymmetricKey<A, I, out K>.encrypt( +suspend fun <K : KeyType, A : AuthCapability<out K>, I : NonceTrait> SymmetricKey<A, I, out K>.encrypt( data: ByteArray ): KmmResult<SealedBox<A, I, out K>> = catching { Encryptor( algorithm, if (this.hasDedicatedMacKey()) encryptionKey else secretKey, if (this.hasDedicatedMacKey()) macKey else null, - null, - null, + aad = null ).encrypt(data) } @@ -38,7 +37,7 @@ fun <K : KeyType, A : AuthCapability<out K>, I : NonceTrait> SymmetricKey<A, I, * invalid parameters (e.g., algorithm mismatch, key length, …) */ @JvmName("encryptAuthenticated") -fun <K : KeyType, A : AuthCapability.Authenticated<out K>, I : NonceTrait> SymmetricKey<A, I, out K>.encrypt( +suspend fun <K : KeyType, A : AuthCapability.Authenticated<out K>, I : NonceTrait> SymmetricKey<A, I, out K>.encrypt( data: ByteArray, authenticatedData: ByteArray? = null ): KmmResult<SealedBox<A, I, out K>> = catching { @@ -46,7 +45,6 @@ fun <K : KeyType, A : AuthCapability.Authenticated<out K>, I : NonceTrait> Symme algorithm, if (this.hasDedicatedMacKey()) encryptionKey else secretKey, if (this.hasDedicatedMacKey()) macKey else null, - null, - authenticatedData, + aad = authenticatedData, ).encrypt(data) } diff --git a/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.ios.kt b/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.ios.kt index 1c8d6a8d..5999f282 100644 --- a/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.ios.kt +++ b/supreme/src/iosMain/kotlin/at/asitplus/signum/supreme/symmetric/Encryptor.ios.kt @@ -3,79 +3,79 @@ package at.asitplus.signum.supreme.symmetric import at.asitplus.signum.HazardousMaterials import at.asitplus.signum.ImplementationError import at.asitplus.signum.indispensable.symmetric.* -import at.asitplus.signum.indispensable.symmetric.AuthCapability.Authenticated import at.asitplus.signum.indispensable.symmetric.SymmetricEncryptionAlgorithm.AES import kotlinx.cinterop.ExperimentalForeignApi -internal actual fun <T, A : AuthCapability<out K>, I : NonceTrait, K : KeyType> initCipher( +internal actual suspend fun <A : AuthCapability<out K>, I : NonceTrait, K : KeyType> initCipher( + mode: PlatformCipher.Mode, algorithm: SymmetricEncryptionAlgorithm<A, I, K>, key: ByteArray, nonce: ByteArray?, aad: ByteArray? -): PlatformCipher<T, A, out K> { +): PlatformCipher<A, I, K> { + + @Suppress("UNCHECKED_CAST") + return IosPlatformCipher<A, I, K>(mode, algorithm, key, nonce, aad) +} + + +private class IosPlatformCipher<A : AuthCapability<out K>, I : NonceTrait, K : KeyType>( + override val mode: PlatformCipher.Mode, + override val algorithm: SymmetricEncryptionAlgorithm<A, I, K>, + override val key: ByteArray, + override val nonce: ByteArray?, + override val aad: ByteArray?, +) : PlatformCipher<A, I, K> { + + //the oneshot ccrypt is fully stateless. no init, no update, no final, so we are stateless here too @OptIn(HazardousMaterials::class) - val nonce = if (algorithm.requiresNonce()) nonce ?: algorithm.randomNonce() else null + override suspend fun doDecrypt(data: ByteArray, authTag: ByteArray?): ByteArray { + require(mode == PlatformCipher.Mode.DECRYPT) { "Cipher not in DECRYPT mode!" } + return when (algorithm.isAuthenticated()) { + true -> { + if (!algorithm.isIntegrated()) throw ImplementationError("iOS AEAD algorithm mapping") + if (algorithm.nonceTrait !is NonceTrait.Required) TODO("ALGORITHM $algorithm UNSUPPORTED") + when (algorithm) { + is AES<*, *, *> -> AESIOS.gcmDecrypt(data, key, nonce!!, authTag!!, aad) + is SymmetricEncryptionAlgorithm.ChaCha20Poly1305 -> ChaChaIOS.decrypt( + data, + key, + nonce!!, + authTag!!, + aad + ) + else -> TODO("ALGORITHM UNSUPPORTED") + } + } - @Suppress("UNCHECKED_CAST") - return PlatformCipher<ByteArray, AuthCapability<out KeyType>, KeyType>( - algorithm, key, nonce, aad - ) as PlatformCipher<T, A, K> -} + false -> { + require(algorithm is AES<*, *, *>) { "Only AES is supported" } + @Suppress("UNCHECKED_CAST") + AESIOS.cbcEcbCrypt( + algorithm as AES<*, KeyType.Integrated, *>, + encrypt = false, + key, + nonce, + data, + pad = when (algorithm) { + is AES.CBC.Unauthenticated, is AES.ECB -> true + is AES.WRAP.RFC3394 -> false + } + ) + } + } + } -@OptIn(ExperimentalForeignApi::class) -internal actual fun <A : AuthCapability<out K>, I : NonceTrait, K : KeyType> PlatformCipher<*, A, out K>.doEncrypt(data: ByteArray): SealedBox<A, I, out K> { - @Suppress("UNCHECKED_CAST") (this as PlatformCipher<ByteArray, A, K>) - - @Suppress("UNCHECKED_CAST") - return when (alg) { - is AES<*, *, *> -> AESIOS.encrypt(alg, data, platformData, nonce, aad) - is SymmetricEncryptionAlgorithm.ChaCha20Poly1305 -> ChaChaIOS.encrypt(data, platformData, nonce!!, aad) - else -> TODO("ALGORITHM $alg UNSUPPORTED") - } as SealedBox<A, I, K> -} - - -@OptIn(ExperimentalForeignApi::class) -internal actual fun SealedBox<Authenticated.Integrated, *, out KeyType.Integrated>.doDecryptAEAD( - secretKey: ByteArray, - authenticatedData: ByteArray -): ByteArray { - if (algorithm.nonceTrait !is NonceTrait.Required) TODO("ALGORITHM $algorithm UNSUPPORTED") - this as SealedBox.WithNonce - return when (algorithm) { - is AES<*, *, *> -> AESIOS.gcmDecrypt(encryptedData, secretKey, nonce, authTag, authenticatedData) - is SymmetricEncryptionAlgorithm.ChaCha20Poly1305 -> ChaChaIOS.decrypt( - encryptedData, - secretKey, - nonce, - authTag, - authenticatedData - ) - - else -> TODO("ALGORITHM UNSUPPORTED") + @OptIn(ExperimentalForeignApi::class) + override suspend fun doEncrypt(data: ByteArray): SealedBox<A, I, out K> { + require(mode == PlatformCipher.Mode.ENCRYPT) { "Cipher not in ENCRYPT mode!" } + @Suppress("UNCHECKED_CAST") + return when (algorithm) { + is AES<*, *, *> -> AESIOS.encrypt(algorithm, data, key, nonce, aad) + is SymmetricEncryptionAlgorithm.ChaCha20Poly1305 -> ChaChaIOS.encrypt(data, key, nonce!!, aad) + else -> TODO("ALGORITHM $algorithm UNSUPPORTED") + } as SealedBox<A, I, K> } } - -@OptIn(ExperimentalForeignApi::class, HazardousMaterials::class) -internal actual fun SealedBox<AuthCapability.Unauthenticated, *, out KeyType.Integrated>.doDecrypt( - secretKey: ByteArray -): ByteArray { - require(algorithm is AES<*, *, *>) { "Only AES is supported" } - - return AESIOS.cbcEcbCrypt( - algorithm as AES<*, KeyType.Integrated, *>, - encrypt = false, - secretKey, - if (this is SealedBox.WithNonce) nonce else null, - encryptedData, - pad = when (algorithm) { - is AES.CBC.Unauthenticated, is AES.ECB -> true - is AES.WRAP.RFC3394 -> false - else -> throw ImplementationError("Illegal AES encryption state.") - - } - ) - -}
diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt index cc91a84c..488f30bb 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/mac/MessageAuthenticationCode.kt @@ -1,5 +1,6 @@ package at.asitplus.signum.indispensable.mac +import at.asitplus.signum.indispensable.DataIntegrityAlgorithm import at.asitplus.signum.indispensable.Digest import at.asitplus.signum.indispensable.asn1.* import at.asitplus.signum.indispensable.asn1.encoding.Asn1 diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SealedBoxCreation.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SealedBoxCreation.kt index 6cb8b62a..30926c56 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SealedBoxCreation.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SealedBoxCreation.kt @@ -3,6 +3,7 @@ package at.asitplus.signum.indispensable.symmetric import at.asitplus.KmmResult import at.asitplus.catching import at.asitplus.signum.indispensable.misc.bytes +import kotlin.jvm.JvmName /** * Creates a [SealedBox] matching the characteristics of the underlying [SealedBoxBuilder.algorithm]. @@ -43,6 +44,7 @@ fun SealedBoxBuilder.WithNonce.Having<AuthCapability.Unauthenticated, KeyType.In * * @return [at.asitplus.KmmResult.failure] on illegal auth tag length */ +@JvmName("fromAuthenticatedGeneric") fun SealedBoxBuilder.Without.Authenticated<AuthCapability.Authenticated<*>, *>.from( encryptedData: ByteArray, authTag: ByteArray @@ -59,6 +61,7 @@ fun SealedBoxBuilder.Without.Authenticated<AuthCapability.Authenticated<*>, *>.f * * @return [at.asitplus.KmmResult.failure] on illegal auth tag length */ +@JvmName("fromAuthenticatedWihtKeyType") fun <K : KeyType> SealedBoxBuilder.Without.Authenticated<AuthCapability.Authenticated<out K>, K>.from( encryptedData: ByteArray, authTag: ByteArray @@ -75,6 +78,7 @@ fun <K : KeyType> SealedBoxBuilder.Without.Authenticated<AuthCapability.Authenti * Use this function to load external encrypted data for decryption. * Returns a KmmResult purely for the sake of consistency */ +@JvmName("fromUnauthenticatedWithoutNonce") fun SealedBoxBuilder.Without<AuthCapability.Unauthenticated, KeyType.Integrated>.from( encryptedData: ByteArray, ): KmmResult<SealedBox<AuthCapability.Unauthenticated, NonceTrait.Without, KeyType.Integrated>> = catching { diff --git a/supreme/build.gradle.kts b/supreme/build.gradle.kts index dfb07b4c..6356473c 100644 --- a/supreme/build.gradle.kts +++ b/supreme/build.gradle.kts @@ -31,6 +31,8 @@ version = supremeVersion wireAndroidInstrumentedTests() kotlin { + compilerOptions.freeCompilerArgs.add("-Xexpect-actual-classes") + applyDefaultHierarchyTemplate() jvm() androidTarget { @OptIn(ExperimentalKotlinGradlePluginApi::class) @@ -59,7 +61,6 @@ kotlin { sourceSets.androidMain.dependencies { implementation("androidx.biometric:biometric:1.2.0-alpha05") } - } swiftklib { diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt index bc047729..55bf083c 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt @@ -52,6 +52,7 @@ suspend fun <I : NonceTrait, M : MessageAuthenticationCode> SealedBox<AuthCapabi key: SymmetricKey.WithDedicatedMac<*>, authenticatedData: ByteArray = byteArrayOf() ) = catching { + @Suppress("UNCHECKED_CAST") (this as SealedBox<Authenticated.WithDedicatedMac<*, *>, *, KeyType.WithDedicatedMacKey>).decryptInternal( key.encryptionKey, key.macKey, @@ -84,7 +85,7 @@ suspend fun <A : AuthCapability.Authenticated<out K>,I: NonceTrait, K : KeyType> key.encryptionKey, key.macKey, authenticatedData ) } - + //compiler knows its exhaustive, but IDEA complains else -> throw ImplementationError("Authenticated Decryption") } } @@ -188,8 +189,8 @@ suspend fun <A : AuthCapability.Authenticated<*>> SymmetricKey<A, NonceTrait.Wit authenticatedData: ByteArray = byteArrayOf() ): KmmResult<ByteArray> = algorithm.sealedBox.from(encryptedData, authTag).transform { + @Suppress("UNCHECKED_CAST") it.decrypt( - @Suppress("UNCHECKED_CAST") this as SymmetricKey<AuthCapability.Authenticated<*>, NonceTrait.Without, *>, authenticatedData )
diff --git a/indispensable/build.gradle.kts b/indispensable/build.gradle.kts index a82e9e05..5ffff7db 100644 --- a/indispensable/build.gradle.kts +++ b/indispensable/build.gradle.kts @@ -57,6 +57,7 @@ kotlin { api(libs.multibase) api(libs.bignum) implementation(project(":internals")) + api(libs.securerandom) } diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/KeyGen.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/KeyGen.kt similarity index 100% rename from supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/KeyGen.kt rename to indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/KeyGen.kt diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricKey.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricKey.kt index 019eab05..83e5bef5 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricKey.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricKey.kt @@ -49,7 +49,7 @@ sealed interface SymmetricKey<A : AuthCapability<out K>, I : NonceTrait, K : Key ) : Integrated<AuthCapability.Authenticated.Integrated, I>(algorithm, additionalProperties, secretKey), SymmetricKey.Authenticating<AuthCapability.Authenticated.Integrated, I, KeyType.Integrated> { - class RequiringNonce( + class RequiringNonce internal constructor( algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.Integrated, NonceTrait.Required, KeyType.Integrated>, secretKey: ByteArray, additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>() @@ -57,7 +57,7 @@ sealed interface SymmetricKey<A : AuthCapability<out K>, I : NonceTrait, K : Key algorithm, additionalProperties, secretKey ), SymmetricKey.RequiringNonce<AuthCapability.Authenticated.Integrated, KeyType.Integrated> - class WithoutNonce( + class WithoutNonce internal constructor( algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.Integrated, NonceTrait.Without, KeyType.Integrated>, secretKey: ByteArray, additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>() @@ -72,13 +72,13 @@ sealed interface SymmetricKey<A : AuthCapability<out K>, I : NonceTrait, K : Key secretKey: ByteArray ) : Integrated<AuthCapability.Unauthenticated, I>(algorithm, additionalProperties, secretKey), SymmetricKey.NonAuthenticating<I> { - class RequiringNonce( + class RequiringNonce internal constructor( algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Unauthenticated, NonceTrait.Required, KeyType.Integrated>, secretKey: ByteArray, additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>() ) : NonAuthenticating<NonceTrait.Required>(algorithm, additionalProperties, secretKey) - class WithoutNonce( + class WithoutNonce internal constructor( algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Unauthenticated, NonceTrait.Without, KeyType.Integrated>, secretKey: ByteArray, additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>() @@ -121,7 +121,7 @@ sealed interface SymmetricKey<A : AuthCapability<out K>, I : NonceTrait, K : Key val macKey: ByteArray ) : SymmetricKey<AuthCapability.Authenticated.WithDedicatedMac<*, I>, I, KeyType.WithDedicatedMacKey>, SymmetricKey.Authenticating<AuthCapability.Authenticated.WithDedicatedMac<*, I>, I, KeyType.WithDedicatedMacKey> { - class RequiringNonce( + class RequiringNonce internal constructor( algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Required>, NonceTrait.Required, KeyType.WithDedicatedMacKey>, secretKey: ByteArray, dedicatedMacKey: ByteArray, @@ -131,15 +131,14 @@ sealed interface SymmetricKey<A : AuthCapability<out K>, I : NonceTrait, K : Key ), SymmetricKey.RequiringNonce<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Required>, KeyType.WithDedicatedMacKey> - class WithoutNonce( + class WithoutNonce internal constructor( algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Without>, NonceTrait.Without, KeyType.WithDedicatedMacKey>, secretKey: ByteArray, dedicatedMacKey: ByteArray, additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>() ) : WithDedicatedMac<NonceTrait.Without>( algorithm, additionalProperties, secretKey, dedicatedMacKey - ), - SymmetricKey.WithoutNonce<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Without>, KeyType.WithDedicatedMacKey> + ), SymmetricKey.WithoutNonce<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Without>, KeyType.WithDedicatedMacKey> override fun equals(other: Any?): Boolean { if (this === other) return true diff --git a/supreme/build.gradle.kts b/supreme/build.gradle.kts index 6356473c..e8c1dd2e 100644 --- a/supreme/build.gradle.kts +++ b/supreme/build.gradle.kts @@ -55,7 +55,6 @@ kotlin { implementation(project(":internals")) implementation(coroutines()) implementation(napier()) - implementation(libs.securerandom) } sourceSets.androidMain.dependencies {
diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4f65a0..8d74702b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,8 @@ * `ivLength` and `encryptionKeyLength` now return `BitLength` instead of `Int` * `text` is now properly called `identifier` * Moved `HazardousMaterials` annotation from `supreme` to `indispensable` +* Moves `SecretExposure` annotation from `supreme` to `indispensable` +* Expose `SecureRandom` as API dependency in `indispensable` * Rename `ObjectIdentifier.parse` -> `ObjectIdentifier.decodeFromAsn1ContentBytes` in accordance with other similar functions * Add dedicated Android targets (SDK 30 /JDK 1.8) to all modules diff --git a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt index 6bff9f32..64bb9d6a 100644 --- a/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt +++ b/demoapp/composeApp/src/commonMain/kotlin/at/asitplus/cryptotest/App.kt @@ -58,7 +58,7 @@ import at.asitplus.cryptotest.theme.LocalThemeIsDark import at.asitplus.signum.indispensable.CryptoPublicKey import at.asitplus.signum.indispensable.KeyAgreementPrivateValue import at.asitplus.signum.indispensable.jsonEncoded -import at.asitplus.signum.supreme.SecretExposure +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.supreme.agree.Ephemeral import at.asitplus.signum.supreme.asKmmResult import at.asitplus.signum.supreme.os.PlatformSignerConfigurationBase diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SecretExposure.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SecretExposure.kt new file mode 100644 index 00000000..fb8c8a21 --- /dev/null +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/SecretExposure.kt @@ -0,0 +1,5 @@ +package at.asitplus.signum.indispensable + +@RequiresOptIn(message = "Access to secret and private key material requires explicit opt-in. Specify @OptIn(SecretExposure::class). Make sure that you actually want to externalise a secret. Check yourself, before you really, really, wreck yourself!") +/** This guards a secret. Do not expose it lightly! */ +annotation class SecretExposure diff --git a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricKey.kt b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricKey.kt index 72bd78f5..0666e8e2 100644 --- a/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricKey.kt +++ b/indispensable/src/commonMain/kotlin/at/asitplus/signum/indispensable/symmetric/SymmetricKey.kt @@ -1,6 +1,8 @@ package at.asitplus.signum.indispensable.symmetric +import at.asitplus.KmmResult import at.asitplus.signum.HazardousMaterials +import at.asitplus.signum.indispensable.SecretExposure import kotlin.contracts.ExperimentalContracts import kotlin.contracts.contract @@ -32,24 +34,47 @@ sealed interface SymmetricKey<A : AuthCapability<out K>, I : NonceTrait, K : Key /** * Self-Contained encryption key, i.e. a single byte array is sufficient */ - sealed class Integrated<A : AuthCapability<KeyType.Integrated>, I : NonceTrait> - protected constructor( - override val algorithm: SymmetricEncryptionAlgorithm<A, I, KeyType.Integrated>, + sealed interface Integrated<A : AuthCapability<KeyType.Integrated>, I : NonceTrait> : + SymmetricKey<A, I, KeyType.Integrated> { + + + override val algorithm: SymmetricEncryptionAlgorithm<A, I, KeyType.Integrated> - override val additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>(), /** * The actual encryption key bytes */ - val secretKey: ByteArray - ) : - SymmetricKey<A, I, KeyType.Integrated> { + @SecretExposure + val secretKey: KmmResult<ByteArray> + + sealed class Authenticating<I : NonceTrait>( - algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.Integrated, I, KeyType.Integrated>, - additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>(), + override val algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.Integrated, I, KeyType.Integrated>, + override val additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>(), secretKey: ByteArray - ) : Integrated<AuthCapability.Authenticated.Integrated, I>(algorithm, additionalProperties, secretKey), + ) : Integrated<AuthCapability.Authenticated.Integrated, I>, SymmetricKey.Authenticating<AuthCapability.Authenticated.Integrated, I, KeyType.Integrated> { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Integrated<*, *>) return false + + if (algorithm != other.algorithm) return false + @OptIn(SecretExposure::class) + if (!secretKey.getOrNull().contentEquals(other.secretKey.getOrNull())) return false + + return true + } + + override fun hashCode(): Int { + var result = algorithm.hashCode() + @OptIn(SecretExposure::class) + result = 31 * result + secretKey.getOrNull().contentHashCode() + return result + } + + @SecretExposure + override val secretKey = KmmResult.success(secretKey) + class RequiringNonce internal constructor( algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.Integrated, NonceTrait.Required, KeyType.Integrated>, secretKey: ByteArray, @@ -68,11 +93,34 @@ sealed interface SymmetricKey<A : AuthCapability<out K>, I : NonceTrait, K : Key } sealed class NonAuthenticating<I : NonceTrait>( - algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Unauthenticated, I, KeyType.Integrated>, - additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>(), + override val algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Unauthenticated, I, KeyType.Integrated>, + override val additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>(), secretKey: ByteArray - ) : Integrated<AuthCapability.Unauthenticated, I>(algorithm, additionalProperties, secretKey), + ) : Integrated<AuthCapability.Unauthenticated, I>, SymmetricKey.NonAuthenticating<I> { + + @SecretExposure + override val secretKey = KmmResult.success(secretKey) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Integrated<*, *>) return false + + if (algorithm != other.algorithm) return false + @OptIn(SecretExposure::class) + if (!secretKey.getOrNull().contentEquals(other.secretKey.getOrNull())) return false + + return true + } + + override fun hashCode(): Int { + var result = algorithm.hashCode() + @OptIn(SecretExposure::class) + result = 31 * result + secretKey.getOrNull().contentHashCode() + return result + } + + class RequiringNonce internal constructor( algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Unauthenticated, NonceTrait.Required, KeyType.Integrated>, secretKey: ByteArray, @@ -86,21 +134,7 @@ sealed interface SymmetricKey<A : AuthCapability<out K>, I : NonceTrait, K : Key ) : NonAuthenticating<NonceTrait.Without>(algorithm, additionalProperties, secretKey) } - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is Integrated<*, *>) return false - if (algorithm != other.algorithm) return false - if (!secretKey.contentEquals(other.secretKey)) return false - - return true - } - - override fun hashCode(): Int { - var result = algorithm.hashCode() - result = 31 * result + secretKey.contentHashCode() - return result - } } @@ -108,58 +142,116 @@ sealed interface SymmetricKey<A : AuthCapability<out K>, I : NonceTrait, K : Key * Encryption key with dedicated MAC key. Used for non-authenticated ciphers that use an external MAC function to * bolt on AEAD capabilities, such as [SymmetricEncryptionAlgorithm.AES.GCM] */ - sealed class WithDedicatedMac<I : NonceTrait> - protected constructor( - override val algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.WithDedicatedMac<*, I>, I, KeyType.WithDedicatedMacKey>, - override val additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>(), + sealed interface WithDedicatedMac<I : NonceTrait> + : SymmetricKey<AuthCapability.Authenticated.WithDedicatedMac<*, I>, I, KeyType.WithDedicatedMacKey>, + SymmetricKey.Authenticating<AuthCapability.Authenticated.WithDedicatedMac<*, I>, I, KeyType.WithDedicatedMacKey> { + + + override val algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.WithDedicatedMac<*, I>, I, KeyType.WithDedicatedMacKey> + /** * The actual encryption key bytes */ - val encryptionKey: ByteArray, + @SecretExposure + val encryptionKey: KmmResult<ByteArray> + /** * The actual dedicated MAC key bytes */ - val macKey: ByteArray - ) : SymmetricKey<AuthCapability.Authenticated.WithDedicatedMac<*, I>, I, KeyType.WithDedicatedMacKey>, - SymmetricKey.Authenticating<AuthCapability.Authenticated.WithDedicatedMac<*, I>, I, KeyType.WithDedicatedMacKey> { + @SecretExposure + val macKey: KmmResult<ByteArray> + class RequiringNonce @HazardousMaterials("This constructor is public to enable testing. DO NOT USE IT!") constructor( - algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Required>, NonceTrait.Required, KeyType.WithDedicatedMacKey>, - secretKey: ByteArray, + override val algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Required>, NonceTrait.Required, KeyType.WithDedicatedMacKey>, + encryptionKey: ByteArray, dedicatedMacKey: ByteArray, - additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>() - ) : WithDedicatedMac<NonceTrait.Required>( - algorithm, additionalProperties, secretKey, dedicatedMacKey - ), - SymmetricKey.RequiringNonce<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Required>, KeyType.WithDedicatedMacKey> + override val additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>() + ) : WithDedicatedMac<NonceTrait.Required>, + SymmetricKey.RequiringNonce<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Required>, KeyType.WithDedicatedMacKey> { + /** + * The actual encryption key bytes + * + * This will fail for hardware-backed keys! + */ + @SecretExposure + override val encryptionKey: KmmResult<ByteArray> = KmmResult.success(encryptionKey) - class WithoutNonce internal constructor( - algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Without>, NonceTrait.Without, KeyType.WithDedicatedMacKey>, - secretKey: ByteArray, - dedicatedMacKey: ByteArray, - additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>() - ) : WithDedicatedMac<NonceTrait.Without>( - algorithm, additionalProperties, secretKey, dedicatedMacKey - ), - SymmetricKey.WithoutNonce<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Without>, KeyType.WithDedicatedMacKey> + /** + * The actual dedicated MAC key bytes + * + * This will fail for hardware-backed keys! + */ + @SecretExposure + override val macKey: KmmResult<ByteArray> = KmmResult.success(dedicatedMacKey) - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (other !is WithDedicatedMac<*>) return false + @OptIn(SecretExposure::class) + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is WithDedicatedMac<*>) return false + + if (algorithm != other.algorithm) return false + if (!encryptionKey.getOrNull().contentEquals(other.encryptionKey.getOrNull())) return false + if (!macKey.getOrNull().contentEquals(other.macKey.getOrNull())) return false + if (additionalProperties != other.additionalProperties) return false + + return true + } + + @OptIn(SecretExposure::class) + override fun hashCode(): Int { + var result = algorithm.hashCode() + result = 31 * result + encryptionKey.getOrNull().contentHashCode() + result = 31 * result + macKey.getOrNull().contentHashCode() + return result + } - if (algorithm != other.algorithm) return false - if (!encryptionKey.contentEquals(other.encryptionKey)) return false - if (!macKey.contentEquals(other.macKey)) return false - if (additionalProperties != other.additionalProperties) return false - return true } - override fun hashCode(): Int { - var result = algorithm.hashCode() - result = 31 * result + encryptionKey.contentHashCode() - result = 31 * result + macKey.contentHashCode() - return result + class WithoutNonce internal constructor( + override val algorithm: SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Without>, NonceTrait.Without, KeyType.WithDedicatedMacKey>, + encryptionKey: ByteArray, + dedicatedMacKey: ByteArray, + override val additionalProperties: MutableMap<String, String> = mutableMapOf<String, String>() + ) : WithDedicatedMac<NonceTrait.Without>, + SymmetricKey.WithoutNonce<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Without>, KeyType.WithDedicatedMacKey> { + /** + * The actual encryption key bytes + * + * This will fail for hardware-backed keys! + */ + @SecretExposure + override val encryptionKey: KmmResult<ByteArray> = KmmResult.success(encryptionKey) + + /** + * The actual dedicated MAC key bytes + * + * This will fail for hardware-backed keys! + */ + @SecretExposure + override val macKey: KmmResult<ByteArray> = KmmResult.success(dedicatedMacKey) + + @OptIn(SecretExposure::class) + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is WithDedicatedMac<*>) return false + + if (algorithm != other.algorithm) return false + if (!encryptionKey.getOrNull().contentEquals(other.encryptionKey.getOrNull())) return false + if (!macKey.getOrNull().contentEquals(other.macKey.getOrNull())) return false + if (additionalProperties != other.additionalProperties) return false + + return true + } + + @OptIn(SecretExposure::class) + override fun hashCode(): Int { + var result = algorithm.hashCode() + result = 31 * result + encryptionKey.getOrNull().contentHashCode() + result = 31 * result + macKey.getOrNull().contentHashCode() + return result + } } } } @@ -206,5 +298,24 @@ fun <A : AuthCapability<K>, K : KeyType> SymmetricKey<A, *, K>.requiresNonce(): /** * The actual encryption key bytes + * + * This will fail for hardware-backed keys! */ +@SecretExposure val <A : AuthCapability<out KeyType.Integrated>, I : NonceTrait> SymmetricKey<A, I, out KeyType.Integrated>.secretKey get() = (this as SymmetricKey.Integrated).secretKey + +/** + * The encryption key bytes, if present. + * + * This will fail for hardware-backed keys! + */ +@SecretExposure +val <A : AuthCapability.Authenticated.WithDedicatedMac<*, I>, I : NonceTrait> SymmetricKey<A, I, out KeyType.WithDedicatedMacKey>.encryptionKey get() = (this as SymmetricKey.WithDedicatedMac).encryptionKey + +/** + * The dedicated MAC key bytes, if present. + * + * This will fail for hardware-backed keys! + */ +@SecretExposure +val <A : AuthCapability.Authenticated.WithDedicatedMac<*, I>, I : NonceTrait> SymmetricKey<A, I, out KeyType.WithDedicatedMacKey>.macKey get() = (this as SymmetricKey.WithDedicatedMac).macKey diff --git a/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/os/AndroidKeyStoreProvider.kt b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/os/AndroidKeyStoreProvider.kt index 0e1631c8..213554bc 100644 --- a/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/os/AndroidKeyStoreProvider.kt +++ b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/os/AndroidKeyStoreProvider.kt @@ -19,7 +19,7 @@ import at.asitplus.signum.indispensable.asn1.Asn1StructuralException import at.asitplus.signum.indispensable.pki.X509Certificate import at.asitplus.signum.indispensable.pki.leaf import at.asitplus.signum.supreme.AppLifecycleMonitor -import at.asitplus.signum.supreme.SecretExposure +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.supreme.SignatureResult import at.asitplus.signum.supreme.UnlockFailed import at.asitplus.signum.supreme.UnsupportedCryptoException diff --git a/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeysImpl.kt b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeysImpl.kt index 2707500b..f422622b 100644 --- a/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeysImpl.kt +++ b/supreme/src/androidMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeysImpl.kt @@ -14,7 +14,7 @@ import at.asitplus.signum.indispensable.parseFromJca import at.asitplus.signum.indispensable.toCryptoPrivateKey import at.asitplus.signum.indispensable.toCryptoPublicKey import at.asitplus.signum.indispensable.toJcaPublicKey -import at.asitplus.signum.supreme.SecretExposure +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.supreme.signCatching import com.ionspin.kotlin.bignum.integer.base63.toJavaBigInteger import java.security.KeyPair diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/Throwables.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/Throwables.kt index 9ca25016..fdb4e833 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/Throwables.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/Throwables.kt @@ -1,10 +1,5 @@ package at.asitplus.signum.supreme - -@RequiresOptIn(message = "Access to secret and private key material requires explicit opt-in. Specify @OptIn(SecretExposure::class). Make sure that you actually want to externalise a secret. Check yourself, before you really, really, wreck yourself!") -/** This guards a secret. Do not expose it lightly! */ -annotation class SecretExposure - sealed class CryptoException(message: String? = null, cause: Throwable? = null) : Throwable(message, cause) open class CryptoOperationFailed(message: String) : CryptoException(message) open class UnsupportedCryptoException(message: String? = null, cause: Throwable? = null) : CryptoException(message, cause) diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeys.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeys.kt index d4985170..2a88b71c 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeys.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeys.kt @@ -8,7 +8,7 @@ import at.asitplus.signum.indispensable.Digest import at.asitplus.signum.indispensable.RSAPadding import at.asitplus.signum.indispensable.SignatureAlgorithm import at.asitplus.signum.indispensable.nativeDigest -import at.asitplus.signum.supreme.SecretExposure +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.supreme.dsl.DSL import at.asitplus.signum.supreme.dsl.DSLConfigureFn import at.asitplus.signum.supreme.os.SignerConfiguration diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/Signer.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/Signer.kt index d12ce4e4..64c71848 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/Signer.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/sign/Signer.kt @@ -5,7 +5,7 @@ import at.asitplus.catching import at.asitplus.signum.indispensable.* import at.asitplus.signum.indispensable.RSAPadding import at.asitplus.signum.indispensable.SignatureAlgorithm -import at.asitplus.signum.supreme.SecretExposure +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.supreme.SignatureResult import at.asitplus.signum.supreme.agree.UsableECDHPrivateValue import at.asitplus.signum.supreme.dsl.DSL diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt index 55bf083c..e0113a55 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/decrypt.kt @@ -3,6 +3,7 @@ package at.asitplus.signum.supreme.symmetric import at.asitplus.KmmResult import at.asitplus.catching import at.asitplus.signum.ImplementationError +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode import at.asitplus.signum.indispensable.symmetric.* import at.asitplus.signum.indispensable.symmetric.AuthCapability.Authenticated @@ -22,18 +23,21 @@ suspend fun SealedBox<*, *, *>.decrypt(key: SymmetricKey<*, *, *>): KmmResult<By @Suppress("UNCHECKED_CAST") when (algorithm.authCapability) { is Authenticated.Integrated -> (this as SealedBox<Authenticated.Integrated, *, KeyType.Integrated>).decryptInternal( - (key as SymmetricKey.Integrated).secretKey, byteArrayOf() + @OptIn(SecretExposure::class) (key as SymmetricKey.Integrated).secretKey.getOrThrow(), + byteArrayOf() ) is Authenticated.WithDedicatedMac<*, *> -> { key as SymmetricKey.WithDedicatedMac (this as SealedBox<Authenticated.WithDedicatedMac<*, *>, *, KeyType.WithDedicatedMacKey>).decryptInternal( - key.encryptionKey, key.macKey, byteArrayOf() + @OptIn(SecretExposure::class) key.encryptionKey.getOrThrow(), + @OptIn(SecretExposure::class) key.macKey.getOrThrow(), + byteArrayOf() ) } is AuthCapability.Unauthenticated -> (this as SealedBox<AuthCapability.Unauthenticated, *, KeyType.Integrated>).decryptInternal( - (key as SymmetricKey.Integrated).secretKey + @OptIn(SecretExposure::class) (key as SymmetricKey.Integrated).secretKey.getOrThrow() ) } } @@ -54,8 +58,10 @@ suspend fun <I : NonceTrait, M : MessageAuthenticationCode> SealedBox<AuthCapabi ) = catching { @Suppress("UNCHECKED_CAST") (this as SealedBox<Authenticated.WithDedicatedMac<*, *>, *, KeyType.WithDedicatedMacKey>).decryptInternal( - key.encryptionKey, - key.macKey, + @OptIn(SecretExposure::class) + key.encryptionKey.getOrThrow(), + @OptIn(SecretExposure::class) + key.macKey.getOrThrow(), authenticatedData ) } @@ -68,7 +74,7 @@ suspend fun <I : NonceTrait, M : MessageAuthenticationCode> SealedBox<AuthCapabi * [key] and [SealedBox].** */ @JvmName("decryptAuthenticatedGeneric") -suspend fun <A : AuthCapability.Authenticated<out K>,I: NonceTrait, K : KeyType> SealedBox<A, I, out K>.decrypt( +suspend fun <A : AuthCapability.Authenticated<out K>, I : NonceTrait, K : KeyType> SealedBox<A, I, out K>.decrypt( key: SymmetricKey<A, I, out K>, authenticatedData: ByteArray = byteArrayOf() ): KmmResult<ByteArray> = catching { @@ -76,13 +82,16 @@ suspend fun <A : AuthCapability.Authenticated<out K>,I: NonceTrait, K : KeyType> @Suppress("UNCHECKED_CAST") when (algorithm.authCapability) { is Authenticated.Integrated -> (this as SealedBox<Authenticated.Integrated, *, KeyType.Integrated>).decryptInternal( - (key as SymmetricKey.Integrated).secretKey, authenticatedData + @OptIn(SecretExposure::class) (key as SymmetricKey.Integrated).secretKey.getOrThrow(), + authenticatedData ) is Authenticated.WithDedicatedMac<*, *> -> { key as SymmetricKey.WithDedicatedMac (this as SealedBox<Authenticated.WithDedicatedMac<*, *>, *, KeyType.WithDedicatedMacKey>).decryptInternal( - key.encryptionKey, key.macKey, authenticatedData + @OptIn(SecretExposure::class) key.encryptionKey.getOrThrow(), + @OptIn(SecretExposure::class) key.macKey.getOrThrow(), + authenticatedData ) } //compiler knows its exhaustive, but IDEA complains @@ -104,7 +113,7 @@ suspend fun <I : NonceTrait> SealedBox<AuthCapability.Unauthenticated, I, KeyTyp require(algorithm == key.algorithm) { "Algorithm mismatch! expected: $algorithm, actual: ${key.algorithm}" } @Suppress("UNCHECKED_CAST") (this as SealedBox<AuthCapability.Unauthenticated, *, KeyType.Integrated>).decryptInternal( - (key as SymmetricKey.Integrated).secretKey + @OptIn(SecretExposure::class) (key as SymmetricKey.Integrated).secretKey.getOrThrow() ) } diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/discouraged/encrypt.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/discouraged/encrypt.kt index 714b45d2..ab660fce 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/discouraged/encrypt.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/discouraged/encrypt.kt @@ -3,6 +3,7 @@ package at.asitplus.signum.supreme.symmetric.discouraged import at.asitplus.KmmResult import at.asitplus.catching import at.asitplus.signum.HazardousMaterials +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.indispensable.symmetric.* import at.asitplus.signum.indispensable.symmetric.SymmetricKey.WithDedicatedMac import at.asitplus.signum.supreme.symmetric.Encryptor @@ -25,13 +26,13 @@ suspend fun <K : KeyType, A : AuthCapability.Authenticated<out K>> KeyWithNonceA data: ByteArray, authenticatedData: ByteArray? = null ): KmmResult<SealedBox<A, NonceTrait.Required, out K>> = catching { - Encryptor( + @OptIn(SecretExposure::class) Encryptor( second.algorithm, - if (second.hasDedicatedMacKey()) (second as WithDedicatedMac<NonceTrait.Required>).encryptionKey else (second as SymmetricKey.Integrated<out A, NonceTrait.Required>).secretKey, - if (second.hasDedicatedMacKey()) (second as WithDedicatedMac<NonceTrait.Required>).macKey else (second as SymmetricKey.Integrated<out A, NonceTrait.Required>).secretKey, + if (second.hasDedicatedMacKey()) (second as WithDedicatedMac<NonceTrait.Required>).encryptionKey.getOrThrow() else (second as SymmetricKey.Integrated<out A, NonceTrait.Required>).secretKey.getOrThrow(), + if (second.hasDedicatedMacKey()) (second as WithDedicatedMac<NonceTrait.Required>).macKey.getOrThrow() else (second as SymmetricKey.Integrated<out A, NonceTrait.Required>).secretKey.getOrThrow(), first, authenticatedData, - ).encrypt(data) as SealedBox<A, NonceTrait.Required,K> + ).encrypt(data) as SealedBox<A, NonceTrait.Required, K> } /** @@ -48,10 +49,10 @@ suspend fun <K : KeyType, A : AuthCapability.Authenticated<out K>> KeyWithNonceA suspend fun <K : KeyType, A : AuthCapability<out K>> KeyWithNonce<A, out K>.encrypt( data: ByteArray ): KmmResult<SealedBox.WithNonce<A, out K>> = catching { - Encryptor( + @OptIn(SecretExposure::class) Encryptor( first.algorithm, - if (first.hasDedicatedMacKey()) (first as WithDedicatedMac<NonceTrait.Required>).encryptionKey else (first as SymmetricKey.Integrated<out A, NonceTrait.Required>).secretKey, - if (first.hasDedicatedMacKey()) (first as WithDedicatedMac<NonceTrait.Required>).macKey else null, + if (first.hasDedicatedMacKey()) (first as WithDedicatedMac<NonceTrait.Required>).encryptionKey.getOrThrow() else (first as SymmetricKey.Integrated<out A, NonceTrait.Required>).secretKey.getOrThrow(), + if (first.hasDedicatedMacKey()) (first as WithDedicatedMac<NonceTrait.Required>).macKey.getOrThrow() else null, second, null, ).encrypt(data) as SealedBox.WithNonce<A, out K> diff --git a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/encrypt.kt b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/encrypt.kt index cf74c028..1a047962 100644 --- a/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/encrypt.kt +++ b/supreme/src/commonMain/kotlin/at/asitplus/signum/supreme/symmetric/encrypt.kt @@ -2,6 +2,7 @@ package at.asitplus.signum.supreme.symmetric import at.asitplus.KmmResult import at.asitplus.catching +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.indispensable.symmetric.* import kotlin.jvm.JvmName @@ -15,10 +16,10 @@ import kotlin.jvm.JvmName suspend fun <K : KeyType, A : AuthCapability<out K>, I : NonceTrait> SymmetricKey<A, I, out K>.encrypt( data: ByteArray ): KmmResult<SealedBox<A, I, out K>> = catching { - Encryptor( + @OptIn(SecretExposure::class) Encryptor( algorithm, - if (this.hasDedicatedMacKey()) encryptionKey else secretKey, - if (this.hasDedicatedMacKey()) macKey else null, + if (this.hasDedicatedMacKey()) encryptionKey.getOrThrow() else secretKey.getOrThrow(), + if (this.hasDedicatedMacKey()) macKey.getOrThrow() else null, aad = null ).encrypt(data) } @@ -40,10 +41,10 @@ suspend fun <K : KeyType, A : AuthCapability.Authenticated<out K>, I : NonceTrai data: ByteArray, authenticatedData: ByteArray? = null ): KmmResult<SealedBox<A, I, out K>> = catching { - Encryptor( + @OptIn(SecretExposure::class) Encryptor( algorithm, - if (this.hasDedicatedMacKey()) encryptionKey else secretKey, - if (this.hasDedicatedMacKey()) macKey else null, + if (this.hasDedicatedMacKey()) encryptionKey.getOrThrow() else secretKey.getOrThrow(), + if (this.hasDedicatedMacKey()) macKey.getOrThrow() else null, aad = authenticatedData, ).encrypt(data) } diff --git a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/EphemeralSignerCommonTests.kt b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/EphemeralSignerCommonTests.kt index d291e0fe..6c18f0f4 100644 --- a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/EphemeralSignerCommonTests.kt +++ b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/EphemeralSignerCommonTests.kt @@ -5,7 +5,7 @@ import at.asitplus.signum.indispensable.asn1.* import at.asitplus.signum.indispensable.pki.* import at.asitplus.signum.indispensable.RSAPadding import at.asitplus.signum.indispensable.SignatureAlgorithm -import at.asitplus.signum.supreme.SecretExposure +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.supreme.os.PlatformSigningKeyConfigurationBase import at.asitplus.signum.supreme.os.SignerConfiguration import at.asitplus.signum.supreme.sign diff --git a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/PrivateKeyCommonTests.kt b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/PrivateKeyCommonTests.kt index 7b5c8e06..44976daa 100644 --- a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/PrivateKeyCommonTests.kt +++ b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/sign/PrivateKeyCommonTests.kt @@ -2,7 +2,7 @@ package at.asitplus.signum.supreme.sign import at.asitplus.signum.indispensable.CryptoPrivateKey import at.asitplus.signum.indispensable.SignatureAlgorithm -import at.asitplus.signum.supreme.SecretExposure +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.supreme.isSuccess import at.asitplus.signum.supreme.signature import io.kotest.core.spec.style.FreeSpec diff --git a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/symmetric/00SymmetricTest.kt b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/symmetric/00SymmetricTest.kt index 2ed268d4..76cd981a 100644 --- a/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/symmetric/00SymmetricTest.kt +++ b/supreme/src/commonTest/kotlin/at/asitplus/signum/supreme/symmetric/00SymmetricTest.kt @@ -1,4 +1,7 @@ +@file:OptIn(SecretExposure::class) + import at.asitplus.signum.HazardousMaterials +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.indispensable.asn1.encoding.encodeTo4Bytes import at.asitplus.signum.indispensable.mac.HMAC import at.asitplus.signum.indispensable.mac.MessageAuthenticationCode @@ -43,7 +46,7 @@ class `00SymmetricTest` : FreeSpec({ val ciphertext = encrypted.encryptedData val authTag = encrypted.authTag val externalAAD = authenticatedData - val keyBytes = secretKey.secretKey + val keyBytes = secretKey.secretKey.getOrThrow() val preSharedKey = algo.keyFrom(keyBytes).getOrThrow() @@ -91,7 +94,7 @@ class `00SymmetricTest` : FreeSpec({ //if we just know algorithm and key bytes, we can also construct a symmetric key reconstructed.decrypt( - algorithm.keyFrom(key.encryptionKey, key.macKey).getOrThrow(/*handle error*/), + algorithm.keyFrom(key.encryptionKey.getOrThrow(), key.macKey.getOrThrow()).getOrThrow(/*handle error*/), aad ).getOrThrow(/*handle error*/) shouldBe payload //greatest success! } @@ -199,8 +202,8 @@ class `00SymmetricTest` : FreeSpec({ } shouldNot succeed val key = when (alg.hasDedicatedMac()) { - true -> alg.keyFrom(alg.randomKey().encryptionKey, alg.randomKey().encryptionKey) - false -> alg.keyFrom(alg.randomKey().secretKey) + true -> alg.keyFrom(alg.randomKey().encryptionKey.getOrThrow(), alg.randomKey().encryptionKey.getOrThrow()) + false -> alg.keyFrom(alg.randomKey().secretKey.getOrThrow()) }.getOrThrow() @@ -283,14 +286,14 @@ class `00SymmetricTest` : FreeSpec({ ).getOrThrow() val wrongWrongDecrypted = wrongCiphertext.decrypt(it.randomKey()) - withClue("KEY: ${key.secretKey.toHexString()}, wrongCiphertext: ${wrongCiphertext.encryptedData.toHexString()}, ciphertext: ${ciphertext.encryptedData.toHexString()}, iv: ${wrongCiphertext.nonce?.toHexString()}") { + withClue("KEY: ${key.secretKey.getOrThrow().toHexString()}, wrongCiphertext: ${wrongCiphertext.encryptedData.toHexString()}, ciphertext: ${ciphertext.encryptedData.toHexString()}, iv: ${wrongCiphertext.nonce?.toHexString()}") { //we're not authenticated, so from time to time, this succeeds //wrongWrongDecrypted shouldNot succeed //instead, we test differently: wrongWrongDecrypted.onSuccess { value -> value shouldNotBe plaintext } } val wrongRightDecrypted = wrongCiphertext.decrypt(key) - withClue("KEY: ${key.secretKey.toHexString()}, wrongCiphertext: ${wrongCiphertext.encryptedData.toHexString()}, ciphertext: ${ciphertext.encryptedData.toHexString()}, iv: ${wrongCiphertext.nonce?.toHexString()}") { + withClue("KEY: ${key.secretKey.getOrThrow().toHexString()}, wrongCiphertext: ${wrongCiphertext.encryptedData.toHexString()}, ciphertext: ${ciphertext.encryptedData.toHexString()}, iv: ${wrongCiphertext.nonce?.toHexString()}") { //we're not authenticated, so from time to time, this succeeds //wrongRightDecrypted shouldNot succeed //instead, we test differently: @@ -492,7 +495,7 @@ class `00SymmetricTest` : FreeSpec({ Random.Default.nextBytes(21257), ) { plaintext -> - val secretKey = it.randomKey().encryptionKey + val secretKey = it.randomKey().encryptionKey.getOrThrow() withData( nameFn = { "MAC KEY $it" }, @@ -521,8 +524,8 @@ class `00SymmetricTest` : FreeSpec({ val manilaKey = SymmetricKey.WithDedicatedMac.RequiringNonce( manilaAlg, - key.encryptionKey, - key.macKey + key.encryptionKey.getOrThrow(), + key.macKey.getOrThrow() ) if (iv != null) manilaKey.andPredefinedNonce(iv).getOrThrow().encrypt(plaintext, aad) .getOrThrow() shouldNotBe ciphertext @@ -582,8 +585,8 @@ class `00SymmetricTest` : FreeSpec({ ).getOrThrow().decrypt( SymmetricKey.WithDedicatedMac.RequiringNonce( ciphertext.algorithm as SymmetricEncryptionAlgorithm<AuthCapability.Authenticated.WithDedicatedMac<*, NonceTrait.Required>, NonceTrait.Required, KeyType.WithDedicatedMacKey>, - key.encryptionKey, - dedicatedMacKey = key.macKey.asList().shuffled().toByteArray() + key.encryptionKey.getOrThrow(), + dedicatedMacKey = key.macKey.getOrThrow().asList().shuffled().toByteArray() ), aad ?: byteArrayOf() ) shouldNot succeed @@ -606,8 +609,8 @@ class `00SymmetricTest` : FreeSpec({ }.let { SymmetricKey.WithDedicatedMac.RequiringNonce( it, - key.encryptionKey, - key.macKey + key.encryptionKey.getOrThrow(), + key.macKey.getOrThrow() ) }, aad ?: byteArrayOf()) shouldNot succeed } @@ -737,27 +740,27 @@ class `00SymmetricTest` : FreeSpec({ when (alg.hasDedicatedMac()) { true -> { key shouldBe alg.keyFrom( - (key as SymmetricKey.WithDedicatedMac).encryptionKey, - (key as SymmetricKey.WithDedicatedMac<*>).macKey + (key as SymmetricKey.WithDedicatedMac).encryptionKey.getOrThrow(), + (key as SymmetricKey.WithDedicatedMac<*>).macKey.getOrThrow() ).getOrThrow() key shouldNotBe alg.keyFrom( - key.encryptionKey, - (key as SymmetricKey.WithDedicatedMac<*>).macKey.asList().shuffled() + key.encryptionKey.getOrThrow(), + (key as SymmetricKey.WithDedicatedMac<*>).macKey.getOrThrow().asList().shuffled() .toByteArray() ).getOrThrow() key shouldNotBe alg.keyFrom( - key.encryptionKey.asList().shuffled().toByteArray(), - (key as SymmetricKey.WithDedicatedMac<*>).macKey + key.encryptionKey.getOrThrow().asList().shuffled().toByteArray(), + (key as SymmetricKey.WithDedicatedMac<*>).macKey.getOrThrow() ).getOrThrow() key shouldNotBe alg.keyFrom( - key.encryptionKey.asList().shuffled().toByteArray(), - (key as SymmetricKey.WithDedicatedMac<*>).macKey.asList().shuffled() + key.encryptionKey.getOrThrow().asList().shuffled().toByteArray(), + (key as SymmetricKey.WithDedicatedMac<*>).macKey.getOrThrow().asList().shuffled() .toByteArray() ).getOrThrow() } - false -> key shouldBe alg.keyFrom((key as SymmetricKey.Integrated).secretKey).getOrThrow() + false -> key shouldBe alg.keyFrom((key as SymmetricKey.Integrated).secretKey.getOrThrow()).getOrThrow() } } @@ -810,14 +813,14 @@ class `00SymmetricTest` : FreeSpec({ if (alg.hasDedicatedMac() && wrongAlg.hasDedicatedMac()) { alg.randomKey().let { key -> wrongAlg.keyFrom( - key.encryptionKey, - key.macKey /*size will not match, but it will get us a valid key*/ + key.encryptionKey.getOrThrow(), + key.macKey.getOrThrow() /*size will not match, but it will get us a valid key*/ ).getOrThrow() shouldNotBe key } } else if (!wrongAlg.hasDedicatedMac() && !alg.hasDedicatedMac()) { alg.randomKey().let { key -> wrongAlg.keyFrom( - key.secretKey, + key.secretKey.getOrThrow(), ).getOrThrow() shouldNotBe key } } @@ -933,7 +936,7 @@ class `00SymmetricTest` : FreeSpec({ true -> { alg.keyFrom( Random.nextBytes(sz), - alg.randomKey().encryptionKey /*mac key should not trigger, as it is unconstrained*/ + alg.randomKey().encryptionKey.getOrThrow() /*mac key should not trigger, as it is unconstrained*/ ) } diff --git a/supreme/src/jvmMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeysImpl.kt b/supreme/src/jvmMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeysImpl.kt index 84cfb311..dbb2a8bc 100644 --- a/supreme/src/jvmMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeysImpl.kt +++ b/supreme/src/jvmMain/kotlin/at/asitplus/signum/supreme/sign/EphemeralKeysImpl.kt @@ -3,7 +3,7 @@ package at.asitplus.signum.supreme.sign import at.asitplus.KmmResult import at.asitplus.catching import at.asitplus.signum.indispensable.* -import at.asitplus.signum.supreme.SecretExposure +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.supreme.signCatching import com.ionspin.kotlin.bignum.integer.base63.toJavaBigInteger import java.security.KeyPair diff --git a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/symmetric/Test.kt b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/symmetric/Test.kt index db68cf2c..f8d90fb1 100644 --- a/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/symmetric/Test.kt +++ b/supreme/src/jvmTest/kotlin/at/asitplus/signum/supreme/symmetric/Test.kt @@ -1,5 +1,8 @@ +@file:OptIn(SecretExposure::class) + import at.asitplus.catching import at.asitplus.signum.HazardousMaterials +import at.asitplus.signum.indispensable.SecretExposure import at.asitplus.signum.indispensable.symmetric.* import at.asitplus.signum.supreme.succeed import at.asitplus.signum.supreme.symmetric.decrypt @@ -71,7 +74,7 @@ class JvmSymmetricTest : FreeSpec({ own.isAuthenticated() shouldBe true jcaCipher.init( Cipher.ENCRYPT_MODE, - SecretKeySpec(secretKey.secretKey, "AES"), + SecretKeySpec(secretKey.secretKey.getOrThrow(), "AES"), GCMParameterSpec( alg.authCapability.tagLength.bits.toInt(), own.nonce/*use our own auto-generated IV*/ @@ -85,7 +88,7 @@ class JvmSymmetricTest : FreeSpec({ jcaCipher.init( Cipher.DECRYPT_MODE, - SecretKeySpec(secretKey.secretKey, "AES"), + SecretKeySpec(secretKey.secretKey.getOrThrow(), "AES"), GCMParameterSpec( alg.authCapability.tagLength.bits.toInt(), own.nonce/*use our own auto-generated IV*/ @@ -116,7 +119,7 @@ class JvmSymmetricTest : FreeSpec({ val own = secretKey.encrypt(data).getOrThrow() jcaCipher.init( Cipher.ENCRYPT_MODE, - SecretKeySpec(secretKey.secretKey, "AES"), + SecretKeySpec(secretKey.secretKey.getOrThrow(), "AES"), IvParameterSpec(own.nonce)/*use our own auto-generated IV, if null iv was provided*/ ) val encrypted = jcaCipher.doFinal(data) @@ -125,7 +128,7 @@ class JvmSymmetricTest : FreeSpec({ jcaCipher.init( Cipher.DECRYPT_MODE, - SecretKeySpec(secretKey.secretKey, "AES"), + SecretKeySpec(secretKey.secretKey.getOrThrow(), "AES"), IvParameterSpec(own.nonce)/*use our own auto-generated IV, if null iv was provided*/ ) own.decrypt(secretKey).getOrThrow() shouldBe jcaCipher.doFinal(encrypted) @@ -185,7 +188,7 @@ class JvmSymmetricTest : FreeSpec({ val own = secretKey.encrypt(data).getOrThrow() jcaCipher.init( Cipher.ENCRYPT_MODE, - SecretKeySpec(secretKey.secretKey, "AES"), + SecretKeySpec(secretKey.secretKey.getOrThrow(), "AES"), ) val encrypted = jcaCipher.doFinal(data) @@ -193,7 +196,7 @@ class JvmSymmetricTest : FreeSpec({ jcaCipher.init( Cipher.DECRYPT_MODE, - SecretKeySpec(secretKey.secretKey, "AES"), + SecretKeySpec(secretKey.secretKey.getOrThrow(), "AES"), ) own.decrypt(secretKey).getOrThrow() shouldBe jcaCipher.doFinal(encrypted) @@ -217,7 +220,7 @@ class JvmSymmetricTest : FreeSpec({ jcaCipher.init( Cipher.ENCRYPT_MODE, - SecretKeySpec(secretKey.secretKey, "AES"), + SecretKeySpec(secretKey.secretKey.getOrThrow(), "AES"), ) val jcaTrail = catching { jcaCipher.doFinal(data) @@ -233,7 +236,7 @@ class JvmSymmetricTest : FreeSpec({ jcaCipher.init( Cipher.DECRYPT_MODE, - SecretKeySpec(secretKey.secretKey, "AES"), + SecretKeySpec(secretKey.secretKey.getOrThrow(), "AES"), ) own.decrypt(secretKey).getOrThrow() shouldBe jcaCipher.doFinal(encrypted) @@ -277,7 +280,7 @@ class JvmSymmetricTest : FreeSpec({ jcaCipher.init( Cipher.ENCRYPT_MODE, - SecretKeySpec(secretKey.secretKey, "ChaCha"), + SecretKeySpec(secretKey.secretKey.getOrThrow(), "ChaCha"), IvParameterSpec(box.nonce) /*need to do this, otherwise we get a random nonce*/ )
ee76fbf
to
9c1266a
Compare
ChaCha20-Poly1305
AES-GCM
AES-CBC-HMAC
AES-CBC (unauthenticated)
AES-ECB