From ceccae7accf94d8df6111eaa63e5fa1a228ebc03 Mon Sep 17 00:00:00 2001 From: Aria Wisp Date: Sat, 6 Sep 2025 21:47:26 -0600 Subject: [PATCH] storage: add experimental storage API module (KeyStore, AccessPolicy, KeyAttributes, Handle; stores; KDocs; tests; ABI) --- .../src/main/kotlin/ckbuild/Projects.kt | 1 + .../api/cryptography-storage.api | 104 +++++++++++++++ .../api/cryptography-storage.klib.api | 120 ++++++++++++++++++ cryptography-storage/build.gradle.kts | 16 +++ .../cryptography/storage/AccessPolicy.kt | 37 ++++++ .../cryptography/storage/AsymmetricStore.kt | 22 ++++ .../storage/ExperimentalKeyStorageApi.kt | 10 ++ .../whyoleg/cryptography/storage/Handle.kt | 14 ++ .../cryptography/storage/KeyAttributes.kt | 15 +++ .../whyoleg/cryptography/storage/KeyStore.kt | 47 +++++++ .../cryptography/storage/SymmetricStore.kt | 22 ++++ .../storage/StorageApiSmokeTest.kt | 97 ++++++++++++++ settings.gradle.kts | 2 + 13 files changed, 507 insertions(+) create mode 100644 cryptography-storage/api/cryptography-storage.api create mode 100644 cryptography-storage/api/cryptography-storage.klib.api create mode 100644 cryptography-storage/build.gradle.kts create mode 100644 cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/AccessPolicy.kt create mode 100644 cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/AsymmetricStore.kt create mode 100644 cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/ExperimentalKeyStorageApi.kt create mode 100644 cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/Handle.kt create mode 100644 cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/KeyAttributes.kt create mode 100644 cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/KeyStore.kt create mode 100644 cryptography-storage/src/commonMain/kotlin/dev/whyoleg/cryptography/storage/SymmetricStore.kt create mode 100644 cryptography-storage/src/commonTest/kotlin/dev/whyoleg/cryptography/storage/StorageApiSmokeTest.kt diff --git a/build-logic/src/main/kotlin/ckbuild/Projects.kt b/build-logic/src/main/kotlin/ckbuild/Projects.kt index 90e19589..e7ac3f13 100644 --- a/build-logic/src/main/kotlin/ckbuild/Projects.kt +++ b/build-logic/src/main/kotlin/ckbuild/Projects.kt @@ -22,6 +22,7 @@ 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), 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..d3584715 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") {