I tried using the secure key import feature of AndroidKeyStore, following the example at:
- https://android.googlesource.com/platform/cts/+/master/tests/tests/keystore/src/android/keystore/cts/ImportWrappedKeyTest.java
When I want to use the imported private key afterwards, calling androidKeyStore.getKey(importedKeyAlias, null)
, I get the following error:
java.security.UnrecoverableKeyException: Failed to obtain X.509 form of public key. Keystore has no public certificate stored.
I tried manually adding a certificate using androidKeyStore.setCertificateEntry(importedKeyAlias, generateCertificate(...))
, but this call results in the error message
java.security.KeyStoreException: Entry exists and is not a trusted certificate.
I am executing the code on a Samsung Galaxy A34 5G
, hence missing the StrongBox hardware, but according to my understanding this should still work with isStrongBox = false
.
Question: How can I use the imported key?
Maybe there is an error in my code, so here is a MWE of the first approach, without trying to manually add a certificate.
fun minimumWorkingExample(isStrongBox: Boolean) {
// primitives
val wrappingKeyAlias = "wrappingKey"
val importedKeyAlias = "importedKey"
val importedKeySizeInBits = 2048
val symmetricWrappingKeyTransformation = "RSA/ECB/OAEPPadding"
// provisioning
val androidKeyStoreProviderName = "AndroidKeyStore"
val androidKeyStore =
KeyStore.getInstance(androidKeyStoreProviderName).apply { load(null, null) }
val challenge = Random.nextBytes(256 / 8)
val wrappingKeyPairSpec = wrappingKeyPairSpecification(
alias = wrappingKeyAlias,
isStrongBoxBacked = isStrongBox,
challenge = challenge,
)
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA).apply {
initialize(wrappingKeyPairSpec)
}.generateKeyPair()
val keyPairToBeImported =
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_RSA).apply {
initialize(importedKeySizeInBits, SecureRandom())
}.generateKeyPair()
// looking at data https://8gwifi.org/PemParserFunctions.jsp
System.out.println("private key format: ${keyPairToBeImported.private.format}")
System.out.println(
"private key data: ${
android.util.Base64.encodeToString(
keyPairToBeImported.private.encoded, android.util.Base64.DEFAULT
)
}"
)
val wrappedPrivateKey = wrapKey(
// as in the example source code
androidKeyStore.getCertificateChain(wrappingKeyAlias).first().publicKey,
keyPairToBeImported.private.encoded,
keyFormat = 1, // represents "PKCS8" according to https://android.googlesource.com/platform/hardware/interfaces/+/master/keymaster/4.0/types.hal
transformation = symmetricWrappingKeyTransformation,
wrappedKeyAuthorizationList(importedKeySizeInBits), // similar to the the example source code
)
val wrappedKeyEntry: KeyStore.Entry = WrappedKeyEntry(
wrappedPrivateKey,
wrappingKeyAlias,
symmetricWrappingKeyTransformation,
wrappingKeyPairSpec,
)
androidKeyStore.setEntry(importedKeyAlias, wrappedKeyEntry, null);
// androidKeyStore.setCertificateEntry(keyAlias, generateCertificate(wrappedKeyPair.publicKey, somePrivateKeyThatIsNotImportant))
System.out.println("Key available: ${androidKeyStore.containsAlias(importedKeyAlias)}")
// load
val importedKey = androidKeyStore.getEntry(importedKeyAlias, null)
System.out.println("isPrivateKey: ${importedKey is PrivateKey}")
}
fun wrappingKeyPairSpecification(
alias: String,
isStrongBoxBacked: Boolean,
challenge: ByteArray,
) = KeyGenParameterSpec.Builder(alias, KeyProperties.PURPOSE_WRAP_KEY)
.setDigests(KeyProperties.DIGEST_SHA256)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_OAEP)
.setBlockModes(KeyProperties.BLOCK_MODE_ECB).setIsStrongBoxBacked(isStrongBoxBacked)
.setAttestationChallenge(challenge)
.build()
@Throws(Exception::class)
fun wrapKey(
publicKey: PublicKey?,
keyMaterial: ByteArray?,
keyFormat: Long,
transformation: String,
authorizationList: DERSequence
): ByteArray {
return wrapKey(
publicKey,
keyMaterial,
keyFormat,
authorizationList,
transformation,
true,
)
}
@Throws(Exception::class)
fun wrapKey(
publicKey: PublicKey?,
keyMaterial: ByteArray?,
keyFormat: Long,
authorizationList: DERSequence,
transformation: String,
correctWrappingRequired: Boolean, // this should be true, but let's keep this for testing purposes
): ByteArray {
// Build description
val descriptionItems: ASN1EncodableVector = ASN1EncodableVector()
descriptionItems.add(ASN1Integer(keyFormat))
descriptionItems.add(authorizationList)
val wrappedKeyDescription: DERSequence = DERSequence(descriptionItems)
// Generate 12 byte initialization vector
val iv = ByteArray(12)
// TODO: use this for production code: SecureRandom.getInstanceStrong().nextBytes(iv)
Random.nextBytes(iv)
// Generate 256 bit AES key. This is the ephemeral key used to encrypt the secure key.
val aesKeyBytes = ByteArray(32)
// TODO: use this for production code: SecureRandom.getInstanceStrong().nextBytes(aesKeyBytes)
Random.nextBytes(aesKeyBytes)
// Encrypt ephemeral keys
val spec = OAEPParameterSpec(
"SHA-256", "MGF1", MGF1ParameterSpec.SHA1, PSource.PSpecified.DEFAULT
)
val pkCipher = Cipher.getInstance(transformation)
if (correctWrappingRequired) {
pkCipher.init(Cipher.ENCRYPT_MODE, publicKey, spec)
} else {
// Use incorrect OAEPParameters while initializing cipher. By default, main digest and
// MGF1 digest are SHA-1 here.
pkCipher.init(Cipher.ENCRYPT_MODE, publicKey)
}
val encryptedEphemeralKeys = pkCipher.doFinal(aesKeyBytes)
// Encrypt secure key
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
val secretKeySpec = SecretKeySpec(aesKeyBytes, "AES")
val gcmParameterSpec = GCMParameterSpec(GCM_TAG_SIZE, iv)
cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, gcmParameterSpec)
val aad: ByteArray = wrappedKeyDescription.getEncoded()
cipher.updateAAD(aad)
var encryptedSecureKey = cipher.doFinal(keyMaterial)
// Get GCM tag. Java puts the tag at the end of the ciphertext data :(
val len = encryptedSecureKey.size
val tagSize: Int = (GCM_TAG_SIZE / 8)
val tag = Arrays.copyOfRange(encryptedSecureKey, len - tagSize, len)
// Remove GCM tag from end of output
encryptedSecureKey = Arrays.copyOfRange(encryptedSecureKey, 0, len - tagSize)
// Build ASN.1 DER encoded sequence WrappedKeyWrapper
val items: ASN1EncodableVector = ASN1EncodableVector()
items.add(ASN1Integer(WRAPPED_FORMAT_VERSION))
items.add(DEROctetString(encryptedEphemeralKeys))
items.add(DEROctetString(iv))
items.add(wrappedKeyDescription)
items.add(DEROctetString(encryptedSecureKey))
items.add(DEROctetString(tag))
return DERSequence(items).getEncoded(ASN1Encoding.DER)
}
private fun removeTagType(tag: Int): Int {
val kmTagTypeMask = 0x0FFFFFFF
return tag and kmTagTypeMask
}
private fun wrappedKeyAuthorizationList(size: Int): DERSequence {
// https://source.android.com/docs/security/features/keystore/tags?hl=de
val algorithmRsaIdentifier = 1L
val paddingNoneIdentifier = 1L
val allPurposes = ASN1EncodableVector()
allPurposes.add(ASN1Integer(KeyProperties.PURPOSE_SIGN.toLong()))
val purposeSet: DERSet = DERSet(allPurposes)
val purpose = DERTaggedObject(true, removeTagType(KM_TAG_PURPOSE), purposeSet)
val algorithm = DERTaggedObject(
true,
removeTagType(KM_TAG_ALGORITHM),
// see https://source.android.com/docs/security/features/keystore/tags?hl=de
ASN1Integer(algorithmRsaIdentifier),
)
val keySize =
DERTaggedObject(true, removeTagType(KM_TAG_KEY_SIZE), ASN1Integer(size.toLong()))
val allDigests: ASN1EncodableVector = ASN1EncodableVector()
val digestSet: DERSet = DERSet(allDigests)
val digest = DERTaggedObject(true, removeTagType(KM_TAG_DIGEST), digestSet)
val allPaddings: ASN1EncodableVector = ASN1EncodableVector()
allPaddings.add(ASN1Integer(paddingNoneIdentifier)) // see https://source.android.com/docs/security/features/keystore/tags?hl=de
val paddingSet: DERSet = DERSet(allPaddings)
val padding = DERTaggedObject(true, removeTagType(KM_TAG_PADDING), paddingSet)
val noAuthRequired = // TODO: add user authentication
DERTaggedObject(true, removeTagType(KM_TAG_NO_AUTH_REQUIRED), DERNull.INSTANCE)
// Build sequence
val allItems: ASN1EncodableVector = ASN1EncodableVector()
allItems.add(purpose)
allItems.add(algorithm)
allItems.add(keySize)
allItems.add(digest)
allItems.add(padding)
allItems.add(noAuthRequired)
return DERSequence(allItems)
}
Relevant dependencies
// https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk18on
implementation("org.bouncycastle:bcprov-jdk18on:1.78.1")