Skip to content

Commit

Permalink
Merge pull request #167 from cashapp/alvin/trifle-android-uuid
Browse files Browse the repository at this point in the history
Generate tag for keyhandles instead of parameterization of aliases
  • Loading branch information
alvinsee authored Sep 28, 2023
2 parents de478de + f2daecc commit 1ed4b04
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 49 deletions.
15 changes: 9 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -136,16 +139,16 @@ val certs: List<Certificate> = 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<Unit> = TrifleApi.verifyChain(certs, root)
val result: Result<Unit> = 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<Unit> = TrifleApi.verifyChain(certs)
val result: Result<Unit> = trifleApi.verifyChain(certs)

// option 2 only checks the validity of the device cert
val result: Result<Unit> = TrifleApi.verifyValidity(cert)
val result: Result<Unit> = trifleApi.verifyValidity(cert)

// Error handling from the Result.isFailure can be found in the enumeration in TrifleErrors
if (result.isFailure) {
Expand All @@ -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()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,53 +15,57 @@ 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)
}

@Test
fun testVerifyCertificateValidity_succeeds() {
val result = TrifleApi.verifyValidity(
val result = trifleApi.verifyValidity(
endEntity.certificate
)
assertTrue(result.isSuccess)
}

@Test
fun testVerifyAttributes_succeeds() {
val result = TrifleApi.verifyCertRequestResponse(
val result = trifleApi.verifyCertRequestResponse(
endEntity.certificate,
endEntity.certRequest
)
Expand All @@ -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)
Expand All @@ -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)))
)
Expand All @@ -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)))
)
Expand All @@ -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)))
)
Expand All @@ -109,7 +113,7 @@ class TrifleApiTest {

@Test
fun testVerifyAttributes_failsWithCSRMismatch() {
val result = TrifleApi.verifyCertRequestResponse(
val result = trifleApi.verifyCertRequestResponse(
endEntity.certificate,
otherEndEntity.certRequest
)
Expand Down
38 changes: 20 additions & 18 deletions android/trifle/src/main/java/app/cash/trifle/KeyHandle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand All @@ -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)
}
Expand All @@ -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)
}
}
}
13 changes: 6 additions & 7 deletions android/trifle/src/main/java/app/cash/trifle/TrifleApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 1ed4b04

Please sign in to comment.