diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml
new file mode 100644
index 0000000..825f058
--- /dev/null
+++ b/.idea/deploymentTargetSelector.xml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/WalletSdk/build.gradle.kts b/WalletSdk/build.gradle.kts
index f9d3630..af1dc82 100644
--- a/WalletSdk/build.gradle.kts
+++ b/WalletSdk/build.gradle.kts
@@ -76,7 +76,7 @@ nmcp {
android {
- namespace = "com.spruceid.walletsdk"
+ namespace = "com.spruceid.wallet.sdk"
compileSdk = 33
defaultConfig {
@@ -128,8 +128,10 @@ dependencies {
implementation("androidx.camera:camera-view:1.3.2")
implementation("com.google.zxing:core:3.3.3")
implementation("com.google.accompanist:accompanist-permissions:0.34.0")
+ implementation("androidx.test.ext:junit-ktx:1.1.5")
/* End UI dependencies */
testImplementation("junit:junit:4.13.2")
+
androidTestImplementation("com.android.support.test:runner:1.0.2")
androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2")
}
diff --git a/WalletSdk/src/androidTest/java/com/spruceid/walletsdk/ExampleInstrumentedTest.kt b/WalletSdk/src/androidTest/java/com/spruceid/walletsdk/ExampleInstrumentedTest.kt
deleted file mode 100644
index e4a5f6f..0000000
--- a/WalletSdk/src/androidTest/java/com/spruceid/walletsdk/ExampleInstrumentedTest.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package com.spruceid.walletsdk
-
-import androidx.test.InstrumentationRegistry
-import androidx.test.runner.AndroidJUnit4
-
-import org.junit.Test
-import org.junit.runner.RunWith
-
-import org.junit.Assert.*
-
-/**
- * Instrumented test, which will execute on an Android device.
- *
- * See [testing documentation](http://d.android.com/tools/testing).
- */
-@RunWith(AndroidJUnit4::class)
-class ExampleInstrumentedTest {
- @Test
- fun useAppContext() {
- // Context of the app under test.
- val appContext = InstrumentationRegistry.getInstrumentation().targetContext
- assertEquals("com.spruceid.wallet.sdk.test", appContext.packageName)
- }
-}
\ No newline at end of file
diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/KeyManager.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/KeyManager.kt
new file mode 100644
index 0000000..d71afc7
--- /dev/null
+++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/KeyManager.kt
@@ -0,0 +1,314 @@
+package com.spruceid.wallet.sdk
+
+import android.os.Build
+import android.security.keystore.KeyGenParameterSpec
+import android.security.keystore.KeyProperties
+import android.util.Base64
+import android.util.Log
+import androidx.annotation.RequiresApi
+import java.security.KeyPairGenerator
+import java.security.KeyStore
+import java.security.Signature
+import java.security.interfaces.ECPublicKey
+import java.security.spec.ECGenParameterSpec
+import javax.crypto.Cipher
+import javax.crypto.KeyGenerator
+import javax.crypto.SecretKey
+import javax.crypto.spec.GCMParameterSpec
+
+/**
+ * Implementation of the secure key management with Strongbox and TEE as backup.
+ */
+class KeyManager {
+
+ /**
+ * Returns the Android Keystore.
+ * @return instance of the key store.
+ */
+ private fun getKeyStore(): KeyStore {
+ return KeyStore.getInstance("AndroidKeyStore").apply {
+ load(null)
+ }
+ }
+
+ /**
+ * Returns a secret key - based on the id of the key.
+ * @property id of the secret key.
+ * @return the public secret key interface object.
+ */
+ private fun getSecretKey(id: String): SecretKey? {
+ val ks = getKeyStore()
+
+ val entry: KeyStore.Entry = ks.getEntry(id, null)
+ if (entry !is KeyStore.SecretKeyEntry) {
+ Log.w("KEYMAN", "Not an instance of a SecretKeyEntry")
+ return null
+ }
+
+ return entry.secretKey
+ }
+
+ /**
+ * Resets the Keystore by removing all of the keys.
+ */
+ fun reset() {
+ val ks = getKeyStore()
+ ks.aliases().iterator().forEach {
+ ks.deleteEntry(it)
+ }
+ }
+
+ /**
+ * Generates a secp256r1 signing key by id/alias in the Keystore with Strongbox when
+ * min SDK and hardware requirements are met, otherwise using TEE.
+ * @property id of the secret key.
+ * @returns KeyManagerEnvironment indicating the environment used to generate the key.
+ */
+ fun generateSigningKey(id: String): KeyManagerEnvironment {
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ generateSigningKeyWithStrongbox(id)
+
+ return KeyManagerEnvironment.Strongbox
+ } else {
+ generateSigningKeyTEE(id)
+
+ return KeyManagerEnvironment.TEE
+ }
+ } catch (e: Exception) {
+ generateSigningKeyTEE(id)
+
+ return KeyManagerEnvironment.TEE
+ }
+ }
+
+ /**
+ * Generates a secp256r1 signing key by id/alias in the Keystore with Strongbox.
+ * @property id of the secret key.
+ */
+ @RequiresApi(Build.VERSION_CODES.P)
+ private fun generateSigningKeyWithStrongbox(id: String) {
+ val generator = KeyPairGenerator.getInstance(
+ KeyProperties.KEY_ALGORITHM_EC,
+ "AndroidKeyStore",
+ )
+
+ val spec = KeyGenParameterSpec.Builder(
+ id,
+ KeyProperties.PURPOSE_SIGN
+ or KeyProperties.PURPOSE_VERIFY,
+ )
+ .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
+ .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
+ .setIsStrongBoxBacked(true)
+ .build()
+
+ generator.initialize(spec)
+ generator.generateKeyPair()
+ }
+
+ /**
+ * Generates a secp256r1 signing key by id/alias in the Keystore with TEE.
+ * @property id of the secret key.
+ */
+ private fun generateSigningKeyTEE(id: String) {
+ val generator = KeyPairGenerator.getInstance(
+ KeyProperties.KEY_ALGORITHM_EC,
+ "AndroidKeyStore",
+ )
+
+ val spec = KeyGenParameterSpec.Builder(
+ id,
+ KeyProperties.PURPOSE_SIGN
+ or KeyProperties.PURPOSE_VERIFY,
+ )
+ .setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA512)
+ .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
+ .build()
+
+ generator.initialize(spec)
+ generator.generateKeyPair()
+ }
+
+ /**
+ * Assumes the value above 32 will always be 33.
+ * BigInteger will add an extra byte to keep the number positive.
+ * But the key values will always be 32 bytes.
+ * @property input byte array to be processed.
+ * @return byte array with 32 bytes.
+ */
+ fun clampOrFill(input: ByteArray): ByteArray {
+ return if (input.size > 32) {
+ input.drop(1).toByteArray()
+ } else if (input.size < 32) {
+ List(32 - input.size){ 0.toByte() }.toByteArray() + input
+ } else {
+ input
+ }
+ }
+
+ /**
+ * Returns a JWK for a particular secret key by key id.
+ * @property id of the secret key.
+ * @return the JWK as a string.
+ */
+ fun getJwk(id: String): String? {
+ val ks = getKeyStore()
+ val key = ks.getEntry(id, null)
+
+ if (key is KeyStore.PrivateKeyEntry) {
+ if (key.certificate.publicKey is ECPublicKey) {
+ val ecPublicKey = key.certificate.publicKey as ECPublicKey
+ val x = Base64.encodeToString(
+ clampOrFill(ecPublicKey.w.affineX.toByteArray()),
+ Base64.URL_SAFE
+ xor Base64.NO_PADDING
+ xor Base64.NO_WRAP
+ )
+ val y = Base64.encodeToString(
+ clampOrFill(ecPublicKey.w.affineY.toByteArray()),
+ Base64.URL_SAFE
+ xor Base64.NO_PADDING
+ xor Base64.NO_WRAP
+ )
+
+ return """{"kty":"EC","crv":"P-256","x":"$x","y":"$y"}"""
+ }
+ }
+
+ return null
+ }
+
+ /**
+ * Checks to see if a key already exists.
+ * @property id of the secret key.
+ * @return indication if the key exists.
+ */
+ fun keyExists(id: String): Boolean {
+ val ks = getKeyStore()
+ return ks.containsAlias(id) && ks.isKeyEntry(id)
+ }
+
+ /**
+ * Signs the provided payload with a SHA256withECDSA private key.
+ * @property id of the secret key.
+ * @property payload to be signed.
+ * @return the signed payload.
+ */
+ fun signPayload(id: String, payload: ByteArray): ByteArray? {
+ val ks = getKeyStore()
+ val entry: KeyStore.Entry = ks.getEntry(id, null)
+ if (entry !is KeyStore.PrivateKeyEntry) {
+ Log.w("KEYMAN", "Not an instance of a PrivateKeyEntry")
+ return null
+ }
+
+ return Signature.getInstance("SHA256withECDSA").run {
+ initSign(entry.privateKey)
+ update(payload)
+ sign()
+ }
+ }
+
+ /**
+ * Generates an AES encryption key with a provided id in the Keystore.
+ * @property id of the secret key.
+ * @returns KeyManagerEnvironment indicating the environment used to generate the key.
+ */
+ fun generateEncryptionKey(id: String): KeyManagerEnvironment {
+ try {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+ generateEncryptionKeyWithStrongbox(id)
+
+ return KeyManagerEnvironment.Strongbox
+ } else {
+ generateEncryptionKeyWithTEE(id)
+
+ return KeyManagerEnvironment.TEE
+ }
+ } catch (e: Exception) {
+ generateEncryptionKeyWithTEE(id)
+
+ return KeyManagerEnvironment.TEE
+ }
+ }
+
+ /**
+ * Generates an AES encryption key with a provided id in the Keystore.
+ * @property id of the secret key.
+ */
+ @RequiresApi(Build.VERSION_CODES.P)
+ private fun generateEncryptionKeyWithStrongbox(id: String) {
+ val generator = KeyGenerator.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES,
+ "AndroidKeyStore",
+ )
+
+ val spec = KeyGenParameterSpec.Builder(
+ id,
+ KeyProperties.PURPOSE_ENCRYPT
+ or KeyProperties.PURPOSE_DECRYPT,
+ )
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .setIsStrongBoxBacked(true)
+ .build()
+
+ generator.init(spec)
+ generator.generateKey()
+ }
+
+ /**
+ * Generates an AES encryption key with a provided id in the Keystore.
+ * @property id of the secret key.
+ */
+ private fun generateEncryptionKeyWithTEE(id: String) {
+ val generator = KeyGenerator.getInstance(
+ KeyProperties.KEY_ALGORITHM_AES,
+ "AndroidKeyStore",
+ )
+
+ val spec = KeyGenParameterSpec.Builder(
+ id,
+ KeyProperties.PURPOSE_ENCRYPT
+ or KeyProperties.PURPOSE_DECRYPT,
+ )
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
+ .build()
+
+ generator.init(spec)
+ generator.generateKey()
+ }
+
+ /**
+ * Encrypts payload by a key referenced by key id.
+ * @property id of the secret key.
+ * @property payload to be encrypted.
+ * @return initialization vector with the encrypted payload.
+ */
+ fun encryptPayload(id: String, payload: ByteArray): Pair {
+ val secretKey = getSecretKey(id)
+
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
+ cipher.init(Cipher.ENCRYPT_MODE, secretKey)
+ val iv = cipher.iv
+ val encrypted = cipher.doFinal(payload)
+ return Pair(iv, encrypted)
+ }
+
+ /**
+ * Decrypts the provided payload by a key id and initialization vector.
+ * @property id of the secret key.
+ * @property iv initialization vector.
+ * @property payload to be encrypted.
+ * @return the decrypted payload.
+ */
+ fun decryptPayload(id: String, iv: ByteArray, payload: ByteArray): ByteArray? {
+ val secretKey = getSecretKey(id)
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
+ val spec = GCMParameterSpec(128, iv)
+ cipher.init(Cipher.DECRYPT_MODE, secretKey, spec)
+ return cipher.doFinal(payload)
+ }
+}
diff --git a/WalletSdk/src/main/java/com/spruceid/wallet/sdk/KeyManagerEnvironment.kt b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/KeyManagerEnvironment.kt
new file mode 100644
index 0000000..50dbd93
--- /dev/null
+++ b/WalletSdk/src/main/java/com/spruceid/wallet/sdk/KeyManagerEnvironment.kt
@@ -0,0 +1,9 @@
+package com.spruceid.wallet.sdk
+
+/**
+ * The Keystore environment used for the key generation.
+ */
+enum class KeyManagerEnvironment(val string: String) {
+ TEE("tee"),
+ Strongbox("strongbox"),
+}
\ No newline at end of file
diff --git a/WalletSdk/src/test/java/com/spruceid/wallet/sdk/KeyManagerTest.kt b/WalletSdk/src/test/java/com/spruceid/wallet/sdk/KeyManagerTest.kt
new file mode 100644
index 0000000..0d483eb
--- /dev/null
+++ b/WalletSdk/src/test/java/com/spruceid/wallet/sdk/KeyManagerTest.kt
@@ -0,0 +1,38 @@
+package com.spruceid.wallet.sdk
+
+import org.junit.Test
+
+import org.junit.Assert.*
+
+/**
+ * Tests for KeyManager supporting functions.
+ *
+ * See [testing documentation](http://d.android.com/tools/testing).
+ */
+class KeyManagerTest {
+
+ @Test
+ fun clampOrFill() {
+ val keyManager = KeyManager()
+
+ // Greater than 32
+ val inputMoreThan = ByteArray(33) { it.toByte() }
+ val expectedMoreThan = inputMoreThan.drop(1).toByteArray()
+ val resultMoreThan = keyManager.clampOrFill(inputMoreThan)
+
+ assertArrayEquals(expectedMoreThan, resultMoreThan)
+
+ // Less than 32.
+ val inputLessThan = ByteArray(30) { it.toByte() }
+ val expectedLessThan = ByteArray(2) { 0.toByte() } + inputLessThan
+ val result = keyManager.clampOrFill(inputLessThan)
+
+ assertArrayEquals(expectedLessThan, result)
+
+ // Equal to 32.
+ val inputEqualTo = ByteArray(32) { it.toByte() }
+ val resultEqualTo = keyManager.clampOrFill(inputEqualTo)
+
+ assertArrayEquals(inputEqualTo, resultEqualTo)
+ }
+}
diff --git a/example/build.gradle.kts b/example/build.gradle.kts
index c4e3eb3..cdf423b 100644
--- a/example/build.gradle.kts
+++ b/example/build.gradle.kts
@@ -50,7 +50,6 @@ android {
}
dependencies {
-
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
implementation("androidx.activity:activity-compose:1.8.2")