Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Deterministic Passkey Generation ENG-487 #23

Merged
merged 15 commits into from
Sep 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
.gradle
/local.properties
/.idea/caches
/.idea/deploymentTargetSelector.xml
/.idea/libraries
/.idea/modules.xml
/.idea/workspace.xml
Expand Down
4 changes: 4 additions & 0 deletions demo/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,10 @@ dependencies {
implementation("androidx.credentials:credentials:1.2.2")
implementation("androidx.credentials:credentials-play-services-auth:1.2.2")

// Deterministic Passkeys
implementation(files("libs/dP256.jar"))
implementation("cash.z.ecc.android:kotlin-bip39:1.0.8")

// Kotlin Coroutine
val coroutineVersion by extra { "1.7.1" }
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutineVersion")
Expand Down
Binary file added demo/libs/dP256.jar
Binary file not shown.
1 change: 1 addition & 0 deletions demo/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@
<meta-data
android:name="android.credentials.provider"
android:resource="@xml/provider"/>

</service>
</application>

Expand Down
774 changes: 450 additions & 324 deletions demo/src/main/java/foundation/algorand/demo/AnswerActivity.kt

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,38 @@ import androidx.annotation.RequiresApi
import androidx.credentials.provider.CallingAppInfo
import foundation.algorand.demo.credential.db.Credential
import foundation.algorand.demo.credential.db.CredentialDatabase
import foundation.algorand.demo.derivedSecret.DerivedSecretRepository
import foundation.algorand.deterministicP256.DeterministicP256
import java.security.*
import java.security.spec.*
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi

// import java.security.interfaces.ECPrivateKey

interface CredentialRepository {
val keyStore: KeyStore
var db: CredentialDatabase
suspend fun saveCredential(context: Context, credential: Credential)
fun getDatabase(context: Context): CredentialDatabase
fun generateCredentialId(): ByteArray
fun getKeyPair(context: Context): KeyPair
fun getKeyPair(context: Context, credentialId: ByteArray): KeyPair
fun generateCredentialId(keyPair: KeyPair): ByteArray
fun getKeyPair(context: Context, credentialId: ByteArray): KeyPair?
fun createDeterministicKeyPair(context: Context, origin: String, userHandle: String): KeyPair
fun appInfoToOrigin(info: CallingAppInfo): String
fun getCredential(context: Context, credentialId: ByteArray): Credential?
fun getCredentialByOrigin(context: Context, origin: String): Credential?
fun sign(keyPair: KeyPair, payload: ByteArray): ByteArray
}

fun CredentialRepository(): CredentialRepository = Repository()
class Repository(): CredentialRepository {

class Repository() : CredentialRepository {
override var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore")
private var generator: KeyPairGenerator = KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC)
private var generator: KeyPairGenerator =
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC)
private var dP256: DeterministicP256 = DeterministicP256()
override lateinit var db: CredentialDatabase
private val derivedSecretRepository = DerivedSecretRepository()
init {
keyStore.load(null)
}
Expand All @@ -42,17 +51,27 @@ class Repository(): CredentialRepository {
getDatabase(context)
db.credentialDao().insertAll(credential)
}

override fun getDatabase(context: Context): CredentialDatabase {
Log.d(TAG, "getDatabase($context)")
if(!::db.isInitialized) {
if (!::db.isInitialized) {
db = CredentialDatabase.getInstance(context)
}
return db
}
override fun generateCredentialId(): ByteArray {

// CredentialId is deterministically generated from the public key
// Taking SHA-256 hash of the public key, giving us a 32-byte credentialId
override fun generateCredentialId(keyPair: KeyPair): ByteArray {
Log.d(TAG, "generateCredentialId()")
val credentialId = ByteArray(32)
SecureRandom().nextBytes(credentialId)

// Get the public key bytes
val publicKeyBytes = keyPair.public.encoded

// Compute SHA-256 hash of the public key
val messageDigest = MessageDigest.getInstance("SHA-256")
val credentialId = messageDigest.digest(publicKeyBytes)

return credentialId
}

Expand Down Expand Up @@ -81,18 +100,28 @@ class Repository(): CredentialRepository {
}
return null
}
override fun getKeyPair(context: Context): KeyPair{
return getKeyPair(context, generateCredentialId())
}
override fun getKeyPair(context:Context, credentialId: ByteArray): KeyPair {

override fun getKeyPair(context: Context, credentialId: ByteArray): KeyPair? {
Log.d(TAG, "getKeyPair($context, $credentialId)")
val savedKeyPair = getKeyPairFromDatabase(context, credentialId)
if (savedKeyPair != null) {
return savedKeyPair
}
generator.initialize(ECGenParameterSpec("secp256r1"))
return generator.generateKeyPair()
return getKeyPairFromDatabase(context, credentialId)
}

override fun createDeterministicKeyPair(
context: Context,
origin: String,
userHandle: String
): KeyPair {
// Note that we take the LOWERCASE of the userHandle, to prevent confusion
Log.d(TAG, "createDeterministicKeyPair($context, , $origin, ${userHandle.lowercase()})")

val derivedParentSecret = derivedSecretRepository.getDerivedParentSecret(context)?.derivedSecret!!.toByteArray()
return dP256.genDomainSpecificKeypair(derivedParentSecret, origin, userHandle.lowercase())
}

override fun sign(keyPair: KeyPair, payload: ByteArray): ByteArray {
return dP256.signWithDomainSpecificKeyPair(keyPair, payload)
}

@OptIn(ExperimentalEncodingApi::class)
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
override fun appInfoToOrigin(info: CallingAppInfo): String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,8 @@ import kotlinx.coroutines.flow.Flow

@Dao
interface CredentialDao {
@Query("SELECT * FROM credential")
fun getAll(): Flow<List<Credential>>
@Query("SELECT * FROM credential")
fun getAllRegular(): List<Credential>
@Query("SELECT * FROM credential") fun getAll(): Flow<List<Credential>>
@Query("SELECT * FROM credential") fun getAllRegular(): List<Credential>
@Query("SELECT * FROM credential WHERE credentialId IN (:credentialIds)")
fun loadAllByIds(credentialIds: List<String>): List<Credential>
@Query("SELECT * FROM credential WHERE credentialId LIKE :credentialId LIMIT 1")
Expand All @@ -21,9 +19,11 @@ interface CredentialDao {
@Query("SELECT * FROM credential WHERE userHandle LIKE :userHandle LIMIT 1")
fun findByUser(userHandle: String): Credential

@Insert
suspend fun insertAll(vararg credentials: Credential)
@Insert suspend fun insertAll(vararg credentials: Credential)

@Delete
fun delete(credential: Credential)
@Delete fun delete(credential: Credential)

// FIXME: This should in the future use a specific
// data class for derived master/parent secret
@Insert fun insertAllNoSuspend(vararg credentials: Credential)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package foundation.algorand.demo.derivedSecret.db

import androidx.room.*
import androidx.room.TypeConverter

enum class SecretType {
PASSKEY,
SPENDKEY
}

class Converters {
@TypeConverter
fun fromSecretType(value: SecretType): String {
return value.name
}

@TypeConverter
fun toSecretType(value: String): SecretType {
return SecretType.valueOf(value)
}
}

@Entity
data class DerivedSecret(
@PrimaryKey val id: String,
// FIXME: Not secure storage of keys, this is just for demonstration
@ColumnInfo(name = "derivedSecret") val derivedSecret: String,
@ColumnInfo(name = "mnemonic") val mnemonic: String,
@ColumnInfo(name = "type") val type: SecretType
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package foundation.algorand.demo.derivedSecret.db

import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import kotlinx.coroutines.flow.Flow
@Dao
interface DerivedSecretDao {
@Query("SELECT * FROM derivedSecret") fun getAll(): Flow<List<DerivedSecret>>
@Query("SELECT * FROM derivedSecret") fun getAllRegular(): List<DerivedSecret>
@Query("SELECT * FROM derivedSecret WHERE id IN (:ids)")
fun loadAllByIds(ids: List<String>): List<DerivedSecret>
@Query("SELECT * FROM derivedSecret WHERE id LIKE :id LIMIT 1")
fun findById(id: String): DerivedSecret?
@Query("SELECT * FROM derivedSecret WHERE type LIKE :secretType LIMIT 1")
fun findBySecretType(secretType: SecretType): DerivedSecret?

@Insert suspend fun insertAll(vararg derivedSecret: DerivedSecret)

@Delete fun delete(derivedSecret: DerivedSecret)

@Insert fun insertAllNoSuspend(vararg derivedSecrets: DerivedSecret)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package foundation.algorand.demo.derivedSecret.db

import android.content.Context
import android.util.Log
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters

@Database(entities = [DerivedSecret::class], version = 1)
@TypeConverters(Converters::class)
abstract class DerivedSecretDatabase : RoomDatabase() {
abstract fun derivedSecretDao(): DerivedSecretDao

companion object {
const val TAG = "DerivedSecretDatabase"
private var INSTANCE: DerivedSecretDatabase? = null
fun getInstance(context: Context): DerivedSecretDatabase {
Log.d(TAG, "getInstance($context)")
if (INSTANCE == null) {
INSTANCE =
Room.databaseBuilder(
context,
DerivedSecretDatabase::class.java,
"derivedSecret"
)
.allowMainThreadQueries()
.build()
}

return INSTANCE!!
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package foundation.algorand.demo.derivedSecret

import android.content.Context
import android.os.Build
import android.security.keystore.KeyProperties
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.credentials.provider.CallingAppInfo
import foundation.algorand.demo.derivedSecret.db.DerivedSecret
import foundation.algorand.demo.derivedSecret.db.DerivedSecretDatabase
import foundation.algorand.deterministicP256.DeterministicP256
import java.security.*
import java.security.spec.*
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import foundation.algorand.demo.derivedSecret.db.SecretType

// import java.security.interfaces.ECPrivateKey

interface DerivedSecretRepository {
val keyStore: KeyStore
var db: DerivedSecretDatabase
fun saveDerivedParentSecret(context: Context, mnemonic: CharArray)
fun getDatabase(context: Context): DerivedSecretDatabase
fun getDerivedParentSecret(context: Context): DerivedSecret?
}

fun DerivedSecretRepository(): DerivedSecretRepository = Repository()

class Repository() : DerivedSecretRepository {
override var keyStore: KeyStore = KeyStore.getInstance("AndroidKeyStore")
private var generator: KeyPairGenerator =
KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC)
private var dP256: DeterministicP256 = DeterministicP256()
override lateinit var db: DerivedSecretDatabase
init {
keyStore.load(null)
}
companion object {
const val TAG = "DerivedSecretRepository"
}

override fun saveDerivedParentSecret(context: Context, mnemonic: CharArray) {
Log.d(TAG, "saveDerivedParentSecret([mnemonic kept hidden])")
getDatabase(context)

getDerivedParentSecret(context)?.let { db.derivedSecretDao().delete(it) }

db.derivedSecretDao()
.insertAllNoSuspend(
DerivedSecret(
id = "derivedParentSecret",
derivedSecret =
dP256.genDerivedMainKeyWithBIP39(mnemonic.concatToString())
.contentToString(),
type = SecretType.PASSKEY,
mnemonic = mnemonic.concatToString() // We could choose to not store the mnemonic
)
)
}

override fun getDatabase(context: Context): DerivedSecretDatabase {
Log.d(TAG, "getDatabase($context)")
if (!::db.isInitialized) {
db = DerivedSecretDatabase.getInstance(context)
}
return db
}

override fun getDerivedParentSecret(context: Context): DerivedSecret? {
getDatabase(context)
return db.derivedSecretDao().findById("derivedParentSecret")
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -114,16 +114,17 @@ class CreatePasskeyViewModel(): ViewModel() {
request.callingRequest as CreatePublicKeyCredentialRequest
val requestOptions = PublicKeyCredentialCreationOptions(publicKeyRequest.requestJson)

// Generate a credentialId
val credentialId = credentialRepository.generateCredentialId()
// Generate a credential key pair
val keyPair = credentialRepository.getKeyPair(context, credentialId)

val requestJson = JSONObject(publicKeyRequest.requestJson)
val userJson = requestJson.getJSONObject("user")
val name = userJson.get("name").toString()
val userId = userJson.get("id").toString()

// Generate a key pair
val keyPair = credentialRepository.createDeterministicKeyPair(context, request.callingAppInfo.origin!!, name)

// Deterministically generate a credentialId
val credentialId = credentialRepository.generateCredentialId(keyPair)

// Save passkey in your database as per your own implementation
viewModelScope.launch {
credentialRepository.saveCredential(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import foundation.algorand.demo.credential.CredentialRepository
import org.json.JSONObject
import java.security.KeyPair
import java.security.Signature
import java.security.interfaces.ECPrivateKey
import kotlin.io.encoding.Base64
Expand Down Expand Up @@ -98,13 +99,20 @@ class GetPasskeyViewModel: ViewModel() {
packageName = packageName,
clientDataHash = clientDataHash!!
)

val keyPair = credentialRepository.getKeyPair(context, credId)
?: throw Error("No keypair corresponding to that credential id!")
// FIXME:add proper error handling!

// NOTE:
// We are assuming that a passkey has already been created and stored under the credId
// Ideally we could simply recreate the passkey at this stage, but it is not necessarily
// the case that the authenticating website will pass along the userId. The WebAuthn
// demo website do pass along the userId for the registration phase, but then only the
// credential id in subseqent attempts to authenticate.

//TODO: Fix signature issues
val sig = Signature.getInstance("SHA256withECDSA")
sig.initSign(keyPair.private as ECPrivateKey )
sig.update(response.dataToSign())
response.signature = sig.sign()
response.signature = credentialRepository.sign(keyPair, response.dataToSign())
val options = request.credentialOptions[0] as GetPublicKeyCredentialOption
val json = options.requestJson
val requestJson = JSONObject(json)
Expand Down
Loading
Loading