Skip to content

Commit

Permalink
Add key handle helper methods to android api
Browse files Browse the repository at this point in the history
  • Loading branch information
alvinsee committed Aug 24, 2023
1 parent ecb65ee commit 362a87e
Show file tree
Hide file tree
Showing 7 changed files with 122 additions and 57 deletions.
49 changes: 27 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<ByteArray>
val response: List<ByteArray>

// certs is an array of certificates where [0] will be device certificate
// Iterate over each ByteArray to convert to Certificate
val certs: List<Certificate> = 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<Unit> = 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<Unit> = 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<Unit> = 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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion android/trifle/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
}
}
51 changes: 21 additions & 30 deletions android/trifle/src/main/java/app/cash/trifle/KeyHandle.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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)
}
Expand All @@ -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)
}
}
}
16 changes: 16 additions & 0 deletions android/trifle/src/main/java/app/cash/trifle/TrifleApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit 362a87e

Please sign in to comment.