diff --git a/build-logic/src/main/kotlin/ckbuild/Projects.kt b/build-logic/src/main/kotlin/ckbuild/Projects.kt index 90e19589..09ad67bf 100644 --- a/build-logic/src/main/kotlin/ckbuild/Projects.kt +++ b/build-logic/src/main/kotlin/ckbuild/Projects.kt @@ -22,11 +22,13 @@ object Projects { "cryptography-serialization-asn1-modules" to setOf(Tag.PUBLISHED), "cryptography-core" to setOf(Tag.PUBLISHED), + "cryptography-storage" to setOf(Tag.PUBLISHED), "cryptography-provider-base" to setOf(Tag.PUBLISHED), "cryptography-provider-jdk" to setOf(Tag.PUBLISHED), "cryptography-provider-jdk-bc" to setOf(Tag.PUBLISHED), "cryptography-provider-apple" to setOf(Tag.PUBLISHED), + "cryptography-provider-apple-keychain" to setOf(Tag.PUBLISHED), "cryptography-provider-cryptokit" to setOf(Tag.PUBLISHED), "cryptography-provider-webcrypto" to setOf(Tag.PUBLISHED), "cryptography-provider-openssl3-api" to setOf(Tag.PUBLISHED), diff --git a/cryptography-providers/apple-keychain/api/cryptography-provider-apple-keychain.klib.api b/cryptography-providers/apple-keychain/api/cryptography-provider-apple-keychain.klib.api new file mode 100644 index 00000000..b1c16343 --- /dev/null +++ b/cryptography-providers/apple-keychain/api/cryptography-provider-apple-keychain.klib.api @@ -0,0 +1,17 @@ +// Klib ABI Dump +// Targets: [iosArm64, iosSimulatorArm64, iosX64, macosArm64, macosX64, tvosArm64, tvosSimulatorArm64, tvosX64, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +final object dev.whyoleg.cryptography.providers.apple.keychain/AppleKeyStore : dev.whyoleg.cryptography.storage/KeyStore { // dev.whyoleg.cryptography.providers.apple.keychain/AppleKeyStore|null[0] + final fun aesCbc(dev.whyoleg.cryptography/BinarySize): dev.whyoleg.cryptography.storage/SymmetricStore // dev.whyoleg.cryptography.providers.apple.keychain/AppleKeyStore.aesCbc|aesCbc(dev.whyoleg.cryptography.BinarySize){}[0] + final fun aesCtr(dev.whyoleg.cryptography/BinarySize): dev.whyoleg.cryptography.storage/SymmetricStore // dev.whyoleg.cryptography.providers.apple.keychain/AppleKeyStore.aesCtr|aesCtr(dev.whyoleg.cryptography.BinarySize){}[0] + final fun aesGcm(dev.whyoleg.cryptography/BinarySize): dev.whyoleg.cryptography.storage/SymmetricStore // dev.whyoleg.cryptography.providers.apple.keychain/AppleKeyStore.aesGcm|aesGcm(dev.whyoleg.cryptography.BinarySize){}[0] + final fun ecdsa(dev.whyoleg.cryptography.algorithms/EC.Curve): dev.whyoleg.cryptography.storage/AsymmetricStore // dev.whyoleg.cryptography.providers.apple.keychain/AppleKeyStore.ecdsa|ecdsa(dev.whyoleg.cryptography.algorithms.EC.Curve){}[0] + final fun rsaOaep(dev.whyoleg.cryptography/BinarySize, dev.whyoleg.cryptography/CryptographyAlgorithmId): dev.whyoleg.cryptography.storage/AsymmetricStore // dev.whyoleg.cryptography.providers.apple.keychain/AppleKeyStore.rsaOaep|rsaOaep(dev.whyoleg.cryptography.BinarySize;dev.whyoleg.cryptography.CryptographyAlgorithmId){}[0] + final fun rsaPkcs1(dev.whyoleg.cryptography/BinarySize, dev.whyoleg.cryptography/CryptographyAlgorithmId): dev.whyoleg.cryptography.storage/AsymmetricStore // dev.whyoleg.cryptography.providers.apple.keychain/AppleKeyStore.rsaPkcs1|rsaPkcs1(dev.whyoleg.cryptography.BinarySize;dev.whyoleg.cryptography.CryptographyAlgorithmId){}[0] + final fun rsaPss(dev.whyoleg.cryptography/BinarySize, dev.whyoleg.cryptography/CryptographyAlgorithmId): dev.whyoleg.cryptography.storage/AsymmetricStore // dev.whyoleg.cryptography.providers.apple.keychain/AppleKeyStore.rsaPss|rsaPss(dev.whyoleg.cryptography.BinarySize;dev.whyoleg.cryptography.CryptographyAlgorithmId){}[0] +} diff --git a/cryptography-providers/apple-keychain/build.gradle.kts b/cryptography-providers/apple-keychain/build.gradle.kts new file mode 100644 index 00000000..6a136d96 --- /dev/null +++ b/cryptography-providers/apple-keychain/build.gradle.kts @@ -0,0 +1,31 @@ +import ckbuild.* +import org.jetbrains.kotlin.gradle.* + +plugins { + id("ckbuild.multiplatform-library") +} + +description = "cryptography-kotlin Apple Keychain-backed KeyStore (experimental)" + +@OptIn(ExperimentalKotlinGradlePluginApi::class) +kotlin { + appleTargets() + + compilerOptions { + optIn.addAll( + OptIns.DelicateCryptographyApi, + OptIns.CryptographyProviderApi, + OptIns.ExperimentalForeignApi, + ) + } + + sourceSets.commonMain.dependencies { + api(projects.cryptographyCore) + api(projects.cryptographyStorage) + implementation(projects.cryptographyProviderBase) + } + + sourceSets.commonTest.dependencies { + implementation(kotlin("test")) + } +} diff --git a/cryptography-providers/apple-keychain/src/commonMain/kotlin/dev/whyoleg/cryptography/providers/apple/keychain/AppleKeyStore.kt b/cryptography-providers/apple-keychain/src/commonMain/kotlin/dev/whyoleg/cryptography/providers/apple/keychain/AppleKeyStore.kt new file mode 100644 index 00000000..3a6d6cc3 --- /dev/null +++ b/cryptography-providers/apple-keychain/src/commonMain/kotlin/dev/whyoleg/cryptography/providers/apple/keychain/AppleKeyStore.kt @@ -0,0 +1,331 @@ +@file:OptIn( + kotlinx.cinterop.UnsafeNumber::class, + kotlinx.cinterop.ExperimentalForeignApi::class, + dev.whyoleg.cryptography.storage.ExperimentalKeyStorageApi::class, +) + +package dev.whyoleg.cryptography.providers.apple.keychain + +import dev.whyoleg.cryptography.* +import dev.whyoleg.cryptography.BinarySize.Companion.bits +import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.operations.* +import dev.whyoleg.cryptography.providers.base.* +import dev.whyoleg.cryptography.storage.* +import kotlinx.cinterop.* +import platform.CoreFoundation.* +import platform.Foundation.* +import platform.Security.* + +@OptIn(ExperimentalForeignApi::class) +@ExperimentalKeyStorageApi +public object AppleKeyStore : KeyStore { + override fun ecdsa(curve: EC.Curve): AsymmetricStore { + // MVP: support P-256 only + require(curve == EC.Curve.P256) { "Unsupported curve: ${curve.name}" } + return AppleEcdsaStore(curve) + } + + override fun rsaPss( + keySize: BinarySize, + digest: CryptographyAlgorithmId + ): AsymmetricStore { + throw UnsupportedOperationException("RSA-PSS not implemented in MVP") + } + + override fun rsaPkcs1( + keySize: BinarySize, + digest: CryptographyAlgorithmId + ): AsymmetricStore { + throw UnsupportedOperationException("RSA-PKCS1 not implemented in MVP") + } + + override fun rsaOaep( + keySize: BinarySize, + digest: CryptographyAlgorithmId + ): AsymmetricStore { + throw UnsupportedOperationException("RSA-OAEP not implemented in MVP") + } + + override fun aesGcm(size: BinarySize): SymmetricStore { + throw UnsupportedOperationException("AES-GCM not implemented in MVP") + } + + override fun aesCbc(size: BinarySize): SymmetricStore { + throw UnsupportedOperationException("AES-CBC not implemented in MVP") + } + + override fun aesCtr(size: BinarySize): SymmetricStore { + throw UnsupportedOperationException("AES-CTR not implemented in MVP") + } +} + +@OptIn(ExperimentalForeignApi::class) +@ExperimentalKeyStorageApi +private class AppleEcdsaStore(private val curve: EC.Curve) : AsymmetricStore { + override fun generate(label: ByteArray, access: AccessPolicy): Handle = memScoped { + val accessCtrl = createAccessControl(access) + val attrs = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, null, null) + // key attributes + CFDictionarySetValue(attrs, kSecAttrKeyType, kSecAttrKeyTypeECSECPrimeRandom) + CFDictionarySetValue(attrs, kSecAttrKeyClass, kSecAttrKeyClassPrivate) + // P-256 only in MVP + CFDictionarySetValue(attrs, kSecAttrKeySizeInBits, cfNumber(256)) + CFDictionarySetValue(attrs, kSecAttrIsPermanent, kCFBooleanTrue) + CFDictionarySetValue(attrs, kSecAttrAccessControl, accessCtrl) + CFDictionarySetValue(attrs, kSecAttrApplicationTag, label.toCFData()) + // optional human-readable label: omitted for compatibility + + val errRef = alloc() + val priv = SecKeyCreateRandomKey(attrs, errRef.ptr) + ?: run { + CFRelease(attrs); CFRelease(accessCtrl) + error(cfErrorMessage(errRef.value)) + } + val pub = SecKeyCopyPublicKey(priv) ?: run { + CFRelease(attrs); CFRelease(accessCtrl); CFRelease(priv) + error("pub_key_null") + } + val pubKey: ECDSA.PublicKey = AppleEcdsaPublicKey(pub) + val privKey: ECDSA.PrivateKey = AppleEcdsaPrivateKey(priv) + CFRelease(attrs); CFRelease(accessCtrl) + Handle(public = pubKey, private = privKey, attributes = KeyAttributes(extractable = false, persistent = true, label = label)) + } + + override fun get(label: ByteArray): Handle? = memScoped { + val query = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, null, null) + CFDictionarySetValue(query, kSecClass, kSecClassKey) + CFDictionarySetValue(query, kSecAttrKeyType, kSecAttrKeyTypeECSECPrimeRandom) + CFDictionarySetValue(query, kSecAttrKeyClass, kSecAttrKeyClassPrivate) + CFDictionarySetValue(query, kSecAttrApplicationTag, label.toCFData()) + CFDictionarySetValue(query, kSecReturnRef, kCFBooleanTrue) + CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitOne) + CFDictionarySetValue(query, kSecUseDataProtectionKeychain, kCFBooleanTrue) + val out = alloc() + val status = SecItemCopyMatching(query, out.ptr) + if (status != errSecSuccess) { CFRelease(query); return null } + @Suppress("UNCHECKED_CAST") + val priv = out.value as SecKeyRef + val pub = SecKeyCopyPublicKey(priv) ?: run { CFRelease(priv); CFRelease(query); return null } + val handle: Handle = Handle(public = AppleEcdsaPublicKey(pub), private = AppleEcdsaPrivateKey(priv), attributes = KeyAttributes(false, true, label)) + CFRelease(query) + handle + } + + override fun exists(label: ByteArray): Boolean = memScoped { + val query = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, null, null) + CFDictionarySetValue(query, kSecClass, kSecClassKey) + CFDictionarySetValue(query, kSecAttrKeyType, kSecAttrKeyTypeECSECPrimeRandom) + CFDictionarySetValue(query, kSecAttrKeyClass, kSecAttrKeyClassPrivate) + CFDictionarySetValue(query, kSecAttrApplicationTag, label.toCFData()) + CFDictionarySetValue(query, kSecMatchLimit, kSecMatchLimitOne) + CFDictionarySetValue(query, kSecUseDataProtectionKeychain, kCFBooleanTrue) + val ok = SecItemCopyMatching(query, null) == errSecSuccess + CFRelease(query) + ok + } + + override fun delete(label: ByteArray): Boolean = memScoped { + val query = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, null, null) + CFDictionarySetValue(query, kSecClass, kSecClassKey) + CFDictionarySetValue(query, kSecAttrKeyType, kSecAttrKeyTypeECSECPrimeRandom) + CFDictionarySetValue(query, kSecAttrKeyClass, kSecAttrKeyClassPrivate) + CFDictionarySetValue(query, kSecAttrApplicationTag, label.toCFData()) + CFDictionarySetValue(query, kSecUseDataProtectionKeychain, kCFBooleanTrue) + val status = SecItemDelete(query) + CFRelease(query) + status == errSecSuccess || status == errSecItemNotFound + } +} + +// --- ECDSA key wrappers (DER format support; RAW not supported in MVP) --- + +@OptIn(ExperimentalForeignApi::class) +private class AppleEcdsaPublicKey(private val key: SecKeyRef) : ECDSA.PublicKey { + + override fun signatureVerifier(digest: CryptographyAlgorithmId, format: ECDSA.SignatureFormat): SignatureVerifier { + require(format == ECDSA.SignatureFormat.DER) { "Only DER signatures supported in MVP" } + return object : SignatureVerifier { + override fun createVerifyFunction(): VerifyFunction = object : VerifyFunction { + private var acc = ByteArray(0) + private var closed = false + override fun update(source: ByteArray, startIndex: Int, endIndex: Int) { + check(!closed) { "Already closed" } + acc += source.copyOfRange(startIndex, endIndex) + } + override fun tryVerify(signature: ByteArray, startIndex: Int, endIndex: Int): Boolean = memScoped { + check(!closed) { "Already closed" } + val error = alloc() + val ok = acc.useNSData { data -> + signature.useNSData(startIndex, endIndex) { sig -> + SecKeyVerifySignature( + key = key, + algorithm = digest.ecdsaSecKeyAlgorithm(), + signedData = data.retainBridgeAs(), + error = error.ptr, + signature = sig.retainBridgeAs() + ) + } + } + acc = ByteArray(0) + ok + } + override fun verify(signature: ByteArray, startIndex: Int, endIndex: Int) { + if (!tryVerify(signature, startIndex, endIndex)) error("Invalid signature") + } + override fun reset() { acc = ByteArray(0); closed = false } + override fun close() { closed = true; acc = ByteArray(0) } + } + } + } + + override fun encodeToByteArrayBlocking(format: EC.PublicKey.Format): ByteArray { + val raw = exportKey(key) + return when (format) { + EC.PublicKey.Format.JWK -> error("JWK not supported") + EC.PublicKey.Format.RAW -> raw + EC.PublicKey.Format.RAW.Compressed -> error("Compressed RAW not supported") + EC.PublicKey.Format.DER -> encodeSpki(raw) + EC.PublicKey.Format.PEM -> encodeSpki(raw).wrapPem("PUBLIC KEY") + } + } +} + +@OptIn(ExperimentalForeignApi::class) +private class AppleEcdsaPrivateKey(private val key: SecKeyRef) : ECDSA.PrivateKey { + + override fun signatureGenerator(digest: CryptographyAlgorithmId, format: ECDSA.SignatureFormat): SignatureGenerator { + require(format == ECDSA.SignatureFormat.DER) { "Only DER signatures supported in MVP" } + return object : SignatureGenerator { + override fun createSignFunction(): SignFunction = object : SignFunction { + private var acc = ByteArray(0) + private var closed = false + override fun update(source: ByteArray, startIndex: Int, endIndex: Int) { + check(!closed) { "Already closed" } + acc += source.copyOfRange(startIndex, endIndex) + } + override fun signToByteArray(): ByteArray = memScoped { + check(!closed) { "Already closed" } + val error = alloc() + val signature = acc.useNSData { data -> + SecKeyCreateSignature( + key = key, + algorithm = digest.ecdsaSecKeyAlgorithm(), + dataToSign = data.retainBridgeAs(), + error = error.ptr + )?.releaseBridgeAs() + } + if (signature == null) error(cfErrorMessage(error.value)) + acc = ByteArray(0) + signature.toByteArray() + } + override fun signIntoByteArray(destination: ByteArray, destinationOffset: Int): Int { + val s = signToByteArray() + s.copyInto(destination, destinationOffset) + return s.size + } + override fun reset() { acc = ByteArray(0); closed = false } + override fun close() { closed = true; acc = ByteArray(0) } + } + } + } + + override fun encodeToByteArrayBlocking(format: EC.PrivateKey.Format): ByteArray { + error("Private key export is disabled for non-extractable keys") + } +} + +// --- Helpers --- + +private fun CryptographyAlgorithmId.ecdsaSecKeyAlgorithm(): SecKeyAlgorithm? = when (this) { + SHA1 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA1 + SHA224 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA224 + SHA256 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA256 + SHA384 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA384 + SHA512 -> kSecKeyAlgorithmECDSASignatureMessageX962SHA512 + else -> null +} + +private fun encodeSpki(rawUncompressedPoint: ByteArray): ByteArray { + // Minimal SPKI encoder: 0x30 SEQ(alg + bitstring) + // For MVP we construct ASN.1 DER via simple concatenation since sizes are fixed for P-256 + // AlgorithmIdentifier for EC P-256: 06 08 2A 86 48 CE 3D 02 01 (id-ecPublicKey) + // parameters = 06 08 2A 86 48 CE 3D 03 01 07 (secp256r1) + val alg = byteArrayOf( + 0x30, 0x13, // SEQUENCE len 19 + 0x06, 0x07, 0x2A, 0x86.toByte(), 0x48, 0xCE.toByte(), 0x3D, 0x02, 0x01, + 0x06, 0x08, 0x2A, 0x86.toByte(), 0x48, 0xCE.toByte(), 0x3D, 0x03, 0x01, 0x07 + ) + val bitStringHeader = byteArrayOf(0x03, (rawUncompressedPoint.size + 1).toByte(), 0x00) + val body = alg + bitStringHeader + rawUncompressedPoint + val header = byteArrayOf(0x30, body.size.toByte()) + return header + body +} + +private fun ByteArray.wrapPem(label: String): ByteArray { + val base64 = kotlin.io.encoding.Base64.encode(this) + val lines = base64.chunked(64).joinToString("\n") + val pem = "-----BEGIN $label-----\n$lines\n-----END $label-----\n" + return pem.encodeToByteArray() +} + +@OptIn(ExperimentalForeignApi::class) +private fun ByteArray.useCFData(block: (CFDataRef?) -> T): T = memScoped { + this@useCFData.usePinned { pin -> + val d = CFDataCreate(kCFAllocatorDefault, pin.addressOf(0).reinterpret(), size.convert()) + try { return block(d) } finally { if (d != null) CFRelease(d) } + } +} + +@OptIn(ExperimentalForeignApi::class) +private fun exportKey(key: SecKeyRef): ByteArray = memScoped { + val err = alloc() + val data = SecKeyCopyExternalRepresentation(key, err.ptr)?.releaseBridgeAs() + if (data == null) error(cfErrorMessage(err.value)) + data.toByteArray() +} + +@OptIn(ExperimentalForeignApi::class) +private fun ByteArray.toCFData(): CFDataRef? = memScoped { + this@toCFData.usePinned { pin -> CFDataCreate(kCFAllocatorDefault, pin.addressOf(0).reinterpret(), size.convert()) } +} + +// string label helper removed (not used) + +@OptIn(ExperimentalForeignApi::class) +private fun createAccessControl(policy: AccessPolicy): SecAccessControlRef? = memScoped { + val flags = (kSecAccessControlPrivateKeyUsage) or (if (policy.requireUserPresence) kSecAccessControlUserPresence else 0u) + val err = alloc() + val ac = SecAccessControlCreateWithFlags( + allocator = kCFAllocatorDefault, + protection = when (policy.accessibility) { + Accessibility.WhenUnlocked -> kSecAttrAccessibleWhenUnlocked + Accessibility.AfterFirstUnlock -> kSecAttrAccessibleAfterFirstUnlock + Accessibility.Always -> kSecAttrAccessibleAlways + Accessibility.WhenPasscodeSetThisDeviceOnly -> kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly + }, + flags = flags, + error = err.ptr + ) + if (ac == null) error(cfErrorMessage(err.value)) + ac +} + +@OptIn(ExperimentalForeignApi::class) +private fun cfErrorMessage(e: CFErrorRef?): String { + if (e == null) return "error" + val desc = CFErrorCopyDescription(e) + val ns = (desc as CFTypeRef?).releaseBridgeAs() + return ns?.toString() ?: "error" +} + +@Suppress("UNCHECKED_CAST") +private fun Any?.retainBridgeAs(): T? = CFBridgingRetain(this)?.let { it as T } + +@Suppress("UNCHECKED_CAST") +private fun CFTypeRef?.releaseBridgeAs(): T? = CFBridgingRelease(this)?.let { it as T } + +@OptIn(ExperimentalForeignApi::class) +private fun cfNumber(i: Int): CFNumberRef? = memScoped { + CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, cValuesOf(i)) +} diff --git a/cryptography-providers/apple-keychain/src/macosArm64Test/kotlin/dev/whyoleg/cryptography/providers/apple/keychain/AppleKeyStoreEcdsaTest.kt b/cryptography-providers/apple-keychain/src/macosArm64Test/kotlin/dev/whyoleg/cryptography/providers/apple/keychain/AppleKeyStoreEcdsaTest.kt new file mode 100644 index 00000000..6e3b58bd --- /dev/null +++ b/cryptography-providers/apple-keychain/src/macosArm64Test/kotlin/dev/whyoleg/cryptography/providers/apple/keychain/AppleKeyStoreEcdsaTest.kt @@ -0,0 +1,35 @@ +package dev.whyoleg.cryptography.providers.apple.keychain + +import dev.whyoleg.cryptography.* +import dev.whyoleg.cryptography.algorithms.* +import dev.whyoleg.cryptography.storage.* +import kotlin.test.* + +@OptIn(ExperimentalKeyStorageApi::class) +class AppleKeyStoreEcdsaTest { + @Test + fun generate_sign_verify_delete() { + val label = "test-ecdsa-${kotlin.random.Random.nextInt()}".encodeToByteArray() + val store = AppleKeyStore.ecdsa(EC.Curve.P256) + assertFalse(store.exists(label)) + + val h = store.generate(label, AccessPolicy()) + assertTrue(store.exists(label)) + + val data = "hello-apple-keystore".encodeToByteArray() + val gen = h.private.signatureGenerator(SHA256, ECDSA.SignatureFormat.DER) + val ver = h.public.signatureVerifier(SHA256, ECDSA.SignatureFormat.DER) + val sig = gen.createSignFunction().run { + update(data, 0, data.size); signToByteArray() + } + val ok = ver.createVerifyFunction().run { + update(data, 0, data.size); tryVerify(sig, 0, sig.size) + } + assertTrue(ok) + + assertTrue(store.delete(label)) + assertFalse(store.exists(label)) + assertNull(store.get(label)) + } +} + diff --git a/cryptography-storage/api/cryptography-storage.api b/cryptography-storage/api/cryptography-storage.api new file mode 100644 index 00000000..48c170ec --- /dev/null +++ b/cryptography-storage/api/cryptography-storage.api @@ -0,0 +1,104 @@ +public final class dev/whyoleg/cryptography/storage/AccessPolicy { + public fun ()V + public fun (ZLdev/whyoleg/cryptography/storage/Accessibility;Ldev/whyoleg/cryptography/storage/DeviceBinding;Z)V + public synthetic fun (ZLdev/whyoleg/cryptography/storage/Accessibility;Ldev/whyoleg/cryptography/storage/DeviceBinding;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Z + public final fun component2 ()Ldev/whyoleg/cryptography/storage/Accessibility; + public final fun component3 ()Ldev/whyoleg/cryptography/storage/DeviceBinding; + public final fun component4 ()Z + public final fun copy (ZLdev/whyoleg/cryptography/storage/Accessibility;Ldev/whyoleg/cryptography/storage/DeviceBinding;Z)Ldev/whyoleg/cryptography/storage/AccessPolicy; + public static synthetic fun copy$default (Ldev/whyoleg/cryptography/storage/AccessPolicy;ZLdev/whyoleg/cryptography/storage/Accessibility;Ldev/whyoleg/cryptography/storage/DeviceBinding;ZILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/AccessPolicy; + public fun equals (Ljava/lang/Object;)Z + public final fun getAccessibility ()Ldev/whyoleg/cryptography/storage/Accessibility; + public final fun getDeviceBinding ()Ldev/whyoleg/cryptography/storage/DeviceBinding; + public final fun getExportablePrivate ()Z + public final fun getRequireUserPresence ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/whyoleg/cryptography/storage/Accessibility : java/lang/Enum { + public static final field AfterFirstUnlock Ldev/whyoleg/cryptography/storage/Accessibility; + public static final field Always Ldev/whyoleg/cryptography/storage/Accessibility; + public static final field WhenPasscodeSetThisDeviceOnly Ldev/whyoleg/cryptography/storage/Accessibility; + public static final field WhenUnlocked Ldev/whyoleg/cryptography/storage/Accessibility; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Ldev/whyoleg/cryptography/storage/Accessibility; + public static fun values ()[Ldev/whyoleg/cryptography/storage/Accessibility; +} + +public abstract interface class dev/whyoleg/cryptography/storage/AsymmetricStore { + public abstract fun delete ([B)Z + public abstract fun exists ([B)Z + public abstract fun generate ([BLdev/whyoleg/cryptography/storage/AccessPolicy;)Ldev/whyoleg/cryptography/storage/Handle; + public static synthetic fun generate$default (Ldev/whyoleg/cryptography/storage/AsymmetricStore;[BLdev/whyoleg/cryptography/storage/AccessPolicy;ILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/Handle; + public abstract fun get ([B)Ldev/whyoleg/cryptography/storage/Handle; +} + +public final class dev/whyoleg/cryptography/storage/DeviceBinding : java/lang/Enum { + public static final field None Ldev/whyoleg/cryptography/storage/DeviceBinding; + public static final field SecureEnclavePreferred Ldev/whyoleg/cryptography/storage/DeviceBinding; + public static final field ThisDeviceOnly Ldev/whyoleg/cryptography/storage/DeviceBinding; + public static fun getEntries ()Lkotlin/enums/EnumEntries; + public static fun valueOf (Ljava/lang/String;)Ldev/whyoleg/cryptography/storage/DeviceBinding; + public static fun values ()[Ldev/whyoleg/cryptography/storage/DeviceBinding; +} + +public abstract interface annotation class dev/whyoleg/cryptography/storage/ExperimentalKeyStorageApi : java/lang/annotation/Annotation { +} + +public final class dev/whyoleg/cryptography/storage/Handle { + public fun (Ljava/lang/Object;Ljava/lang/Object;Ldev/whyoleg/cryptography/storage/KeyAttributes;)V + public final fun component1 ()Ljava/lang/Object; + public final fun component2 ()Ljava/lang/Object; + public final fun component3 ()Ldev/whyoleg/cryptography/storage/KeyAttributes; + public final fun copy (Ljava/lang/Object;Ljava/lang/Object;Ldev/whyoleg/cryptography/storage/KeyAttributes;)Ldev/whyoleg/cryptography/storage/Handle; + public static synthetic fun copy$default (Ldev/whyoleg/cryptography/storage/Handle;Ljava/lang/Object;Ljava/lang/Object;Ldev/whyoleg/cryptography/storage/KeyAttributes;ILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/Handle; + public fun equals (Ljava/lang/Object;)Z + public final fun getAttributes ()Ldev/whyoleg/cryptography/storage/KeyAttributes; + public final fun getPrivate ()Ljava/lang/Object; + public final fun getPublic ()Ljava/lang/Object; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class dev/whyoleg/cryptography/storage/KeyAttributes { + public fun (ZZ[B)V + public final fun component1 ()Z + public final fun component2 ()Z + public final fun component3 ()[B + public final fun copy (ZZ[B)Ldev/whyoleg/cryptography/storage/KeyAttributes; + public static synthetic fun copy$default (Ldev/whyoleg/cryptography/storage/KeyAttributes;ZZ[BILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/KeyAttributes; + public fun equals (Ljava/lang/Object;)Z + public final fun getExtractable ()Z + public final fun getLabel ()[B + public final fun getPersistent ()Z + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class dev/whyoleg/cryptography/storage/KeyStore { + public abstract fun aesCbc-6q1zMKY (I)Ldev/whyoleg/cryptography/storage/SymmetricStore; + public static synthetic fun aesCbc-6q1zMKY$default (Ldev/whyoleg/cryptography/storage/KeyStore;IILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/SymmetricStore; + public abstract fun aesCtr-6q1zMKY (I)Ldev/whyoleg/cryptography/storage/SymmetricStore; + public static synthetic fun aesCtr-6q1zMKY$default (Ldev/whyoleg/cryptography/storage/KeyStore;IILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/SymmetricStore; + public abstract fun aesGcm-6q1zMKY (I)Ldev/whyoleg/cryptography/storage/SymmetricStore; + public static synthetic fun aesGcm-6q1zMKY$default (Ldev/whyoleg/cryptography/storage/KeyStore;IILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/SymmetricStore; + public abstract fun ecdsa-yfdS0HE (Ljava/lang/String;)Ldev/whyoleg/cryptography/storage/AsymmetricStore; + public static synthetic fun ecdsa-yfdS0HE$default (Ldev/whyoleg/cryptography/storage/KeyStore;Ljava/lang/String;ILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/AsymmetricStore; + public abstract fun rsaOaep-ksXStGo (ILdev/whyoleg/cryptography/CryptographyAlgorithmId;)Ldev/whyoleg/cryptography/storage/AsymmetricStore; + public static synthetic fun rsaOaep-ksXStGo$default (Ldev/whyoleg/cryptography/storage/KeyStore;ILdev/whyoleg/cryptography/CryptographyAlgorithmId;ILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/AsymmetricStore; + public abstract fun rsaPkcs1-ksXStGo (ILdev/whyoleg/cryptography/CryptographyAlgorithmId;)Ldev/whyoleg/cryptography/storage/AsymmetricStore; + public static synthetic fun rsaPkcs1-ksXStGo$default (Ldev/whyoleg/cryptography/storage/KeyStore;ILdev/whyoleg/cryptography/CryptographyAlgorithmId;ILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/AsymmetricStore; + public abstract fun rsaPss-ksXStGo (ILdev/whyoleg/cryptography/CryptographyAlgorithmId;)Ldev/whyoleg/cryptography/storage/AsymmetricStore; + public static synthetic fun rsaPss-ksXStGo$default (Ldev/whyoleg/cryptography/storage/KeyStore;ILdev/whyoleg/cryptography/CryptographyAlgorithmId;ILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/AsymmetricStore; +} + +public abstract interface class dev/whyoleg/cryptography/storage/SymmetricStore { + public abstract fun delete ([B)Z + public abstract fun exists ([B)Z + public abstract fun generate ([BLdev/whyoleg/cryptography/storage/AccessPolicy;)Ldev/whyoleg/cryptography/storage/Handle; + public static synthetic fun generate$default (Ldev/whyoleg/cryptography/storage/SymmetricStore;[BLdev/whyoleg/cryptography/storage/AccessPolicy;ILjava/lang/Object;)Ldev/whyoleg/cryptography/storage/Handle; + public abstract fun get ([B)Ldev/whyoleg/cryptography/storage/Handle; +} + diff --git a/cryptography-storage/api/cryptography-storage.klib.api b/cryptography-storage/api/cryptography-storage.klib.api new file mode 100644 index 00000000..ceab1916 --- /dev/null +++ b/cryptography-storage/api/cryptography-storage.klib.api @@ -0,0 +1,120 @@ +// Klib ABI Dump +// Targets: [androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86, iosArm64, iosSimulatorArm64, iosX64, js, linuxArm64, linuxX64, macosArm64, macosX64, mingwX64, tvosArm64, tvosSimulatorArm64, tvosX64, wasmJs, wasmWasi, watchosArm32, watchosArm64, watchosDeviceArm64, watchosSimulatorArm64, watchosX64] +// Rendering settings: +// - Signature version: 2 +// - Show manifest properties: true +// - Show declarations: true + +// Library unique name: +open annotation class dev.whyoleg.cryptography.storage/ExperimentalKeyStorageApi : kotlin/Annotation { // dev.whyoleg.cryptography.storage/ExperimentalKeyStorageApi|null[0] + constructor () // dev.whyoleg.cryptography.storage/ExperimentalKeyStorageApi.|(){}[0] +} + +final enum class dev.whyoleg.cryptography.storage/Accessibility : kotlin/Enum { // dev.whyoleg.cryptography.storage/Accessibility|null[0] + enum entry AfterFirstUnlock // dev.whyoleg.cryptography.storage/Accessibility.AfterFirstUnlock|null[0] + enum entry Always // dev.whyoleg.cryptography.storage/Accessibility.Always|null[0] + enum entry WhenPasscodeSetThisDeviceOnly // dev.whyoleg.cryptography.storage/Accessibility.WhenPasscodeSetThisDeviceOnly|null[0] + enum entry WhenUnlocked // dev.whyoleg.cryptography.storage/Accessibility.WhenUnlocked|null[0] + + final val entries // dev.whyoleg.cryptography.storage/Accessibility.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // dev.whyoleg.cryptography.storage/Accessibility.entries.|#static(){}[0] + + final fun valueOf(kotlin/String): dev.whyoleg.cryptography.storage/Accessibility // dev.whyoleg.cryptography.storage/Accessibility.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // dev.whyoleg.cryptography.storage/Accessibility.values|values#static(){}[0] +} + +final enum class dev.whyoleg.cryptography.storage/DeviceBinding : kotlin/Enum { // dev.whyoleg.cryptography.storage/DeviceBinding|null[0] + enum entry None // dev.whyoleg.cryptography.storage/DeviceBinding.None|null[0] + enum entry SecureEnclavePreferred // dev.whyoleg.cryptography.storage/DeviceBinding.SecureEnclavePreferred|null[0] + enum entry ThisDeviceOnly // dev.whyoleg.cryptography.storage/DeviceBinding.ThisDeviceOnly|null[0] + + final val entries // dev.whyoleg.cryptography.storage/DeviceBinding.entries|#static{}entries[0] + final fun (): kotlin.enums/EnumEntries // dev.whyoleg.cryptography.storage/DeviceBinding.entries.|#static(){}[0] + + final fun valueOf(kotlin/String): dev.whyoleg.cryptography.storage/DeviceBinding // dev.whyoleg.cryptography.storage/DeviceBinding.valueOf|valueOf#static(kotlin.String){}[0] + final fun values(): kotlin/Array // dev.whyoleg.cryptography.storage/DeviceBinding.values|values#static(){}[0] +} + +abstract interface <#A: kotlin/Any?, #B: kotlin/Any?> dev.whyoleg.cryptography.storage/AsymmetricStore { // dev.whyoleg.cryptography.storage/AsymmetricStore|null[0] + abstract fun delete(kotlin/ByteArray): kotlin/Boolean // dev.whyoleg.cryptography.storage/AsymmetricStore.delete|delete(kotlin.ByteArray){}[0] + abstract fun exists(kotlin/ByteArray): kotlin/Boolean // dev.whyoleg.cryptography.storage/AsymmetricStore.exists|exists(kotlin.ByteArray){}[0] + abstract fun generate(kotlin/ByteArray, dev.whyoleg.cryptography.storage/AccessPolicy = ...): dev.whyoleg.cryptography.storage/Handle<#A, #B> // dev.whyoleg.cryptography.storage/AsymmetricStore.generate|generate(kotlin.ByteArray;dev.whyoleg.cryptography.storage.AccessPolicy){}[0] + abstract fun get(kotlin/ByteArray): dev.whyoleg.cryptography.storage/Handle<#A, #B>? // dev.whyoleg.cryptography.storage/AsymmetricStore.get|get(kotlin.ByteArray){}[0] +} + +abstract interface <#A: kotlin/Any?> dev.whyoleg.cryptography.storage/SymmetricStore { // dev.whyoleg.cryptography.storage/SymmetricStore|null[0] + abstract fun delete(kotlin/ByteArray): kotlin/Boolean // dev.whyoleg.cryptography.storage/SymmetricStore.delete|delete(kotlin.ByteArray){}[0] + abstract fun exists(kotlin/ByteArray): kotlin/Boolean // dev.whyoleg.cryptography.storage/SymmetricStore.exists|exists(kotlin.ByteArray){}[0] + abstract fun generate(kotlin/ByteArray, dev.whyoleg.cryptography.storage/AccessPolicy = ...): dev.whyoleg.cryptography.storage/Handle<#A, kotlin/Unit> // dev.whyoleg.cryptography.storage/SymmetricStore.generate|generate(kotlin.ByteArray;dev.whyoleg.cryptography.storage.AccessPolicy){}[0] + abstract fun get(kotlin/ByteArray): dev.whyoleg.cryptography.storage/Handle<#A, kotlin/Unit>? // dev.whyoleg.cryptography.storage/SymmetricStore.get|get(kotlin.ByteArray){}[0] +} + +abstract interface dev.whyoleg.cryptography.storage/KeyStore { // dev.whyoleg.cryptography.storage/KeyStore|null[0] + abstract fun aesCbc(dev.whyoleg.cryptography/BinarySize = ...): dev.whyoleg.cryptography.storage/SymmetricStore // dev.whyoleg.cryptography.storage/KeyStore.aesCbc|aesCbc(dev.whyoleg.cryptography.BinarySize){}[0] + abstract fun aesCtr(dev.whyoleg.cryptography/BinarySize = ...): dev.whyoleg.cryptography.storage/SymmetricStore // dev.whyoleg.cryptography.storage/KeyStore.aesCtr|aesCtr(dev.whyoleg.cryptography.BinarySize){}[0] + abstract fun aesGcm(dev.whyoleg.cryptography/BinarySize = ...): dev.whyoleg.cryptography.storage/SymmetricStore // dev.whyoleg.cryptography.storage/KeyStore.aesGcm|aesGcm(dev.whyoleg.cryptography.BinarySize){}[0] + abstract fun ecdsa(dev.whyoleg.cryptography.algorithms/EC.Curve = ...): dev.whyoleg.cryptography.storage/AsymmetricStore // dev.whyoleg.cryptography.storage/KeyStore.ecdsa|ecdsa(dev.whyoleg.cryptography.algorithms.EC.Curve){}[0] + abstract fun rsaOaep(dev.whyoleg.cryptography/BinarySize = ..., dev.whyoleg.cryptography/CryptographyAlgorithmId = ...): dev.whyoleg.cryptography.storage/AsymmetricStore // dev.whyoleg.cryptography.storage/KeyStore.rsaOaep|rsaOaep(dev.whyoleg.cryptography.BinarySize;dev.whyoleg.cryptography.CryptographyAlgorithmId){}[0] + abstract fun rsaPkcs1(dev.whyoleg.cryptography/BinarySize = ..., dev.whyoleg.cryptography/CryptographyAlgorithmId = ...): dev.whyoleg.cryptography.storage/AsymmetricStore // dev.whyoleg.cryptography.storage/KeyStore.rsaPkcs1|rsaPkcs1(dev.whyoleg.cryptography.BinarySize;dev.whyoleg.cryptography.CryptographyAlgorithmId){}[0] + abstract fun rsaPss(dev.whyoleg.cryptography/BinarySize = ..., dev.whyoleg.cryptography/CryptographyAlgorithmId = ...): dev.whyoleg.cryptography.storage/AsymmetricStore // dev.whyoleg.cryptography.storage/KeyStore.rsaPss|rsaPss(dev.whyoleg.cryptography.BinarySize;dev.whyoleg.cryptography.CryptographyAlgorithmId){}[0] +} + +final class <#A: kotlin/Any?, #B: kotlin/Any?> dev.whyoleg.cryptography.storage/Handle { // dev.whyoleg.cryptography.storage/Handle|null[0] + constructor (#A, #B, dev.whyoleg.cryptography.storage/KeyAttributes) // dev.whyoleg.cryptography.storage/Handle.|(1:0;1:1;dev.whyoleg.cryptography.storage.KeyAttributes){}[0] + + final val attributes // dev.whyoleg.cryptography.storage/Handle.attributes|{}attributes[0] + final fun (): dev.whyoleg.cryptography.storage/KeyAttributes // dev.whyoleg.cryptography.storage/Handle.attributes.|(){}[0] + final val private // dev.whyoleg.cryptography.storage/Handle.private|{}private[0] + final fun (): #B // dev.whyoleg.cryptography.storage/Handle.private.|(){}[0] + final val public // dev.whyoleg.cryptography.storage/Handle.public|{}public[0] + final fun (): #A // dev.whyoleg.cryptography.storage/Handle.public.|(){}[0] + + final fun component1(): #A // dev.whyoleg.cryptography.storage/Handle.component1|component1(){}[0] + final fun component2(): #B // dev.whyoleg.cryptography.storage/Handle.component2|component2(){}[0] + final fun component3(): dev.whyoleg.cryptography.storage/KeyAttributes // dev.whyoleg.cryptography.storage/Handle.component3|component3(){}[0] + final fun copy(#A = ..., #B = ..., dev.whyoleg.cryptography.storage/KeyAttributes = ...): dev.whyoleg.cryptography.storage/Handle<#A, #B> // dev.whyoleg.cryptography.storage/Handle.copy|copy(1:0;1:1;dev.whyoleg.cryptography.storage.KeyAttributes){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // dev.whyoleg.cryptography.storage/Handle.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // dev.whyoleg.cryptography.storage/Handle.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // dev.whyoleg.cryptography.storage/Handle.toString|toString(){}[0] +} + +final class dev.whyoleg.cryptography.storage/AccessPolicy { // dev.whyoleg.cryptography.storage/AccessPolicy|null[0] + constructor (kotlin/Boolean = ..., dev.whyoleg.cryptography.storage/Accessibility = ..., dev.whyoleg.cryptography.storage/DeviceBinding = ..., kotlin/Boolean = ...) // dev.whyoleg.cryptography.storage/AccessPolicy.|(kotlin.Boolean;dev.whyoleg.cryptography.storage.Accessibility;dev.whyoleg.cryptography.storage.DeviceBinding;kotlin.Boolean){}[0] + + final val accessibility // dev.whyoleg.cryptography.storage/AccessPolicy.accessibility|{}accessibility[0] + final fun (): dev.whyoleg.cryptography.storage/Accessibility // dev.whyoleg.cryptography.storage/AccessPolicy.accessibility.|(){}[0] + final val deviceBinding // dev.whyoleg.cryptography.storage/AccessPolicy.deviceBinding|{}deviceBinding[0] + final fun (): dev.whyoleg.cryptography.storage/DeviceBinding // dev.whyoleg.cryptography.storage/AccessPolicy.deviceBinding.|(){}[0] + final val exportablePrivate // dev.whyoleg.cryptography.storage/AccessPolicy.exportablePrivate|{}exportablePrivate[0] + final fun (): kotlin/Boolean // dev.whyoleg.cryptography.storage/AccessPolicy.exportablePrivate.|(){}[0] + final val requireUserPresence // dev.whyoleg.cryptography.storage/AccessPolicy.requireUserPresence|{}requireUserPresence[0] + final fun (): kotlin/Boolean // dev.whyoleg.cryptography.storage/AccessPolicy.requireUserPresence.|(){}[0] + + final fun component1(): kotlin/Boolean // dev.whyoleg.cryptography.storage/AccessPolicy.component1|component1(){}[0] + final fun component2(): dev.whyoleg.cryptography.storage/Accessibility // dev.whyoleg.cryptography.storage/AccessPolicy.component2|component2(){}[0] + final fun component3(): dev.whyoleg.cryptography.storage/DeviceBinding // dev.whyoleg.cryptography.storage/AccessPolicy.component3|component3(){}[0] + final fun component4(): kotlin/Boolean // dev.whyoleg.cryptography.storage/AccessPolicy.component4|component4(){}[0] + final fun copy(kotlin/Boolean = ..., dev.whyoleg.cryptography.storage/Accessibility = ..., dev.whyoleg.cryptography.storage/DeviceBinding = ..., kotlin/Boolean = ...): dev.whyoleg.cryptography.storage/AccessPolicy // dev.whyoleg.cryptography.storage/AccessPolicy.copy|copy(kotlin.Boolean;dev.whyoleg.cryptography.storage.Accessibility;dev.whyoleg.cryptography.storage.DeviceBinding;kotlin.Boolean){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // dev.whyoleg.cryptography.storage/AccessPolicy.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // dev.whyoleg.cryptography.storage/AccessPolicy.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // dev.whyoleg.cryptography.storage/AccessPolicy.toString|toString(){}[0] +} + +final class dev.whyoleg.cryptography.storage/KeyAttributes { // dev.whyoleg.cryptography.storage/KeyAttributes|null[0] + constructor (kotlin/Boolean, kotlin/Boolean, kotlin/ByteArray?) // dev.whyoleg.cryptography.storage/KeyAttributes.|(kotlin.Boolean;kotlin.Boolean;kotlin.ByteArray?){}[0] + + final val extractable // dev.whyoleg.cryptography.storage/KeyAttributes.extractable|{}extractable[0] + final fun (): kotlin/Boolean // dev.whyoleg.cryptography.storage/KeyAttributes.extractable.|(){}[0] + final val label // dev.whyoleg.cryptography.storage/KeyAttributes.label|{}label[0] + final fun (): kotlin/ByteArray? // dev.whyoleg.cryptography.storage/KeyAttributes.label.|(){}[0] + final val persistent // dev.whyoleg.cryptography.storage/KeyAttributes.persistent|{}persistent[0] + final fun (): kotlin/Boolean // dev.whyoleg.cryptography.storage/KeyAttributes.persistent.|(){}[0] + + final fun component1(): kotlin/Boolean // dev.whyoleg.cryptography.storage/KeyAttributes.component1|component1(){}[0] + final fun component2(): kotlin/Boolean // dev.whyoleg.cryptography.storage/KeyAttributes.component2|component2(){}[0] + final fun component3(): kotlin/ByteArray? // dev.whyoleg.cryptography.storage/KeyAttributes.component3|component3(){}[0] + final fun copy(kotlin/Boolean = ..., kotlin/Boolean = ..., kotlin/ByteArray? = ...): dev.whyoleg.cryptography.storage/KeyAttributes // dev.whyoleg.cryptography.storage/KeyAttributes.copy|copy(kotlin.Boolean;kotlin.Boolean;kotlin.ByteArray?){}[0] + final fun equals(kotlin/Any?): kotlin/Boolean // dev.whyoleg.cryptography.storage/KeyAttributes.equals|equals(kotlin.Any?){}[0] + final fun hashCode(): kotlin/Int // dev.whyoleg.cryptography.storage/KeyAttributes.hashCode|hashCode(){}[0] + final fun toString(): kotlin/String // dev.whyoleg.cryptography.storage/KeyAttributes.toString|toString(){}[0] +} diff --git a/cryptography-storage/build.gradle.kts b/cryptography-storage/build.gradle.kts new file mode 100644 index 00000000..38de1d7f --- /dev/null +++ b/cryptography-storage/build.gradle.kts @@ -0,0 +1,16 @@ +import ckbuild.* + +plugins { + id("ckbuild.multiplatform-library") +} + +description = "cryptography-kotlin storage API (experimental)" + +kotlin { + allTargets() + + sourceSets.commonMain.dependencies { + api(projects.cryptographyCore) + } +} + diff --git a/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/AccessPolicy.kt b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/AccessPolicy.kt new file mode 100644 index 00000000..ad6ca46e --- /dev/null +++ b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/AccessPolicy.kt @@ -0,0 +1,37 @@ +package dev.whyoleg.cryptography.storage + +/** + * Provider-agnostic access policy that controls key generation/import and usage. + * Implementations map these fields to platform-specific controls (e.g., Keychain attributes). + */ +@ExperimentalKeyStorageApi +public data class AccessPolicy( + /** Require an interactive user presence/authentication on sensitive operations if supported. */ + val requireUserPresence: Boolean = false, + /** Storage accessibility class (e.g., Keychain accessibility). */ + val accessibility: Accessibility = Accessibility.AfterFirstUnlock, + /** Whether to bind keys to the current device or prefer hardware-bound storage when available. */ + val deviceBinding: DeviceBinding = DeviceBinding.None, + /** Allow exporting private material (discouraged; defaults to false). */ + val exportablePrivate: Boolean = false, +) + +/** Storage accessibility levels mapped by providers to platform capabilities. */ +@ExperimentalKeyStorageApi +public enum class Accessibility { + WhenUnlocked, + AfterFirstUnlock, + Always, + WhenPasscodeSetThisDeviceOnly, +} + +/** Device binding preference for generated/imported keys. */ +@ExperimentalKeyStorageApi +public enum class DeviceBinding { + /** No device binding requested. */ + None, + /** Keep on this device only (non-migratable). */ + ThisDeviceOnly, + /** Prefer hardware-backed secure enclave if available. */ + SecureEnclavePreferred, +} diff --git a/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/AsymmetricStore.kt b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/AsymmetricStore.kt new file mode 100644 index 00000000..30e1b3b9 --- /dev/null +++ b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/AsymmetricStore.kt @@ -0,0 +1,22 @@ +package dev.whyoleg.cryptography.storage + +/** + * Algorithm-agnostic storage for asymmetric key pairs. + * + * All methods accept a binary-safe [label] which is mapped to provider-specific aliases. + * Implementations must enforce [AccessPolicy] and non-extractable semantics. + */ +@ExperimentalKeyStorageApi +public interface AsymmetricStore { + /** Generate and persist a new key pair under [label]. Returns a handle with attributes. */ + public fun generate(label: ByteArray, access: AccessPolicy = AccessPolicy()): Handle + + /** Fetch an existing key pair by [label], or null if not found. */ + public fun get(label: ByteArray): Handle? + + /** Check existence by [label] without returning a handle. */ + public fun exists(label: ByteArray): Boolean + + /** Delete a key pair by [label]. Returns true if an item was removed. */ + public fun delete(label: ByteArray): Boolean +} diff --git a/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/ExperimentalKeyStorageApi.kt b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/ExperimentalKeyStorageApi.kt new file mode 100644 index 00000000..99662800 --- /dev/null +++ b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/ExperimentalKeyStorageApi.kt @@ -0,0 +1,10 @@ +package dev.whyoleg.cryptography.storage + +/** + * Marks storage-related APIs as experimental. + * + * Storage APIs are new and may evolve. Consumers should explicitly opt in and + * be prepared for source changes until the API is stabilized. + */ +@RequiresOptIn(level = RequiresOptIn.Level.WARNING) +public annotation class ExperimentalKeyStorageApi diff --git a/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/Handle.kt b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/Handle.kt new file mode 100644 index 00000000..c5ae86c3 --- /dev/null +++ b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/Handle.kt @@ -0,0 +1,14 @@ +package dev.whyoleg.cryptography.storage + +/** + * A resolved key handle consisting of algorithm-typed public/private objects and key [attributes]. + * + * Providers may return lightweight wrappers that route cryptographic operations to the underlying + * platform (e.g., Keychain). Private handles for non-extractable keys must not expose private material. + */ +@ExperimentalKeyStorageApi +public data class Handle( + val public: Public, + val private: Private, + val attributes: KeyAttributes, +) diff --git a/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/KeyAttributes.kt b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/KeyAttributes.kt new file mode 100644 index 00000000..efb14590 --- /dev/null +++ b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/KeyAttributes.kt @@ -0,0 +1,15 @@ +package dev.whyoleg.cryptography.storage + +/** + * Provider-agnostic key attributes returned alongside key handles. + * + * - [extractable]: whether the private material can be exported in any form. + * - [persistent]: whether the key is stored by the platform and survives process restarts. + * - [label]: optional provider label/alias used to look up the key (binary-safe). + */ +@ExperimentalKeyStorageApi +public data class KeyAttributes( + val extractable: Boolean, + val persistent: Boolean, + val label: ByteArray?, +) diff --git a/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/KeyStore.kt b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/KeyStore.kt new file mode 100644 index 00000000..7ca61cd6 --- /dev/null +++ b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/KeyStore.kt @@ -0,0 +1,47 @@ +package dev.whyoleg.cryptography.storage + +import dev.whyoleg.cryptography.* +import dev.whyoleg.cryptography.BinarySize.Companion.bits +import dev.whyoleg.cryptography.algorithms.* + +/** + * Entry point for provider-backed key storage. + * + * Implementations expose algorithm-typed stores that can generate, fetch, and delete + * persistent keys under binary-safe labels and enforce [AccessPolicy]. Returned handles + * integrate with existing algorithm APIs (e.g., ECDSA/RSA/AES) without exporting private material + * when keys are non-extractable. + */ +@ExperimentalKeyStorageApi +public interface KeyStore { + /** ECDSA key store for the given [curve] (default P-256). */ + public fun ecdsa(curve: EC.Curve = EC.Curve.P256): AsymmetricStore + + // RSA families + /** RSA-PSS key store configured with [keySize] and [digest]. */ + public fun rsaPss( + keySize: BinarySize = 4096.bits, + digest: CryptographyAlgorithmId = SHA512, + ): AsymmetricStore + + /** RSA-PKCS1 v1.5 key store configured with [keySize] and [digest]. */ + public fun rsaPkcs1( + keySize: BinarySize = 4096.bits, + digest: CryptographyAlgorithmId = SHA512, + ): AsymmetricStore + + /** RSA-OAEP key store configured with [keySize] and [digest]. */ + public fun rsaOaep( + keySize: BinarySize = 4096.bits, + digest: CryptographyAlgorithmId = SHA512, + ): AsymmetricStore + + // AES families + /** AES-GCM key store. */ + public fun aesGcm(size: BinarySize = AES.Key.Size.B256): SymmetricStore + /** AES-CBC key store. */ + public fun aesCbc(size: BinarySize = AES.Key.Size.B256): SymmetricStore + /** AES-CTR key store. */ + public fun aesCtr(size: BinarySize = AES.Key.Size.B256): SymmetricStore + // ECB is deliberately excluded due to DelicateCryptographyApi +} diff --git a/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/SymmetricStore.kt b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/SymmetricStore.kt new file mode 100644 index 00000000..bcf02532 --- /dev/null +++ b/cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/SymmetricStore.kt @@ -0,0 +1,22 @@ +package dev.whyoleg.cryptography.storage + +/** + * Algorithm-agnostic storage for symmetric keys. + * + * Symmetric stores return handles with a [public][Handle.public] value equal to the algorithm key + * and a [private][Handle.private] placeholder (typically [Unit]). + */ +@ExperimentalKeyStorageApi +public interface SymmetricStore { + /** Generate and persist a new key under [label]. Returns a handle with attributes. */ + public fun generate(label: ByteArray, access: AccessPolicy = AccessPolicy()): Handle + + /** Fetch an existing key by [label], or null if not found. */ + public fun get(label: ByteArray): Handle? + + /** Check existence by [label] without returning a handle. */ + public fun exists(label: ByteArray): Boolean + + /** Delete a key by [label]. Returns true if an item was removed. */ + public fun delete(label: ByteArray): Boolean +} diff --git a/cryptography-storage/src/commonTest/kotlin/dev/whyoleg/cryptography/storage/StorageApiSmokeTest.kt b/cryptography-storage/src/commonTest/kotlin/dev/whyoleg/cryptography/storage/StorageApiSmokeTest.kt new file mode 100644 index 00000000..dda9dec6 --- /dev/null +++ b/cryptography-storage/src/commonTest/kotlin/dev/whyoleg/cryptography/storage/StorageApiSmokeTest.kt @@ -0,0 +1,97 @@ +package dev.whyoleg.cryptography.storage + +import kotlin.test.* + +@OptIn(ExperimentalKeyStorageApi::class) +class StorageApiSmokeTest { + @Test + fun accessPolicy_defaults() { + val p = AccessPolicy() + assertFalse(p.requireUserPresence) + assertEquals(Accessibility.AfterFirstUnlock, p.accessibility) + assertEquals(DeviceBinding.None, p.deviceBinding) + assertFalse(p.exportablePrivate) + } + + @Test + fun asymmetric_store_basic_crud() { + class InMemoryAsym : AsymmetricStore { + private val map = mutableMapOf>() + override fun generate(label: ByteArray, access: AccessPolicy): Handle { + error("not implemented: provide generator in test") + } + + fun generateWith(label: ByteArray, access: AccessPolicy, generator: (String) -> Handle): Handle { + val k = label.decodeToString() + val h = generator(k) + map[k] = h + return h + } + + override fun get(label: ByteArray): Handle? = map[label.decodeToString()] + override fun exists(label: ByteArray): Boolean = map.containsKey(label.decodeToString()) + override fun delete(label: ByteArray): Boolean = map.remove(label.decodeToString()) != null + } + + val store = InMemoryAsym() + val label = "wallet-ed25519" + assertFalse(store.exists(label.encodeToByteArray())) + + val generated = store.generateWith(label.encodeToByteArray(), AccessPolicy()) { k -> + Handle( + public = "PUB:$k", + private = "PRIV:$k", + attributes = KeyAttributes(extractable = false, persistent = true, label = k.encodeToByteArray()) + ) + } + assertEquals("PUB:$label", generated.public) + assertTrue(store.exists(label.encodeToByteArray())) + + val fetched = store.get(label.encodeToByteArray()) + assertNotNull(fetched) + assertEquals(generated.public, fetched.public) + assertEquals(generated.private, fetched.private) + + assertTrue(store.delete(label.encodeToByteArray())) + assertFalse(store.exists(label.encodeToByteArray())) + assertNull(store.get(label.encodeToByteArray())) + } + + @Test + fun symmetric_store_basic_crud() { + class InMemorySym : SymmetricStore { + private val map = mutableMapOf>() + override fun generate(label: ByteArray, access: AccessPolicy): Handle { + error("not implemented: provide generator in test") + } + + fun generateWith(label: ByteArray, access: AccessPolicy, generator: (String) -> Handle): Handle { + val k = label.decodeToString() + val h = generator(k) + map[k] = h + return h + } + + override fun get(label: ByteArray): Handle? = map[label.decodeToString()] + override fun exists(label: ByteArray): Boolean = map.containsKey(label.decodeToString()) + override fun delete(label: ByteArray): Boolean = map.remove(label.decodeToString()) != null + } + + val store = InMemorySym() + val label = "aes-gcm-key" + assertFalse(store.exists(label.encodeToByteArray())) + val generated = store.generateWith(label.encodeToByteArray(), AccessPolicy()) { k -> + Handle( + public = "K:$k", + private = Unit, + attributes = KeyAttributes(extractable = false, persistent = true, label = k.encodeToByteArray()) + ) + } + assertEquals("K:$label", generated.public) + assertTrue(store.exists(label.encodeToByteArray())) + assertNotNull(store.get(label.encodeToByteArray())) + assertTrue(store.delete(label.encodeToByteArray())) + assertFalse(store.exists(label.encodeToByteArray())) + } +} + diff --git a/settings.gradle.kts b/settings.gradle.kts index 43670a5f..8567eccb 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -47,6 +47,8 @@ projects("cryptography-kotlin") { // providers API, high-level API module("cryptography-core") + // storage API (additive, experimental) + module("cryptography-storage") // providers folder("cryptography-providers", prefix = "cryptography-provider") { @@ -58,6 +60,7 @@ projects("cryptography-kotlin") { module("bc") // preconfigured JDK with BC provider } module("apple") + module("apple-keychain") module("webcrypto") folder("openssl3") { module("api")