Skip to content

Commit 4744d4a

Browse files
authored
Refactor Wallet Core usage from android device auth flow
1 parent ae8391b commit 4744d4a

20 files changed

Lines changed: 271 additions & 97 deletions

File tree

android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/GetAuthPayloadImpl.kt

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,17 @@ import com.gemwallet.android.application.GetAuthPayload
44
import com.gemwallet.android.application.PasswordStore
55
import com.gemwallet.android.application.device.coordinators.GetDeviceId
66
import com.gemwallet.android.blockchain.operators.LoadPrivateKeyOperator
7-
import com.gemwallet.android.blockchain.operators.walletcore.WCChainTypeProxy
87
import com.gemwallet.android.data.services.gemapi.GemDeviceApiClient
9-
import com.gemwallet.android.domains.referral.values.ReferralError
108
import com.gemwallet.android.ext.getAccount
11-
import com.gemwallet.android.math.append0x
12-
import com.gemwallet.android.math.hex
9+
import com.gemwallet.android.ext.referralChain
1310
import com.wallet.core.primitives.AuthPayload
1411
import com.wallet.core.primitives.Chain
1512
import com.wallet.core.primitives.Wallet
1613
import uniffi.gemstone.GemAuthNonce
17-
import wallet.core.jni.PrivateKey
18-
import java.util.Arrays
14+
import uniffi.gemstone.createAuthMessage
15+
import uniffi.gemstone.signAuthMessageHash
1916
import java.io.IOException
17+
import java.util.Arrays
2018

2119
class GetAuthPayloadImpl(
2220
private val gemDeviceApiClient: GemDeviceApiClient,
@@ -25,7 +23,8 @@ class GetAuthPayloadImpl(
2523
private val loadPrivateKeyOperator: LoadPrivateKeyOperator,
2624
) : GetAuthPayload {
2725

28-
override suspend fun getAuthPayload(wallet: Wallet, chain: Chain): AuthPayload {
26+
override suspend fun getAuthPayload(wallet: Wallet): AuthPayload {
27+
val chain = Chain.referralChain
2928
val account = wallet.getAccount(chain) ?: throw Exception() // TODO
3029
val deviceId = getDeviceId.getDeviceId()
3130
val key = loadPrivateKeyOperator(
@@ -36,14 +35,12 @@ class GetAuthPayloadImpl(
3635

3736
try {
3837
val nonce = gemDeviceApiClient.getAuthNonce() ?: throw IOException("Auth nonce unavailable")
39-
val message = uniffi.gemstone.createAuthMessage(
40-
chain = Chain.Ethereum.string,
38+
val message = createAuthMessage(
4139
address = account.address,
4240
authNonce = GemAuthNonce(nonce.nonce, nonce.timestamp)
4341
)
4442

45-
val signature = PrivateKey(key).sign(message.hash, WCChainTypeProxy()(chain).curve())
46-
.hex.append0x()
43+
val signature = signAuthMessageHash(message.hash, key)
4744
return AuthPayload(
4845
deviceId = deviceId,
4946
chain = account.chain,

android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/device/GetDeviceIdImpl.kt

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ package com.gemwallet.android.data.coordinators.device
22

33
import com.gemwallet.android.application.SecurityStore
44
import com.gemwallet.android.application.device.coordinators.GetDeviceId
5-
import com.gemwallet.android.math.hex
5+
import com.gemwallet.android.data.services.gemapi.DeviceKeyPair
66
import kotlinx.coroutines.runBlocking
7-
import wallet.core.jni.Curve
8-
import wallet.core.jni.HDWallet
97

108
class GetDeviceIdImpl(
119
private val store: SecurityStore<Any>
@@ -24,9 +22,9 @@ class GetDeviceIdImpl(
2422
return data
2523
} catch (_: Throwable) {}
2624

27-
val deviceKey = HDWallet(128, "").getMasterKey(Curve.ED25519)
28-
val privateKey = deviceKey.data().hex
29-
val publicKey = deviceKey.publicKeyEd25519.data().hex
25+
val deviceKey = DeviceKeyPair.generate()
26+
val privateKey = deviceKey.privateKeyHex
27+
val publicKey = deviceKey.publicKeyHex
3028

3129
store.putValue(Keys.PrivateKey, privateKey)
3230
store.putValue(Keys.PublicKey, publicKey)

android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/referral/CreateReferralImpl.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@ package com.gemwallet.android.data.coordinators.referral
33
import com.gemwallet.android.application.GetAuthPayload
44
import com.gemwallet.android.application.referral.coordinators.CreateReferral
55
import com.gemwallet.android.data.services.gemapi.GemDeviceApiClient
6-
import com.gemwallet.android.domains.referral.values.ReferralError
7-
import com.gemwallet.android.ext.getAccount
8-
import com.gemwallet.android.ext.referralChain
96
import com.wallet.core.primitives.AuthenticatedRequest
10-
import com.wallet.core.primitives.Chain
117
import com.wallet.core.primitives.ReferralCode
128
import com.wallet.core.primitives.Rewards
139
import com.wallet.core.primitives.Wallet
@@ -20,8 +16,7 @@ class CreateReferralImpl(
2016

2117

2218
override suspend fun createReferral(code: String, wallet: Wallet): Rewards {
23-
val account = wallet.getAccount(Chain.referralChain) ?: throw ReferralError.BadWallet
24-
val authPayload = getAuthPayload.getAuthPayload(wallet, account.chain)
19+
val authPayload = getAuthPayload.getAuthPayload(wallet)
2520
return gemDeviceApiClient.createReferral(
2621
walletId = wallet.id,
2722
body = AuthenticatedRequest(

android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/referral/RedeemImpl.kt

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,7 @@ import com.gemwallet.android.data.repositories.session.SessionRepository
77
import com.gemwallet.android.data.services.gemapi.GemDeviceApiClient
88
import com.gemwallet.android.domains.referral.values.ReferralError
99
import com.gemwallet.android.ext.getAccount
10-
import com.gemwallet.android.ext.referralChain
1110
import com.wallet.core.primitives.AuthenticatedRequest
12-
import com.wallet.core.primitives.Chain
1311
import com.wallet.core.primitives.RedemptionRequest
1412
import com.wallet.core.primitives.RedemptionResult
1513
import com.wallet.core.primitives.RewardRedemptionOption
@@ -25,8 +23,7 @@ class RedeemImpl(
2523
) : Redeem {
2624

2725
override suspend fun redeem(wallet: Wallet, rewards: Rewards, option: RewardRedemptionOption): RedemptionResult {
28-
val account = wallet.getAccount(Chain.referralChain) ?: throw ReferralError.BadWallet
29-
val authPayload = getAuthPayload.getAuthPayload(wallet, account.chain)
26+
val authPayload = getAuthPayload.getAuthPayload(wallet)
3027
if (rewards.points < option.points) {
3128
throw ReferralError.InsufficientPoints
3229
}

android/data/coordinators/src/main/kotlin/com/gemwallet/android/data/coordinators/referral/UseReferralCodeImpl.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,7 @@ package com.gemwallet.android.data.coordinators.referral
33
import com.gemwallet.android.application.GetAuthPayload
44
import com.gemwallet.android.application.referral.coordinators.UseReferralCode
55
import com.gemwallet.android.data.services.gemapi.GemDeviceApiClient
6-
import com.gemwallet.android.domains.referral.values.ReferralError
7-
import com.gemwallet.android.ext.getAccount
8-
import com.gemwallet.android.ext.referralChain
96
import com.wallet.core.primitives.AuthenticatedRequest
10-
import com.wallet.core.primitives.Chain
117
import com.wallet.core.primitives.ReferralCode
128
import com.wallet.core.primitives.Wallet
139

@@ -18,8 +14,7 @@ class UseReferralCodeImpl(
1814

1915

2016
override suspend fun useReferralCode(code: String, wallet: Wallet): Boolean {
21-
val account = wallet.getAccount(Chain.referralChain) ?: throw ReferralError.BadWallet
22-
val auth = getAuthPayload.getAuthPayload(wallet, account.chain)
17+
val auth = getAuthPayload.getAuthPayload(wallet)
2318
gemDeviceApiClient.useReferralCode(
2419
walletId = wallet.id,
2520
body = AuthenticatedRequest(

android/data/services/remote-gem/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,10 @@ dependencies {
5858

5959
implementation(libs.ktx.core)
6060
implementation(libs.kotlinx.coroutines.android)
61+
implementation(libs.tink)
6162

6263
testImplementation(libs.junit)
64+
testImplementation(testFixtures(project(":gemcore")))
6365
androidTestImplementation(libs.androidx.junit)
6466
androidTestImplementation(libs.androidx.espresso.core)
6567
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package com.gemwallet.android.data.services.gemapi
2+
3+
import com.gemwallet.android.math.fromHex
4+
import com.gemwallet.android.math.hex
5+
import com.google.crypto.tink.subtle.Ed25519Sign
6+
import java.security.GeneralSecurityException
7+
import java.util.Arrays
8+
9+
class DeviceKeyPair private constructor(
10+
private val privateKey: ByteArray,
11+
private val publicKey: ByteArray,
12+
) {
13+
val privateKeyHex: String
14+
get() = privateKey.hex
15+
16+
val publicKeyHex: String
17+
get() = publicKey.hex
18+
19+
fun sign(message: ByteArray): String {
20+
return Ed25519Sign(privateKey).sign(message).hex
21+
}
22+
23+
companion object {
24+
fun generate(): DeviceKeyPair {
25+
val keyPair = Ed25519Sign.KeyPair.newKeyPair()
26+
return DeviceKeyPair(
27+
privateKey = keyPair.privateKey,
28+
publicKey = keyPair.publicKey,
29+
)
30+
}
31+
32+
fun fromHex(privateKeyHex: String): DeviceKeyPair {
33+
val privateKey = try {
34+
privateKeyHex.fromHex()
35+
} catch (err: IllegalArgumentException) {
36+
throw IllegalArgumentException("Invalid device private key", err)
37+
}
38+
val seed = privateKey.copyOf()
39+
return try {
40+
DeviceKeyPair(
41+
privateKey = privateKey,
42+
publicKey = Ed25519Sign.KeyPair.newKeyPairFromSeed(seed).publicKey,
43+
)
44+
} catch (err: GeneralSecurityException) {
45+
Arrays.fill(privateKey, 0)
46+
throw IllegalArgumentException("Invalid device private key", err)
47+
} finally {
48+
Arrays.fill(seed, 0)
49+
}
50+
}
51+
}
52+
}

android/data/services/remote-gem/src/main/kotlin/com/gemwallet/android/data/services/gemapi/http/DeviceRequestSigner.kt

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,9 @@
11
package com.gemwallet.android.data.services.gemapi.http
22

33
import com.gemwallet.android.application.device.coordinators.GetDeviceId
4-
import com.gemwallet.android.math.fromHex
5-
import com.gemwallet.android.math.hex
4+
import com.gemwallet.android.data.services.gemapi.DeviceKeyPair
5+
import com.gemwallet.android.math.sha256Hex
66
import java.util.Base64
7-
import wallet.core.jni.Curve
8-
import wallet.core.jni.Hash
9-
import wallet.core.jni.PrivateKey
107

118
data class DeviceSignature(
129
val authorization: String,
@@ -17,34 +14,36 @@ data class DeviceSignature(
1714
}
1815

1916
class DeviceRequestSigner(
20-
private val getDeviceId: GetDeviceId,
17+
getDeviceId: GetDeviceId,
2118
) {
19+
private val deviceKeyPair = DeviceKeyPair.fromHex(getDeviceId.getDeviceKey())
2220
private var bodyHash: (ByteArray) -> String = { body: ByteArray ->
23-
Hash.sha256(body).hex
21+
body.sha256Hex()
2422
}
25-
private var signMessage: (String, ByteArray) -> String = { privateKeyHex: String, message: ByteArray ->
26-
PrivateKey(privateKeyHex.fromHex()).sign(message, Curve.ED25519).hex
23+
private var signMessage: (DeviceKeyPair, ByteArray) -> String = { deviceKeyPair, message ->
24+
deviceKeyPair.sign(message)
2725
}
2826
private var currentTimeMillis: () -> Long = System::currentTimeMillis
2927

3028
fun sign(method: String, path: String, body: ByteArray? = null, walletId: String = ""): DeviceSignature {
31-
val publicKeyHex = getDeviceId.getDeviceId()
3229
val bodyHash = bodyHash(body ?: ByteArray(0))
3330
val timestamp = currentTimeMillis().toString()
3431

3532
val message = "${timestamp}.${method}.${path}.${walletId}.${bodyHash}"
36-
val signatureHex = signMessage(getDeviceId.getDeviceKey(), message.toByteArray())
33+
val signatureHex = signMessage(deviceKeyPair, message.toByteArray())
3734

38-
val payload = "${publicKeyHex}.${timestamp}.${walletId}.${bodyHash}.${signatureHex}"
35+
val payload = "${deviceKeyPair.publicKeyHex}.${timestamp}.${walletId}.${bodyHash}.${signatureHex}"
3936
val encoded = Base64.getEncoder().encodeToString(payload.toByteArray())
4037
return DeviceSignature(authorization = "Gem $encoded")
4138
}
4239

4340
internal constructor(
4441
getDeviceId: GetDeviceId,
45-
bodyHash: (ByteArray) -> String,
46-
signMessage: (String, ByteArray) -> String,
47-
currentTimeMillis: () -> Long,
42+
bodyHash: (ByteArray) -> String = { body -> body.sha256Hex() },
43+
signMessage: (DeviceKeyPair, ByteArray) -> String = { deviceKeyPair, message ->
44+
deviceKeyPair.sign(message)
45+
},
46+
currentTimeMillis: () -> Long = System::currentTimeMillis,
4847
) : this(getDeviceId) {
4948
this.bodyHash = bodyHash
5049
this.signMessage = signMessage
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package com.gemwallet.android.data.services.gemapi
2+
3+
internal object DeviceKeyPairFixture {
4+
val privateKeyHex = "9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60"
5+
val publicKeyHex = "d75a980182b10ab7d54bfed3c964073a0ee172f3daa62325af021a68f707511a"
6+
val deviceAuthMessage = "device-auth".toByteArray()
7+
val deviceAuthSignatureHex = "121bb28074b00114a7b267ae8a6292c9ffe56db6254b0c65389fd726dbdeffe95b15e6f48d3a3980f7a983da44a3de24c0771d0d8723cef2ced6d08d343a2101"
8+
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package com.gemwallet.android.data.services.gemapi
2+
3+
import com.gemwallet.android.math.fromHex
4+
import com.google.crypto.tink.subtle.Ed25519Verify
5+
import org.junit.Assert.assertEquals
6+
import org.junit.Assert.assertThrows
7+
import org.junit.Test
8+
9+
class DeviceKeyPairTest {
10+
11+
@Test
12+
fun generateCreatesVerifiableEd25519KeyPair() {
13+
val keyPair = DeviceKeyPair.generate()
14+
val publicKey = keyPair.publicKeyHex.fromHex()
15+
16+
val signature = keyPair.sign(DeviceKeyPairFixture.deviceAuthMessage).fromHex()
17+
18+
Ed25519Verify(publicKey).verify(signature, DeviceKeyPairFixture.deviceAuthMessage)
19+
assertEquals(64, keyPair.privateKeyHex.length)
20+
assertEquals(64, keyPair.publicKeyHex.length)
21+
}
22+
23+
@Test
24+
fun fromHexUsesDecodedEd25519PrivateKey() {
25+
val keyPair = DeviceKeyPair.fromHex(DeviceKeyPairFixture.privateKeyHex)
26+
val publicKey = DeviceKeyPairFixture.publicKeyHex.fromHex()
27+
28+
val signatureHex = keyPair.sign(DeviceKeyPairFixture.deviceAuthMessage)
29+
30+
assertEquals(DeviceKeyPairFixture.privateKeyHex, keyPair.privateKeyHex)
31+
assertEquals(DeviceKeyPairFixture.publicKeyHex, keyPair.publicKeyHex)
32+
assertEquals(DeviceKeyPairFixture.deviceAuthSignatureHex, signatureHex)
33+
Ed25519Verify(publicKey).verify(signatureHex.fromHex(), DeviceKeyPairFixture.deviceAuthMessage)
34+
}
35+
36+
@Test
37+
fun fromHexRejectsInvalidPrivateKeyHex() {
38+
assertThrows(IllegalArgumentException::class.java) {
39+
DeviceKeyPair.fromHex("abcd")
40+
}
41+
}
42+
}

0 commit comments

Comments
 (0)