diff --git a/README.md b/README.md index 7df7cb9..70f678f 100644 --- a/README.md +++ b/README.md @@ -97,9 +97,12 @@ let encodedTrifleSignedDataProto = try trifleSignedData.serialize() ## Android SDK Usage ```kotlin +// App start up +val trifleApi = TrifleApi(reverseDomain = "abc") + // Check if a key already exists. // If no key exists, generate a public key pair -var keyHandle: KeyHandle = TrifleApi.generateKeyHandle("abc") +var keyHandle: KeyHandle = trifleApi.generateKeyHandle() // Storing keys. Key handles are serializable. val encoded: ByteArray = keyHandle.serialize() @@ -120,7 +123,7 @@ trifleApi.delete(keyHandle) // Create cert request with an entity identity that is associated with the public key // Once you receive the certificate, you can delete the cert request. -val certReq: CertificateRequest = TrifleApi.generateMobileCertificateRequest(entity, keyHandle) +val certReq: CertificateRequest = trifleApi.generateMobileCertificateRequest(entity, keyHandle) // Serialize as an opaque ByteArray val encoded: ByteArray = certReq.serialize() @@ -136,16 +139,16 @@ val certs: List = response.map { Certificate.deserialize(it) } // Check if app has the root cert of Certificate Authority (CA). // Verify that the cert chain validates OK (i.e., it has been generated by the right CA). -val result: Result = TrifleApi.verifyChain(certs, root) +val result: Result = trifleApi.verifyChain(certs, root) // 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 -val result: Result = TrifleApi.verifyChain(certs) +val result: Result = trifleApi.verifyChain(certs) // option 2 only checks the validity of the device cert -val result: Result = TrifleApi.verifyValidity(cert) +val result: Result = trifleApi.verifyValidity(cert) // Error handling from the Result.isFailure can be found in the enumeration in TrifleErrors if (result.isFailure) { @@ -160,7 +163,7 @@ if (result.isFailure) { } // Sign the data -val signedData: SignedData = TrifleApi.createSignedData(clientData, keyHandle, certs) +val signedData: SignedData = trifleApi.createSignedData(clientData, keyHandle, certs) // Serialize as an opaque ByteArray val encodedSignedData: ByteArray = signedData.serialize() 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 7a832d6..b3fb121 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 @@ -12,9 +12,9 @@ class MainActivity : AppCompatActivity() { val version = LibraryVersion() Log.v(TAG, "Trifle version: $version") - //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 = TrifleApi.generateKeyHandle("alias") + //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 = TrifleApi("app.cash.trifle.keys").generateKeyHandle() setContentView(R.layout.activity_main) } 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 4162a3b..b4509e9 100644 --- a/android/trifle/src/androidTest/java/app/cash/trifle/KeyHandleTest.kt +++ b/android/trifle/src/androidTest/java/app/cash/trifle/KeyHandleTest.kt @@ -9,7 +9,7 @@ import org.junit.Test class KeyHandleTest { private lateinit var keyHandle: KeyHandle @Before fun setUp() { - keyHandle = TrifleApi.generateKeyHandle("test-alias") + keyHandle = TrifleApi("app.cash.trifle.keys").generateKeyHandle() } @Test fun serializeDeserialize() { @@ -19,7 +19,7 @@ class KeyHandleTest { @Test fun failsDeserializationDueToDeletedKeyHandle() { val serializedBytes = keyHandle.serialize() - KeyHandle.deleteAlias(keyHandle.alias) + KeyHandle.deleteTag(keyHandle.tag) 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 index 58b5c0e..9b9d913 100644 --- a/android/trifle/src/androidTest/java/app/cash/trifle/TrifleApiTest.kt +++ b/android/trifle/src/androidTest/java/app/cash/trifle/TrifleApiTest.kt @@ -15,37 +15,41 @@ import org.junit.rules.ExpectedException import java.time.Duration import java.time.Instant import java.util.Date +import java.util.UUID class TrifleApiTest { @JvmField @Rule val thrown: ExpectedException = ExpectedException.none() + private val trifleApi = TrifleApi("app.cash.trifle.keys") private lateinit var keyHandle: KeyHandle @Before fun setUp() { - keyHandle = TrifleApi.generateKeyHandle("test-alias") + keyHandle = trifleApi.generateKeyHandle() } @Test fun testGenerateKeyHandle() { assertNotNull(keyHandle) + val uuid = keyHandle.tag.substringAfterLast('.') + assertNotNull(UUID.fromString(uuid)) } @Test fun testIsValid_forCreatedKeyHandle_returnsTrue() { - assertTrue(TrifleApi.isValid(keyHandle)) + assertTrue(trifleApi.isValid(keyHandle)) } @Test fun testIsValid_forDeletedKeyHandle_returnsFalse() { - TrifleApi.delete(keyHandle) - assertFalse(TrifleApi.isValid(keyHandle)) + trifleApi.delete(keyHandle) + assertFalse(trifleApi.isValid(keyHandle)) } @Test fun testDeleteKeyHandle() { - TrifleApi.delete(keyHandle) + trifleApi.delete(keyHandle) } @Test fun testVerify_succeeds() { - val result = TrifleApi.verifyChain( + val result = trifleApi.verifyChain( certificateChain = endEntity.certChain ) assertTrue(result.isSuccess) @@ -53,7 +57,7 @@ class TrifleApiTest { @Test fun testVerifyCertificateValidity_succeeds() { - val result = TrifleApi.verifyValidity( + val result = trifleApi.verifyValidity( endEntity.certificate ) assertTrue(result.isSuccess) @@ -61,7 +65,7 @@ class TrifleApiTest { @Test fun testVerifyAttributes_succeeds() { - val result = TrifleApi.verifyCertRequestResponse( + val result = trifleApi.verifyCertRequestResponse( endEntity.certificate, endEntity.certRequest ) @@ -70,7 +74,7 @@ class TrifleApiTest { @Test fun testVerify_failsWithNoTrustAnchorForADifferentRootCertificate() { - val result = TrifleApi.verifyChain( + val result = trifleApi.verifyChain( certificateChain = listOf(endEntity.certificate) + otherEndEntity.certChain.drop(1) ) assertTrue(result.isFailure) @@ -79,7 +83,7 @@ class TrifleApiTest { @Test fun testVerify_failsWithExpiredCertificateForAnExpiredCertificate() { - val result = TrifleApi.verifyChain( + val result = trifleApi.verifyChain( certificateChain = endEntity.certChain, date = Date.from(Instant.now().plus(Duration.ofDays(365))) ) @@ -89,7 +93,7 @@ class TrifleApiTest { @Test fun testVerify_failsWithExpiredCertificateForAnExpiredStoredCertificate() { - val result = TrifleApi.verifyValidity( + val result = trifleApi.verifyValidity( endEntity.certificate, Date.from(Instant.now().plus(Duration.ofDays(365))) ) @@ -99,7 +103,7 @@ class TrifleApiTest { @Test fun testVerify_failsWithNotYetValidCertificateForAStoredCertificateYetToBeValid() { - val result = TrifleApi.verifyValidity( + val result = trifleApi.verifyValidity( endEntity.certificate, date = Date.from(Instant.now().minus(Duration.ofDays(1))) ) @@ -109,7 +113,7 @@ class TrifleApiTest { @Test fun testVerifyAttributes_failsWithCSRMismatch() { - val result = TrifleApi.verifyCertRequestResponse( + val result = trifleApi.verifyCertRequestResponse( endEntity.certificate, otherEndEntity.certRequest ) 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 b3e0136..d25fcc6 100644 --- a/android/trifle/src/main/java/app/cash/trifle/KeyHandle.kt +++ b/android/trifle/src/main/java/app/cash/trifle/KeyHandle.kt @@ -7,17 +7,18 @@ import java.security.KeyPair import java.security.KeyPairGenerator import java.security.KeyStore import java.security.spec.ECGenParameterSpec +import java.util.UUID -data class KeyHandle internal constructor(val alias: String) { +data class KeyHandle internal constructor(val tag: String) { init { - if (!containsAlias(alias)) { - // Need to generate a new key for this key alias in the keystore. + if (!containsTag(tag)) { + // Need to generate a new key for the generated tag in the keystore. val kpg: KeyPairGenerator = KeyPairGenerator.getInstance( KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE_TYPE ) val parameterSpec: KeyGenParameterSpec = KeyGenParameterSpec.Builder( - alias, + tag, KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY ).run { setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1")) @@ -28,14 +29,14 @@ data class KeyHandle internal constructor(val alias: String) { kpg.initialize(parameterSpec) kpg.generateKeyPair() - Log.i("TRIFLE", "Created KeyHandle with alias $alias") + Log.i("TRIFLE", "Created KeyHandle with tag $tag") } } internal val keyPair: KeyPair by lazy { - val exceptionMessage = String.format(EXCEPTION_MSG, alias) + val exceptionMessage = String.format(EXCEPTION_MSG, tag) try { - val entry: KeyStore.Entry = KEY_STORE.getEntry(alias, null) + val entry: KeyStore.Entry = KEY_STORE.getEntry(tag, null) if (entry is KeyStore.PrivateKeyEntry) { return@lazy KeyPair(entry.certificate.publicKey, entry.privateKey) } @@ -45,34 +46,35 @@ data class KeyHandle internal constructor(val alias: String) { throw IllegalStateException(exceptionMessage) } - fun serialize(): ByteArray = alias.toByteArray(Charsets.UTF_8) + fun serialize(): ByteArray = tag.toByteArray(Charsets.UTF_8) 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" + "Android KeyStore does not contain a keypair corresponding to the %s tag" 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) - if (!containsAlias(alias)) { - throw IllegalStateException(String.format(EXCEPTION_MSG, alias)) + val tag = bytes.toString(Charsets.UTF_8) + if (!containsTag(tag)) { + throw IllegalStateException(String.format(EXCEPTION_MSG, tag)) } - return KeyHandle(alias) + return KeyHandle(tag) } - //TODO(dcashman): Consoidate API surface with iOS surface. - internal fun generateKeyHandle(alias: String): KeyHandle = KeyHandle(alias) + //TODO(dcashman): Consolidate API surface with iOS surface. + internal fun generateKeyHandle(reverseDomain: String): KeyHandle = + KeyHandle("$reverseDomain.sign.${UUID.randomUUID()}") - internal fun containsAlias(alias: String): Boolean = KEY_STORE.containsAlias(alias) + internal fun containsTag(tag: String): Boolean = KEY_STORE.containsAlias(tag) - internal fun deleteAlias(alias: String) { - if (containsAlias(alias)) KEY_STORE.deleteEntry(alias) + internal fun deleteTag(tag: String) { + if (containsTag(tag)) KEY_STORE.deleteEntry(tag) } } } 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 88ef0c7..d44cbf6 100644 --- a/android/trifle/src/main/java/app/cash/trifle/TrifleApi.kt +++ b/android/trifle/src/main/java/app/cash/trifle/TrifleApi.kt @@ -4,34 +4,33 @@ import app.cash.trifle.validators.CertChainValidatorFactory import app.cash.trifle.validators.CertificateValidatorFactory import java.util.Date -object TrifleApi { +class TrifleApi(private val reverseDomain: String) { + /** * Create a new mobile Trifle keypair for which can be used to create a * certificate request and to sign messages. The library (Trifle) will * automatically try to choose the best algorithm and key type available on * this device. * - * @param alias - key alias used for identifying a keyhandle. - * * @return An opaque Trifle representation [KeyHandle] of the key-pair, which the client will need to store. */ - fun generateKeyHandle(alias: String): KeyHandle = KeyHandle.generateKeyHandle(alias) + fun generateKeyHandle(): KeyHandle = KeyHandle.generateKeyHandle(reverseDomain) /** - * [KeyHandle] is valid iff [KeyHandle.alias] exists in the Key Store. + * [KeyHandle] is valid iff [KeyHandle.tag] 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) + fun isValid(keyHandle: KeyHandle): Boolean = KeyHandle.containsTag(keyHandle.tag) /** * Deletes the [KeyHandle] from the Key Store. * * @param keyHandle - key handle that is to be deleted. */ - fun delete(keyHandle: KeyHandle) = KeyHandle.deleteAlias(keyHandle.alias) + fun delete(keyHandle: KeyHandle) = KeyHandle.deleteTag(keyHandle.tag) /** * Generate a Trifle [CertificateRequest], signed by the provided