diff --git a/README.md b/README.md index de9eaeb..4e6abaf 100644 --- a/README.md +++ b/README.md @@ -97,60 +97,65 @@ let encodedTrifleSignedDataProto = try trifleSignedData.serialize() ```kotlin // Check if a key already exists. // If no key exists, generate a public key pair -var keyHandle = TrifleApi.generateKeyHandle(alias: abc) +var keyHandle: KeyHandle = TrifleApi.generateKeyHandle("abc") -// Storing keys. Keys are codable. -val encoder = Gson() -let jsonKeyHandle = try encoder.toJson(keyHandle) +// Storing keys. Key handles are serializable. +val encoded: ByteArray = keyHandle.serialize() // Load the key from storage when we need to use it +val decoded: KeyHandle = KeyHandle.deserialize(encoded) // Check the validity of loaded key -// TBD trifleApi.isValid(keyHandle: keyHandle) +val valid: Boolean = trifleApi.isValid(keyHandle) // Destroy key that is no longer in use or is invalid -// TBD trifleApi.delete(keyHandle: keyHandle) +trifleApi.delete(keyHandle) // Check if loaded key already has a cert. If yes, skip to checking for cert validity // Else if key does not have a cert OR if a new cert must be generated (eg because of existing // cert is already expired, or app needs to re-attest, app is re-installed, app is restored // from backup, ... etc) -// Create cert request -val certReq = TrifleApi.generateMobileCertificateRequest(entity = entity, keyHandle = keyHandle) +// Create cert request with an entity identity that is associated with the public key +val certReq: CertificateRequest = TrifleApi.generateMobileCertificateRequest(entity, keyHandle) -// Serialize to proto to be sent over wire +// Serialize as an opaque ByteArray +val encoded: ByteArray = certReq.serialize() -// Send certificate request to Certificate Authority endpoint. Response will be [Data] -// Iterate over each Data to convert to TrifleCertificate +// Send certificate request to Certificate Authority endpoint. Response will be List +val response: List -// certs is an array of certificates where [0] will be device certificate +// Iterate over each ByteArray to convert to Certificate +val certs: List = response.map { Certificate.deserialize(it) } + +// certs is a list of certificates where [0] will be device certificate // and the rest of the elements will be intermediate chain. // Check if app has the root cert of Certificate Authority (CA). // Validate cert matches the certificate request (so generated key) // and the root (so it has been generated by the right CA). -// TBD var isValid = TrifleApi.verify(csr, certs, anchor) +val result: Result = certs[0].verify(certReq, certs, root) +if (result.isFailure) { + failureHandler(result) +} +// otherwise, continue -// Once it passes validation, certReq is no longer needed and it can be deleted +// Once it passes validation, certReq is no longer needed, and it can be deleted // Store cert along with the respective keyHandle // To check only for the validity of a stored cert, you can do either of below choices // Option 1 is a more complete check of the device cert and the full chain -// TBD var isValid = TrifleApi.verify(certs) +val result: Result = cert.verify(ancestorCertificateChain = certs.drop(1)) // option 2 only checks the validity of the device cert -// TBD var isValid = TrifleApi.verify(certs) +val result: Result = cert.verify() // Sign the data -let trifleSignedData = TrifleApi.createSignedData( - dataThatIsSigned, - keyHandle, - certs ) +val signedData: SignedData = TrifleApi.createSignedData(clientData, keyHandle, certs) -// Serialize to proto to be sent over wire -val encodedTrifleSignedDataProto = trifleSignedData.serialize() +// Serialize as an opaque ByteArray +val encodedSignedData: ByteArray = signedData.serialize() ``` ## Key Lifecycle diff --git a/android/sample_app/src/main/java/app/cash/trifle/MainActivity.kt b/android/sample_app/src/main/java/app/cash/trifle/MainActivity.kt index f7d796e..7a832d6 100644 --- a/android/sample_app/src/main/java/app/cash/trifle/MainActivity.kt +++ b/android/sample_app/src/main/java/app/cash/trifle/MainActivity.kt @@ -14,7 +14,7 @@ class MainActivity : AppCompatActivity() { //Example Trifle use. Shouldn't typically be on the main UI thread, but that's the only reason // we're here so it's ok. - val keyHandle = KeyHandle.generateKeyHandle("alias") + val keyHandle = TrifleApi.generateKeyHandle("alias") setContentView(R.layout.activity_main) } diff --git a/android/trifle/build.gradle.kts b/android/trifle/build.gradle.kts index b595cb0..e044e9a 100644 --- a/android/trifle/build.gradle.kts +++ b/android/trifle/build.gradle.kts @@ -39,7 +39,7 @@ dependencies { testImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test.ext:junit:1.1.5") androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1") - androidTestImplementation("com.google.truth:truth:1.1.3") + androidTestImplementation("junit:junit:4.13.2") api(project(":jvm")) } diff --git a/android/trifle/src/androidTest/java/app/cash/trifle/KeyHandleTest.kt b/android/trifle/src/androidTest/java/app/cash/trifle/KeyHandleTest.kt index 16603e8..4162a3b 100644 --- a/android/trifle/src/androidTest/java/app/cash/trifle/KeyHandleTest.kt +++ b/android/trifle/src/androidTest/java/app/cash/trifle/KeyHandleTest.kt @@ -1,12 +1,27 @@ package app.cash.trifle -import com.google.common.truth.Truth.assertThat +import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows +import org.junit.Before import org.junit.Test + class KeyHandleTest { + private lateinit var keyHandle: KeyHandle + @Before fun setUp() { + keyHandle = TrifleApi.generateKeyHandle("test-alias") + } + @Test fun serializeDeserialize() { - val keyHandle = KeyHandle.generateKeyHandle("test-alias") val serializedBytes = keyHandle.serialize() - assertThat(KeyHandle.deserialize(serializedBytes)).isEqualTo(keyHandle) + assertEquals(KeyHandle.deserialize(serializedBytes), keyHandle) + } + + @Test fun failsDeserializationDueToDeletedKeyHandle() { + val serializedBytes = keyHandle.serialize() + KeyHandle.deleteAlias(keyHandle.alias) + assertThrows(IllegalStateException::class.java) { + KeyHandle.deserialize(serializedBytes) + } } } diff --git a/android/trifle/src/androidTest/java/app/cash/trifle/TrifleApiTest.kt b/android/trifle/src/androidTest/java/app/cash/trifle/TrifleApiTest.kt new file mode 100644 index 0000000..20b3a22 --- /dev/null +++ b/android/trifle/src/androidTest/java/app/cash/trifle/TrifleApiTest.kt @@ -0,0 +1,38 @@ +package app.cash.trifle + +import org.junit.Assert.assertFalse +import org.junit.Assert.assertNotNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.rules.ExpectedException + + +class TrifleApiTest { + @JvmField + @Rule + val thrown: ExpectedException = ExpectedException.none() + + private lateinit var keyHandle: KeyHandle + @Before fun setUp() { + keyHandle = TrifleApi.generateKeyHandle("test-alias") + } + + @Test fun testGenerateKeyHandle() { + assertNotNull(keyHandle) + } + + @Test fun testIsValid_forCreatedKeyHandle_returnsTrue() { + assertTrue(TrifleApi.isValid(keyHandle)) + } + + @Test fun testIsValid_forDeletedKeyHandle_returnsFalse() { + TrifleApi.delete(keyHandle) + assertFalse(TrifleApi.isValid(keyHandle)) + } + + @Test fun testDeleteKeyHandle() { + TrifleApi.delete(keyHandle) + } +} diff --git a/android/trifle/src/main/java/app/cash/trifle/KeyHandle.kt b/android/trifle/src/main/java/app/cash/trifle/KeyHandle.kt index 1bd453a..b3e0136 100644 --- a/android/trifle/src/main/java/app/cash/trifle/KeyHandle.kt +++ b/android/trifle/src/main/java/app/cash/trifle/KeyHandle.kt @@ -8,18 +8,9 @@ import java.security.KeyPairGenerator import java.security.KeyStore import java.security.spec.ECGenParameterSpec -class KeyHandle internal constructor(private val alias: String) { - // Throw an illegal state exception if we can't get hold of the proper key material. This - // *should never happen* since the only way to obtain a KeyHandle is to deserialize one, which - // should have already checked for this, or to generate a new one. - private val exceptionMessage = - "Android KeyStore does not contain a keypair corresponding to the $alias alias" - private val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE_TYPE).apply { - load(null) - } - +data class KeyHandle internal constructor(val alias: String) { init { - if (!keyStore.containsAlias(alias)) { + if (!containsAlias(alias)) { // Need to generate a new key for this key alias in the keystore. val kpg: KeyPairGenerator = KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_EC, @@ -36,15 +27,15 @@ class KeyHandle internal constructor(private val alias: String) { } kpg.initialize(parameterSpec) - - val kp = kpg.generateKeyPair() + kpg.generateKeyPair() Log.i("TRIFLE", "Created KeyHandle with alias $alias") } } internal val keyPair: KeyPair by lazy { + val exceptionMessage = String.format(EXCEPTION_MSG, alias) try { - val entry: KeyStore.Entry = keyStore.getEntry(alias, null) + val entry: KeyStore.Entry = KEY_STORE.getEntry(alias, null) if (entry is KeyStore.PrivateKeyEntry) { return@lazy KeyPair(entry.certificate.publicKey, entry.privateKey) } @@ -56,32 +47,32 @@ class KeyHandle internal constructor(private val alias: String) { fun serialize(): ByteArray = alias.toByteArray(Charsets.UTF_8) - override fun equals(other: Any?): Boolean { - val other = other as? KeyHandle ?: return false - return alias == other.alias - } - - override fun hashCode(): Int = alias.hashCode() - companion object { + // Throw an illegal state exception if we can't get hold of the proper key material. This + // *should never happen* since the only way to obtain a KeyHandle is to deserialize one, which + // should have already checked for this, or to generate a new one. + private const val EXCEPTION_MSG = + "Android KeyStore does not contain a keypair corresponding to the %s alias" private const val ANDROID_KEYSTORE_TYPE: String = "AndroidKeyStore" + private val KEY_STORE = KeyStore.getInstance(ANDROID_KEYSTORE_TYPE).apply { + load(null) + } fun deserialize(bytes: ByteArray): KeyHandle { val alias = bytes.toString(Charsets.UTF_8) - val ks = KeyStore.getInstance(ANDROID_KEYSTORE_TYPE).apply { - load(null) - } - if (!ks.containsAlias(alias)) { - throw IllegalArgumentException( - "Android KeyStore does not contain a keypair corresponding to the $alias alias" - ) + if (!containsAlias(alias)) { + throw IllegalStateException(String.format(EXCEPTION_MSG, alias)) } return KeyHandle(alias) } //TODO(dcashman): Consoidate API surface with iOS surface. - fun generateKeyHandle(alias: String): KeyHandle { - return KeyHandle(alias) + internal fun generateKeyHandle(alias: String): KeyHandle = KeyHandle(alias) + + internal fun containsAlias(alias: String): Boolean = KEY_STORE.containsAlias(alias) + + internal fun deleteAlias(alias: String) { + if (containsAlias(alias)) KEY_STORE.deleteEntry(alias) } } } diff --git a/android/trifle/src/main/java/app/cash/trifle/TrifleApi.kt b/android/trifle/src/main/java/app/cash/trifle/TrifleApi.kt index 8eadac2..710873e 100644 --- a/android/trifle/src/main/java/app/cash/trifle/TrifleApi.kt +++ b/android/trifle/src/main/java/app/cash/trifle/TrifleApi.kt @@ -13,6 +13,22 @@ object TrifleApi { */ fun generateKeyHandle(alias: String): KeyHandle = KeyHandle.generateKeyHandle(alias) + /** + * [KeyHandle] is valid iff [KeyHandle.alias] exists in the Key Store. + * + * @param keyHandle - key handle that is to be validated. + * + * @returns - boolean value for validity + */ + fun isValid(keyHandle: KeyHandle): Boolean = KeyHandle.containsAlias(keyHandle.alias) + + /** + * Deletes the [KeyHandle] from the Key Store. + * + * @param keyHandle - key handle that is to be deleted. + */ + fun delete(keyHandle: KeyHandle) = KeyHandle.deleteAlias(keyHandle.alias) + /** * Generate a Trifle [CertificateRequest], signed by the provided * keyHandle, that can be presented to the Certificate Authority (CA) for