@authon/react-native-wallet 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (159) hide show
  1. package/README.md +221 -2
  2. package/android/build.gradle +40 -0
  3. package/android/consumer-rules.pro +10 -0
  4. package/android/src/main/AndroidManifest.xml +4 -0
  5. package/android/src/main/java/dev/authon/reactnative/wallet/AuthonWalletModule.kt +181 -0
  6. package/android/src/vendor/GENERATED.md +4 -0
  7. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/AuthonWallet.kt +191 -0
  8. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Bytes.kt +74 -0
  9. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/CredentialManagerPasskeys.kt +192 -0
  10. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DeviceStore.kt +170 -0
  11. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/DialogWalletApprover.kt +141 -0
  12. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Eip712.kt +176 -0
  13. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmAccount.kt +45 -0
  14. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/EvmTransaction.kt +180 -0
  15. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/GfTables.kt +44 -0
  16. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/HdWallet.kt +130 -0
  17. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Http.kt +82 -0
  18. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/JsValues.kt +83 -0
  19. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt +232 -0
  20. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Prf.kt +111 -0
  21. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Proof.kt +134 -0
  22. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/Shamir.kt +106 -0
  23. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/SignSummary.kt +335 -0
  24. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletApi.kt +353 -0
  25. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletError.kt +58 -0
  26. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletOps.kt +449 -0
  27. package/android/src/vendor/kotlin/dev/authon/sdk/wallet/WalletTypes.kt +71 -0
  28. package/build/authonContext.d.ts +19 -0
  29. package/build/authonContext.d.ts.map +1 -0
  30. package/build/authonContext.js +20 -0
  31. package/build/authonContext.js.map +1 -0
  32. package/build/client.d.ts +76 -0
  33. package/build/client.d.ts.map +1 -0
  34. package/build/client.js +349 -0
  35. package/build/client.js.map +1 -0
  36. package/build/errors.d.ts +18 -0
  37. package/build/errors.d.ts.map +1 -0
  38. package/build/errors.js +9 -0
  39. package/build/errors.js.map +1 -0
  40. package/build/index.d.ts +7 -0
  41. package/build/index.d.ts.map +1 -0
  42. package/build/index.js +4 -0
  43. package/build/index.js.map +1 -0
  44. package/build/native.d.ts +39 -0
  45. package/build/native.d.ts.map +1 -0
  46. package/build/native.js +12 -0
  47. package/build/native.js.map +1 -0
  48. package/build/types.d.ts +121 -0
  49. package/build/types.d.ts.map +1 -0
  50. package/build/types.js +3 -0
  51. package/build/types.js.map +1 -0
  52. package/build/useAuthonWallet.d.ts +25 -0
  53. package/build/useAuthonWallet.d.ts.map +1 -0
  54. package/build/useAuthonWallet.js +77 -0
  55. package/build/useAuthonWallet.js.map +1 -0
  56. package/expo-module.config.json +9 -0
  57. package/ios/AuthonReactNativeWallet.podspec +37 -0
  58. package/ios/AuthonWalletModule.swift +190 -0
  59. package/ios/vendor/AuthonWallet/AuthonWallet.swift +212 -0
  60. package/ios/vendor/AuthonWallet/Crypto/BIP39Wordlist.swift +262 -0
  61. package/ios/vendor/AuthonWallet/Crypto/HDKey.swift +108 -0
  62. package/ios/vendor/AuthonWallet/Crypto/Secp256k1.swift +104 -0
  63. package/ios/vendor/AuthonWallet/Crypto/Shamir.swift +123 -0
  64. package/ios/vendor/AuthonWallet/EVM/Address.swift +57 -0
  65. package/ios/vendor/AuthonWallet/EVM/EvmAccount.swift +48 -0
  66. package/ios/vendor/AuthonWallet/EVM/Transaction.swift +165 -0
  67. package/ios/vendor/AuthonWallet/EVM/TypedData.swift +193 -0
  68. package/ios/vendor/AuthonWallet/Support/BigInteger.swift +220 -0
  69. package/ios/vendor/AuthonWallet/Support/Bytes.swift +89 -0
  70. package/ios/vendor/AuthonWallet/Support/JSONValue.swift +341 -0
  71. package/ios/vendor/AuthonWallet/UI/WalletApprover.swift +250 -0
  72. package/ios/vendor/AuthonWallet/Wallet/Backup.swift +69 -0
  73. package/ios/vendor/AuthonWallet/Wallet/DeviceStore.swift +351 -0
  74. package/ios/vendor/AuthonWallet/Wallet/Passkeys.swift +190 -0
  75. package/ios/vendor/AuthonWallet/Wallet/Proof.swift +143 -0
  76. package/ios/vendor/AuthonWallet/Wallet/Shares.swift +116 -0
  77. package/ios/vendor/AuthonWallet/Wallet/Summary.swift +342 -0
  78. package/ios/vendor/AuthonWallet/Wallet/WalletAPI.swift +350 -0
  79. package/ios/vendor/AuthonWallet/Wallet/WalletOps.swift +555 -0
  80. package/ios/vendor/AuthonWallet/WalletError.swift +54 -0
  81. package/ios/vendor/CKeccak/LICENSE +22 -0
  82. package/ios/vendor/CKeccak/README.md +27 -0
  83. package/ios/vendor/CKeccak/byte_order.h +59 -0
  84. package/ios/vendor/CKeccak/include/ckeccak.h +9 -0
  85. package/ios/vendor/CKeccak/memzero.c +75 -0
  86. package/ios/vendor/CKeccak/memzero.h +8 -0
  87. package/ios/vendor/CKeccak/options.h +99 -0
  88. package/ios/vendor/CKeccak/sha3.c +413 -0
  89. package/ios/vendor/CKeccak/sha3.h +89 -0
  90. package/ios/vendor/CSecp256k1/COPYING +19 -0
  91. package/ios/vendor/CSecp256k1/README.md +35 -0
  92. package/ios/vendor/CSecp256k1/build/precomputed_ecmult.c +3 -0
  93. package/ios/vendor/CSecp256k1/build/precomputed_ecmult_gen.c +3 -0
  94. package/ios/vendor/CSecp256k1/build/secp256k1.c +3 -0
  95. package/ios/vendor/CSecp256k1/include/secp256k1.h +899 -0
  96. package/ios/vendor/CSecp256k1/include/secp256k1_preallocated.h +134 -0
  97. package/ios/vendor/CSecp256k1/include/secp256k1_recovery.h +113 -0
  98. package/ios/vendor/CSecp256k1/src/assumptions.h +87 -0
  99. package/ios/vendor/CSecp256k1/src/checkmem.h +102 -0
  100. package/ios/vendor/CSecp256k1/src/ecdsa.h +21 -0
  101. package/ios/vendor/CSecp256k1/src/ecdsa_impl.h +304 -0
  102. package/ios/vendor/CSecp256k1/src/eckey.h +25 -0
  103. package/ios/vendor/CSecp256k1/src/eckey_impl.h +92 -0
  104. package/ios/vendor/CSecp256k1/src/ecmult.h +61 -0
  105. package/ios/vendor/CSecp256k1/src/ecmult_const.h +38 -0
  106. package/ios/vendor/CSecp256k1/src/ecmult_const_impl.h +399 -0
  107. package/ios/vendor/CSecp256k1/src/ecmult_gen.h +143 -0
  108. package/ios/vendor/CSecp256k1/src/ecmult_gen_impl.h +341 -0
  109. package/ios/vendor/CSecp256k1/src/ecmult_impl.h +853 -0
  110. package/ios/vendor/CSecp256k1/src/field.h +350 -0
  111. package/ios/vendor/CSecp256k1/src/field_10x26.h +57 -0
  112. package/ios/vendor/CSecp256k1/src/field_10x26_impl.h +1232 -0
  113. package/ios/vendor/CSecp256k1/src/field_5x52.h +62 -0
  114. package/ios/vendor/CSecp256k1/src/field_5x52_impl.h +522 -0
  115. package/ios/vendor/CSecp256k1/src/field_5x52_int128_impl.h +274 -0
  116. package/ios/vendor/CSecp256k1/src/field_impl.h +457 -0
  117. package/ios/vendor/CSecp256k1/src/group.h +212 -0
  118. package/ios/vendor/CSecp256k1/src/group_impl.h +974 -0
  119. package/ios/vendor/CSecp256k1/src/hash.h +44 -0
  120. package/ios/vendor/CSecp256k1/src/hash_impl.h +299 -0
  121. package/ios/vendor/CSecp256k1/src/hsort.h +33 -0
  122. package/ios/vendor/CSecp256k1/src/hsort_impl.h +125 -0
  123. package/ios/vendor/CSecp256k1/src/int128.h +90 -0
  124. package/ios/vendor/CSecp256k1/src/int128_impl.h +18 -0
  125. package/ios/vendor/CSecp256k1/src/int128_native.h +19 -0
  126. package/ios/vendor/CSecp256k1/src/int128_native_impl.h +94 -0
  127. package/ios/vendor/CSecp256k1/src/int128_struct.h +14 -0
  128. package/ios/vendor/CSecp256k1/src/int128_struct_impl.h +205 -0
  129. package/ios/vendor/CSecp256k1/src/modinv32.h +43 -0
  130. package/ios/vendor/CSecp256k1/src/modinv32_impl.h +725 -0
  131. package/ios/vendor/CSecp256k1/src/modinv64.h +47 -0
  132. package/ios/vendor/CSecp256k1/src/modinv64_impl.h +780 -0
  133. package/ios/vendor/CSecp256k1/src/modules/recovery/main_impl.h +159 -0
  134. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.c +16456 -0
  135. package/ios/vendor/CSecp256k1/src/precomputed_ecmult.h +38 -0
  136. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.c +1779 -0
  137. package/ios/vendor/CSecp256k1/src/precomputed_ecmult_gen.h +26 -0
  138. package/ios/vendor/CSecp256k1/src/scalar.h +105 -0
  139. package/ios/vendor/CSecp256k1/src/scalar_4x64.h +19 -0
  140. package/ios/vendor/CSecp256k1/src/scalar_4x64_impl.h +1000 -0
  141. package/ios/vendor/CSecp256k1/src/scalar_8x32.h +19 -0
  142. package/ios/vendor/CSecp256k1/src/scalar_8x32_impl.h +816 -0
  143. package/ios/vendor/CSecp256k1/src/scalar_impl.h +321 -0
  144. package/ios/vendor/CSecp256k1/src/scalar_low.h +24 -0
  145. package/ios/vendor/CSecp256k1/src/scalar_low_impl.h +206 -0
  146. package/ios/vendor/CSecp256k1/src/scratch.h +44 -0
  147. package/ios/vendor/CSecp256k1/src/scratch_impl.h +99 -0
  148. package/ios/vendor/CSecp256k1/src/secp256k1.c +831 -0
  149. package/ios/vendor/CSecp256k1/src/selftest.h +32 -0
  150. package/ios/vendor/CSecp256k1/src/util.h +451 -0
  151. package/ios/vendor/GENERATED.md +4 -0
  152. package/package.json +65 -3
  153. package/src/authonContext.ts +30 -0
  154. package/src/client.ts +375 -0
  155. package/src/errors.ts +52 -0
  156. package/src/index.ts +21 -0
  157. package/src/native.ts +50 -0
  158. package/src/types.ts +120 -0
  159. package/src/useAuthonWallet.ts +103 -0
@@ -0,0 +1,83 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/JsValues.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import kotlinx.serialization.json.JsonArray
5
+ import kotlinx.serialization.json.JsonElement
6
+ import kotlinx.serialization.json.JsonNull
7
+ import kotlinx.serialization.json.JsonObject
8
+ import kotlinx.serialization.json.JsonPrimitive
9
+ import kotlinx.serialization.json.booleanOrNull
10
+ import java.math.BigDecimal
11
+ import java.math.BigInteger
12
+
13
+ /** Conversions that follow JavaScript semantics where the frame relies on them. */
14
+ internal object JsValues {
15
+ /** `BigInt(value)`: integers, decimal strings, 0x/0o/0b strings; "" is 0. Null when JS would throw. */
16
+ fun toBigIntOrNull(value: Any?): BigInteger? = when (value) {
17
+ null -> null
18
+ is BigInteger -> value
19
+ is Int -> BigInteger.valueOf(value.toLong())
20
+ is Long -> BigInteger.valueOf(value)
21
+ is Short -> BigInteger.valueOf(value.toLong())
22
+ is Byte -> BigInteger.valueOf(value.toLong())
23
+ is Double, is Float -> numberToBigInt(BigDecimal(value.toString()))
24
+ is BigDecimal -> numberToBigInt(value)
25
+ is Boolean -> if (value) BigInteger.ONE else BigInteger.ZERO
26
+ is String -> stringToBigInt(value)
27
+ is JsonNull -> null
28
+ is JsonPrimitive ->
29
+ if (value.isString) stringToBigInt(value.content)
30
+ else value.booleanOrNull?.let { if (it) BigInteger.ONE else BigInteger.ZERO }
31
+ ?: runCatching { numberToBigInt(BigDecimal(value.content)) }.getOrNull()
32
+ else -> null
33
+ }
34
+
35
+ private fun numberToBigInt(d: BigDecimal): BigInteger? = try {
36
+ d.toBigIntegerExact()
37
+ } catch (_: ArithmeticException) {
38
+ null
39
+ }
40
+
41
+ private fun stringToBigInt(raw: String): BigInteger? {
42
+ val s = raw.trim()
43
+ if (s.isEmpty()) return BigInteger.ZERO
44
+ val radix = when {
45
+ s.startsWith("0x") || s.startsWith("0X") -> 16
46
+ s.startsWith("0o") || s.startsWith("0O") -> 8
47
+ s.startsWith("0b") || s.startsWith("0B") -> 2
48
+ else -> 10
49
+ }
50
+ return try {
51
+ if (radix == 10) {
52
+ if (!Regex("^[+-]?[0-9]+$").matches(s)) null else BigInteger(s)
53
+ } else {
54
+ val body = s.substring(2)
55
+ if (body.isEmpty() || body.any { Character.digit(it, radix) < 0 }) null else BigInteger(body, radix)
56
+ }
57
+ } catch (_: NumberFormatException) {
58
+ null
59
+ }
60
+ }
61
+
62
+ /** Converts Kotlin maps/lists/primitives (e.g. from a React Native bridge) to JSON. */
63
+ fun toJson(value: Any?): JsonElement = when (value) {
64
+ null -> JsonNull
65
+ is JsonElement -> value
66
+ is String -> JsonPrimitive(value)
67
+ is Boolean -> JsonPrimitive(value)
68
+ is BigInteger -> JsonPrimitive(value.toString())
69
+ is Number -> {
70
+ val d = value.toDouble()
71
+ if ((value is Double || value is Float) && d == Math.floor(d) && !d.isInfinite() && Math.abs(d) < 1e21) {
72
+ JsonPrimitive(BigDecimal(d).toBigInteger())
73
+ } else {
74
+ JsonPrimitive(value)
75
+ }
76
+ }
77
+ is Map<*, *> -> JsonObject(value.entries.associate { (k, v) -> k.toString() to toJson(v) })
78
+ is Iterable<*> -> JsonArray(value.map { toJson(it) })
79
+ is Array<*> -> JsonArray(value.map { toJson(it) })
80
+ is ByteArray -> JsonPrimitive("0x" + Bytes.toHex(value))
81
+ else -> throw walletError(WalletErrorCode.INVALID_REQUEST, "unsupported value type ${value::class.java.simpleName}")
82
+ }
83
+ }
@@ -0,0 +1,232 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/KeystoreDeviceStore.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import android.content.Context
5
+ import android.content.pm.PackageManager
6
+ import android.os.Build
7
+ import android.security.keystore.KeyGenParameterSpec
8
+ import android.security.keystore.KeyInfo
9
+ import android.security.keystore.KeyProperties
10
+ import android.security.keystore.StrongBoxUnavailableException
11
+ import android.util.AtomicFile
12
+ import kotlinx.coroutines.Dispatchers
13
+ import kotlinx.coroutines.withContext
14
+ import kotlinx.serialization.json.Json
15
+ import kotlinx.serialization.json.JsonObject
16
+ import kotlinx.serialization.json.JsonPrimitive
17
+ import kotlinx.serialization.json.buildJsonObject
18
+ import kotlinx.serialization.json.contentOrNull
19
+ import kotlinx.serialization.json.intOrNull
20
+ import kotlinx.serialization.json.longOrNull
21
+ import kotlinx.serialization.json.put
22
+ import java.io.File
23
+ import java.io.IOException
24
+ import java.security.Key
25
+ import java.security.KeyFactory
26
+ import java.security.KeyPairGenerator
27
+ import java.security.KeyStore
28
+ import java.security.PrivateKey
29
+ import java.security.Signature
30
+ import java.security.interfaces.ECPublicKey
31
+ import java.security.spec.ECGenParameterSpec
32
+ import java.util.UUID
33
+ import javax.crypto.KeyGenerator
34
+ import javax.crypto.SecretKey
35
+ import javax.crypto.SecretKeyFactory
36
+
37
+ /** Where a Keystore key lives, as reported by KeyInfo. */
38
+ enum class KeySecurityLevel { STRONGBOX, TRUSTED_ENVIRONMENT, UNKNOWN_SECURE, SOFTWARE, UNKNOWN }
39
+
40
+ /**
41
+ * Device share storage in the Android Keystore (NATIVE.md section 3).
42
+ * - Signing key: P-256, non-exportable, StrongBox when available, hardware TEE accepted.
43
+ * Software-only keys are refused unless [allowSoftwareKeys] (tests and emulators only).
44
+ * - D is sealed with a Keystore AES-256-GCM key, AAD = storage key `projectId\0userId`, and written
45
+ * with AtomicFile to `noBackupFilesDir`, so it is not part of cloud backups or device transfers.
46
+ * Every write is read back and compared.
47
+ */
48
+ class KeystoreDeviceStore @JvmOverloads constructor(
49
+ context: Context,
50
+ private val allowSoftwareKeys: Boolean = false,
51
+ ) : DeviceStore {
52
+ private val appContext = context.applicationContext
53
+ private val dir = File(appContext.noBackupFilesDir, "authon-wallet")
54
+ private val hasStrongBox = Build.VERSION.SDK_INT >= 28 &&
55
+ appContext.packageManager.hasSystemFeature(PackageManager.FEATURE_STRONGBOX_KEYSTORE)
56
+
57
+ private val keyStore: KeyStore by lazy { KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) } }
58
+
59
+ private inner class KeystoreKeys(override val handle: String, override val publicJwk: DevicePublicJwk) : DeviceKeys {
60
+ private fun signKey(): PrivateKey = keyStore.getKey("$handle.sign", null) as? PrivateKey
61
+ ?: throw walletError(WalletErrorCode.NEEDS_RESTORE, "device key missing")
62
+
63
+ private fun aesKey(): SecretKey = keyStore.getKey("$handle.share", null) as? SecretKey
64
+ ?: throw walletError(WalletErrorCode.NEEDS_RESTORE, "device share key missing")
65
+
66
+ override fun sign(data: ByteArray): ByteArray {
67
+ val s = Signature.getInstance("SHA256withECDSA")
68
+ s.initSign(signKey())
69
+ s.update(data)
70
+ return DeviceStorage.derToRaw(s.sign())
71
+ }
72
+
73
+ override fun seal(plain: ByteArray, aad: ByteArray): SealedShare = DeviceStorage.gcmSeal(aesKey(), plain, aad)
74
+
75
+ override fun open(sealed: SealedShare, aad: ByteArray): ByteArray = DeviceStorage.gcmOpen(aesKey(), sealed, aad)
76
+ }
77
+
78
+ /** Security level of the signing key behind [keys] (diagnostics and tests). */
79
+ fun securityLevelOf(keys: DeviceKeys): KeySecurityLevel = securityLevel(keyStore.getKey("${keys.handle}.sign", null))
80
+
81
+ override suspend fun generateKeys(): DeviceKeys = withContext(Dispatchers.IO) {
82
+ val handle = "dev.authon.wallet.${UUID.randomUUID()}"
83
+ try {
84
+ val pair = withStrongBoxFallback { strongBox ->
85
+ val spec = KeyGenParameterSpec.Builder("$handle.sign", KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY)
86
+ .setAlgorithmParameterSpec(ECGenParameterSpec("secp256r1"))
87
+ .setDigests(KeyProperties.DIGEST_SHA256)
88
+ .apply { if (strongBox) setIsStrongBoxBacked(true) }
89
+ .build()
90
+ KeyPairGenerator.getInstance(KeyProperties.KEY_ALGORITHM_EC, ANDROID_KEYSTORE).apply { initialize(spec) }.generateKeyPair()
91
+ }
92
+ val aes = withStrongBoxFallback { strongBox ->
93
+ val spec = KeyGenParameterSpec.Builder("$handle.share", KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT)
94
+ .setKeySize(256)
95
+ .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
96
+ .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
97
+ .apply { if (strongBox) setIsStrongBoxBacked(true) }
98
+ .build()
99
+ KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEYSTORE).apply { init(spec) }.generateKey()
100
+ }
101
+ if (!allowSoftwareKeys) {
102
+ val levels = listOf(securityLevel(pair.private), securityLevel(aes))
103
+ if (levels.any { it == KeySecurityLevel.SOFTWARE || it == KeySecurityLevel.UNKNOWN }) {
104
+ deleteAliases(handle)
105
+ throw walletError(WalletErrorCode.STORAGE_UNAVAILABLE, "this device has no hardware-backed keystore")
106
+ }
107
+ }
108
+ KeystoreKeys(handle, DeviceStorage.jwkOf(pair.public as ECPublicKey))
109
+ } catch (e: WalletException) {
110
+ throw e
111
+ } catch (e: Exception) {
112
+ deleteAliases(handle)
113
+ throw WalletException(WalletErrorCode.STORAGE_UNAVAILABLE, "device keys could not be created", e)
114
+ }
115
+ }
116
+
117
+ private fun <T> withStrongBoxFallback(block: (Boolean) -> T): T {
118
+ if (hasStrongBox) {
119
+ try {
120
+ return block(true)
121
+ } catch (_: StrongBoxUnavailableException) {
122
+ // fall through to the TEE
123
+ }
124
+ }
125
+ return block(false)
126
+ }
127
+
128
+ private fun securityLevel(key: Key?): KeySecurityLevel {
129
+ if (key == null) return KeySecurityLevel.UNKNOWN
130
+ val info: KeyInfo = try {
131
+ when (key) {
132
+ is PrivateKey -> KeyFactory.getInstance(key.algorithm, ANDROID_KEYSTORE).getKeySpec(key, KeyInfo::class.java)
133
+ is SecretKey -> SecretKeyFactory.getInstance(key.algorithm, ANDROID_KEYSTORE).getKeySpec(key, KeyInfo::class.java) as KeyInfo
134
+ else -> return KeySecurityLevel.UNKNOWN
135
+ }
136
+ } catch (_: Exception) {
137
+ return KeySecurityLevel.UNKNOWN
138
+ }
139
+ if (Build.VERSION.SDK_INT >= 31) {
140
+ return when (info.securityLevel) {
141
+ KeyProperties.SECURITY_LEVEL_STRONGBOX -> KeySecurityLevel.STRONGBOX
142
+ KeyProperties.SECURITY_LEVEL_TRUSTED_ENVIRONMENT -> KeySecurityLevel.TRUSTED_ENVIRONMENT
143
+ KeyProperties.SECURITY_LEVEL_UNKNOWN_SECURE -> KeySecurityLevel.UNKNOWN_SECURE
144
+ KeyProperties.SECURITY_LEVEL_SOFTWARE -> KeySecurityLevel.SOFTWARE
145
+ else -> KeySecurityLevel.UNKNOWN
146
+ }
147
+ }
148
+ @Suppress("DEPRECATION")
149
+ return if (info.isInsideSecureHardware) KeySecurityLevel.UNKNOWN_SECURE else KeySecurityLevel.SOFTWARE
150
+ }
151
+
152
+ private fun deleteAliases(handle: String) {
153
+ for (alias in listOf("$handle.sign", "$handle.share")) runCatching { keyStore.deleteEntry(alias) }
154
+ }
155
+
156
+ private fun fileFor(key: String) = AtomicFile(File(dir, Bytes.toHex(Bytes.sha256(Bytes.utf8(key))) + ".json"))
157
+
158
+ override suspend fun discardKeys(keys: DeviceKeys) = withContext(Dispatchers.IO) { deleteAliases(keys.handle) }
159
+
160
+ override suspend fun get(key: String): DeviceRecord? = withContext(Dispatchers.IO) { read(key) }
161
+
162
+ private fun read(key: String): DeviceRecord? {
163
+ val file = fileFor(key)
164
+ if (!file.baseFile.exists()) return null
165
+ val o = try {
166
+ Json.parseToJsonElement(String(file.readFully(), Charsets.UTF_8)) as? JsonObject
167
+ } catch (e: Exception) {
168
+ throw WalletException(WalletErrorCode.STORAGE_UNAVAILABLE, "device record unreadable", e)
169
+ } ?: return null
170
+ fun s(k: String) = (o[k] as? JsonPrimitive)?.contentOrNull
171
+ if (s("key") != key) return null
172
+ val handle = s("handle") ?: return null
173
+ val jwk = o["publicJwk"] as? JsonObject ?: return null
174
+ return DeviceRecord(
175
+ key = key,
176
+ keys = KeystoreKeys(
177
+ handle,
178
+ DevicePublicJwk((jwk["x"] as JsonPrimitive).content, (jwk["y"] as JsonPrimitive).content),
179
+ ),
180
+ iv = Bytes.fromBase64(s("iv") ?: return null),
181
+ ciphertext = Bytes.fromBase64(s("ciphertext") ?: return null),
182
+ deviceId = s("deviceId") ?: return null,
183
+ walletId = s("walletId") ?: return null,
184
+ address = s("address") ?: return null,
185
+ shareVersion = (o["shareVersion"] as? JsonPrimitive)?.intOrNull ?: return null,
186
+ createdAt = (o["createdAt"] as? JsonPrimitive)?.longOrNull ?: 0L,
187
+ )
188
+ }
189
+
190
+ override suspend fun put(record: DeviceRecord) = withContext(Dispatchers.IO) {
191
+ val previous = runCatching { read(record.key) }.getOrNull()
192
+ val bytes = buildJsonObject {
193
+ put("v", 1)
194
+ put("key", record.key)
195
+ put("handle", record.keys.handle)
196
+ put("publicJwk", record.keys.publicJwk.toJson())
197
+ put("iv", Bytes.toBase64(record.iv))
198
+ put("ciphertext", Bytes.toBase64(record.ciphertext))
199
+ put("deviceId", record.deviceId)
200
+ put("walletId", record.walletId)
201
+ put("address", record.address)
202
+ put("shareVersion", record.shareVersion)
203
+ put("createdAt", record.createdAt)
204
+ }.toString().toByteArray(Charsets.UTF_8)
205
+ val file = fileFor(record.key)
206
+ try {
207
+ if (!dir.exists() && !dir.mkdirs()) throw IOException("cannot create ${dir.name}")
208
+ val out = file.startWrite()
209
+ try {
210
+ out.write(bytes)
211
+ file.finishWrite(out)
212
+ } catch (e: IOException) {
213
+ file.failWrite(out)
214
+ throw e
215
+ }
216
+ if (!file.readFully().contentEquals(bytes)) throw IOException("read-back differs")
217
+ } catch (e: IOException) {
218
+ throw WalletException(WalletErrorCode.STORAGE_UNAVAILABLE, "device share could not be stored", e)
219
+ }
220
+ if (previous != null && previous.keys.handle != record.keys.handle) deleteAliases(previous.keys.handle)
221
+ }
222
+
223
+ override suspend fun delete(key: String) = withContext(Dispatchers.IO) {
224
+ val previous = runCatching { read(key) }.getOrNull()
225
+ fileFor(key).delete()
226
+ if (previous != null) deleteAliases(previous.keys.handle)
227
+ }
228
+
229
+ private companion object {
230
+ const val ANDROID_KEYSTORE = "AndroidKeyStore"
231
+ }
232
+ }
@@ -0,0 +1,111 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Prf.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import javax.crypto.Cipher
5
+ import javax.crypto.Mac
6
+ import javax.crypto.spec.GCMParameterSpec
7
+ import javax.crypto.spec.SecretKeySpec
8
+
9
+ /** A WebAuthn assertion, base64url fields, as the API verifies it. */
10
+ data class PasskeyAssertion(
11
+ val credentialId: String,
12
+ val clientDataJSON: String,
13
+ val authenticatorData: String,
14
+ val signature: String,
15
+ )
16
+
17
+ class PasskeyCreateResult(
18
+ val credentialId: String,
19
+ /** 32-byte PRF output. The SDK wipes it after use. */
20
+ val prfOutput: ByteArray,
21
+ /** SPKI public key (base64url) when the provider reports it. */
22
+ val publicKey: String?,
23
+ /** COSE algorithm of [publicKey] (-7 ES256, -257 RS256, -8 EdDSA). */
24
+ val publicKeyAlg: Int?,
25
+ /** The raw attestationObject (base64url); the API reads the COSE key from it when [publicKey] is null. */
26
+ val attestationObject: String?,
27
+ )
28
+
29
+ class PasskeyGetResult(val prfOutput: ByteArray, val assertion: PasskeyAssertion)
30
+
31
+ /** Passkey operations with the PRF extension for RP `wallet.authon.dev`. */
32
+ interface PasskeyProvider {
33
+ suspend fun create(userId: ByteArray, userName: String, salt: ByteArray): PasskeyCreateResult
34
+
35
+ /** [challenge] is the server's restore challenge when the API verifies the assertion. */
36
+ suspend fun get(credentialId: String, salt: ByteArray, challenge: ByteArray?): PasskeyGetResult
37
+ }
38
+
39
+ /** Backup encryption of `apps/wallet/src/prf.ts`: PRF salt v1/v2, HKDF-SHA256, AES-256-GCM, AAD, `v2.` prefix. */
40
+ internal object Prf {
41
+ const val PRF_SALT_LABEL = "authon-wallet-backup-v1"
42
+ const val PRF_SALT_LABEL_V2 = "authon-wallet-backup-v2"
43
+ const val DEFAULT_WRITE_BACKUP_VERSION = 1
44
+ private const val HKDF_INFO = "authon-wallet-backup-v1/aes-gcm-256"
45
+ private const val V2_PREFIX = "v2."
46
+
47
+ fun passkeyUserId(userHandle: String): ByteArray = Bytes.sha256(Bytes.utf8(userHandle))
48
+
49
+ fun salt(version: Int, projectId: String?, walletId: String?): ByteArray =
50
+ if (version == 1) {
51
+ Bytes.sha256(Bytes.utf8(PRF_SALT_LABEL))
52
+ } else {
53
+ if (projectId == null || walletId == null) throw walletError(WalletErrorCode.INTERNAL, "backup context required")
54
+ Bytes.sha256(Bytes.utf8("$PRF_SALT_LABEL_V2\u0000$projectId\u0000$walletId"))
55
+ }
56
+
57
+ fun backupVersionOf(payload: String): Int = if (payload.startsWith(V2_PREFIX)) 2 else 1
58
+
59
+ /** RFC 5869 HKDF-SHA256 with an empty salt, one output block (the AES-256 key). */
60
+ fun deriveBackupKey(prfOutput: ByteArray): ByteArray = hkdfSha256(prfOutput, ByteArray(0), Bytes.utf8(HKDF_INFO), 32)
61
+
62
+ fun hkdfSha256(ikm: ByteArray, salt: ByteArray, info: ByteArray, length: Int): ByteArray {
63
+ require(length in 1..255 * 32)
64
+ val extract = Mac.getInstance("HmacSHA256")
65
+ // An empty salt means HashLen zero bytes (RFC 5869 2.2); JCA rejects empty HMAC keys.
66
+ extract.init(SecretKeySpec(if (salt.isEmpty()) ByteArray(32) else salt, "HmacSHA256"))
67
+ val prk = extract.doFinal(ikm)
68
+ val expand = Mac.getInstance("HmacSHA256")
69
+ expand.init(SecretKeySpec(prk, "HmacSHA256"))
70
+ val out = ByteArray(length)
71
+ var t = ByteArray(0)
72
+ var offset = 0
73
+ var counter = 1
74
+ while (offset < length) {
75
+ expand.update(t)
76
+ expand.update(info)
77
+ expand.update(counter.toByte())
78
+ t = expand.doFinal()
79
+ val n = minOf(t.size, length - offset)
80
+ t.copyInto(out, offset, 0, n)
81
+ offset += n
82
+ counter++
83
+ }
84
+ Bytes.wipe(prk, t)
85
+ return out
86
+ }
87
+
88
+ private fun aad(walletId: String, version: Int) = Bytes.utf8(if (version == 1) "backup:$walletId" else "backup:v2:$walletId")
89
+
90
+ fun encryptBackup(key: ByteArray, share: ByteArray, walletId: String, version: Int, iv: ByteArray = Bytes.randomBytes(12)): String {
91
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
92
+ cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, iv))
93
+ cipher.updateAAD(aad(walletId, version))
94
+ val body = Bytes.toBase64(Bytes.concat(iv, cipher.doFinal(share)))
95
+ return if (version == 1) body else V2_PREFIX + body
96
+ }
97
+
98
+ fun decryptBackup(key: ByteArray, payload: String, walletId: String): ByteArray {
99
+ val version = backupVersionOf(payload)
100
+ val raw = Bytes.fromBase64(if (version == 1) payload else payload.substring(V2_PREFIX.length))
101
+ if (raw.size < 12 + 16) throw walletError(WalletErrorCode.INVALID_REQUEST, "backup ciphertext malformed")
102
+ return try {
103
+ val cipher = Cipher.getInstance("AES/GCM/NoPadding")
104
+ cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key, "AES"), GCMParameterSpec(128, raw, 0, 12))
105
+ cipher.updateAAD(aad(walletId, version))
106
+ cipher.doFinal(raw, 12, raw.size - 12)
107
+ } catch (e: Exception) {
108
+ throw WalletException(WalletErrorCode.INVALID_REQUEST, "backup cannot be decrypted with this passkey", e)
109
+ }
110
+ }
111
+ }
@@ -0,0 +1,134 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Proof.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import kotlinx.serialization.json.JsonArray
5
+ import kotlinx.serialization.json.JsonObject
6
+ import kotlinx.serialization.json.JsonPrimitive
7
+ import kotlinx.serialization.json.buildJsonArray
8
+ import kotlinx.serialization.json.buildJsonObject
9
+ import kotlinx.serialization.json.put
10
+ import java.nio.ByteBuffer
11
+ import java.nio.CharBuffer
12
+ import java.nio.charset.CharacterCodingException
13
+ import java.nio.charset.CodingErrorAction
14
+
15
+ /** P-256 device public key as the API stores it (JWK, base64url x/y without padding). */
16
+ data class DevicePublicJwk(val x: String, val y: String) {
17
+ val kty: String get() = "EC"
18
+ val crv: String get() = "P-256"
19
+
20
+ fun toJson(): JsonObject = buildJsonObject {
21
+ put("kty", kty)
22
+ put("crv", crv)
23
+ put("x", x)
24
+ put("y", y)
25
+ }
26
+ }
27
+
28
+ /** Server-issued proof nonce (`expiresAt` in Unix seconds). */
29
+ data class ProofNonce(val nonce: String, val expiresAt: Long)
30
+
31
+ /** Wallet key proofs and reserved-message rules of `apps/wallet/src/proof.ts`. */
32
+ internal object Proof {
33
+ const val DOMAIN_NAME = "Authon Wallet Proof"
34
+ const val DOMAIN_VERSION = "1"
35
+ private val RESERVED_PREFIXES = listOf("create:", "device:", "restore:")
36
+
37
+ fun canonicalJwk(jwk: DevicePublicJwk): String =
38
+ "{\"crv\":${json(jwk.crv)},\"kty\":${json(jwk.kty)},\"x\":${json(jwk.x)},\"y\":${json(jwk.y)}}"
39
+
40
+ fun devicePublicKeyHash(jwk: DevicePublicJwk): String = Bytes.toHex(Bytes.sha256(Bytes.utf8(canonicalJwk(jwk))))
41
+
42
+ private fun json(s: String): String = JsonPrimitive(s).toString()
43
+
44
+ private fun fields(vararg pairs: Pair<String, String>): JsonArray = buildJsonArray {
45
+ for ((name, type) in pairs) add(buildJsonObject { put("name", name); put("type", type) })
46
+ }
47
+
48
+ private val BOUND = arrayOf("nonce" to "string", "expiresAt" to "uint256")
49
+
50
+ private val TYPES: Map<String, JsonArray> = mapOf(
51
+ "CreateWallet" to fields("projectId" to "string", "userId" to "string", "deviceKeyHash" to "string", *BOUND),
52
+ "AddDevice" to fields("walletId" to "string", "shareVersion" to "uint256", "deviceKeyHash" to "string", *BOUND),
53
+ "RestoreWallet" to fields("walletId" to "string", "shareVersion" to "uint256", "deviceKeyHash" to "string", *BOUND),
54
+ )
55
+
56
+ fun typedData(primaryType: String, message: List<Pair<String, String>>, bound: ProofNonce): JsonObject = buildJsonObject {
57
+ put("domain", buildJsonObject { put("name", DOMAIN_NAME); put("version", DOMAIN_VERSION) })
58
+ put("types", buildJsonObject { put(primaryType, TYPES.getValue(primaryType)) })
59
+ put("primaryType", primaryType)
60
+ put(
61
+ "message",
62
+ buildJsonObject {
63
+ for ((k, v) in message) put(k, v)
64
+ put("nonce", bound.nonce)
65
+ put("expiresAt", bound.expiresAt.toString())
66
+ },
67
+ )
68
+ }
69
+
70
+ fun createTypedData(projectId: String, userId: String, deviceKey: DevicePublicJwk, bound: ProofNonce) =
71
+ typedData("CreateWallet", listOf("projectId" to projectId, "userId" to userId, "deviceKeyHash" to devicePublicKeyHash(deviceKey)), bound)
72
+
73
+ fun deviceTypedData(walletId: String, shareVersion: Int, deviceKey: DevicePublicJwk, bound: ProofNonce) =
74
+ typedData(
75
+ "AddDevice",
76
+ listOf("walletId" to walletId, "shareVersion" to shareVersion.toString(), "deviceKeyHash" to devicePublicKeyHash(deviceKey)),
77
+ bound,
78
+ )
79
+
80
+ /** shareVersion in the signed message is the current version + 1. */
81
+ fun restoreTypedData(walletId: String, currentShareVersion: Int, newDeviceKey: DevicePublicJwk, bound: ProofNonce) =
82
+ typedData(
83
+ "RestoreWallet",
84
+ listOf(
85
+ "walletId" to walletId,
86
+ "shareVersion" to (currentShareVersion + 1).toString(),
87
+ "deviceKeyHash" to devicePublicKeyHash(newDeviceKey),
88
+ ),
89
+ bound,
90
+ )
91
+
92
+ /** Proofs are always signed with account 0 of the wallet secret. */
93
+ fun sign(secret: ByteArray, typedData: JsonObject): String = HdWallet.account(secret, 0).signTypedData(typedData)
94
+
95
+ /** True for app text that looks like a (legacy) wallet proof. */
96
+ fun isReservedMessageText(text: String): Boolean {
97
+ var i = 0
98
+ while (i < text.length && (text[i].code <= 0x1f || text[i] == '' || isJsWhitespace(text[i]))) i++
99
+ val t = text.substring(i).lowercase()
100
+ return RESERVED_PREFIXES.any { t.startsWith(it) }
101
+ }
102
+
103
+ /** JavaScript `\s`: WhiteSpace and LineTerminator code points. */
104
+ private fun isJsWhitespace(c: Char): Boolean =
105
+ c == ' ' || c == '\t' || c == '\n' || c == '\u000b' || c == '\u000c' || c == '\r' || c == ' ' || c == ' ' ||
106
+ c in ' '..' ' || c == '
' || c == '
' || c == ' ' || c == ' ' || c == ' ' ||
107
+ c == ''
108
+
109
+ /** Decodes a raw hex message as strict UTF-8; null when it is not valid UTF-8 or not hex. */
110
+ fun rawMessageText(hex: String): String? {
111
+ val bytes = Bytes.fromHexOrNull(hex) ?: return null
112
+ return try {
113
+ Charsets.UTF_8.newDecoder()
114
+ .onMalformedInput(CodingErrorAction.REPORT)
115
+ .onUnmappableCharacter(CodingErrorAction.REPORT)
116
+ .decode(ByteBuffer.wrap(bytes))
117
+ .let { stripBom(it) }
118
+ } catch (_: CharacterCodingException) {
119
+ null
120
+ }
121
+ }
122
+
123
+ // TextDecoder('utf-8') drops a leading BOM by default.
124
+ private fun stripBom(chars: CharBuffer): String {
125
+ val s = chars.toString()
126
+ return if (s.startsWith("")) s.substring(1) else s
127
+ }
128
+
129
+ fun isReservedTypedData(typedData: JsonObject): Boolean {
130
+ val name = ((typedData["domain"] as? JsonObject)?.get("name") as? JsonPrimitive)?.takeIf { it.isString }?.content
131
+ ?: return false
132
+ return name.trim().lowercase() == DOMAIN_NAME.lowercase()
133
+ }
134
+ }
@@ -0,0 +1,106 @@
1
+ // GENERATED by scripts/sync-native-wallet.mjs from kotlin/authon/src/main/kotlin/dev/authon/sdk/wallet/Shamir.kt. Do not edit: change the source and run the script.
2
+ package dev.authon.sdk.wallet
3
+
4
+ import java.security.SecureRandom
5
+
6
+ /**
7
+ * Port of the `shamir-secret-sharing` JS library used by the web frame: GF(2^8) with the
8
+ * same tables, a share is `y bytes || x` (x in the last byte). [combine] is deterministic and
9
+ * byte-identical to the library; [split] draws its coefficients and coordinates from SecureRandom.
10
+ */
11
+ internal object Shamir {
12
+ private val random = SecureRandom()
13
+
14
+ private fun add(a: Int, b: Int): Int = a xor b
15
+
16
+ private fun div(a: Int, b: Int): Int {
17
+ require(b != 0) { "cannot divide by zero" }
18
+ val diff = (GfTables.LOG[a] - GfTables.LOG[b] + 255) % 255
19
+ return if (a == 0) 0 else GfTables.EXP[diff]
20
+ }
21
+
22
+ private fun mult(a: Int, b: Int): Int {
23
+ val sum = (GfTables.LOG[a] + GfTables.LOG[b]) % 255
24
+ return if (a == 0 || b == 0) 0 else GfTables.EXP[sum]
25
+ }
26
+
27
+ private fun interpolate(xs: IntArray, ys: IntArray, x: Int): Int {
28
+ var result = 0
29
+ for (i in xs.indices) {
30
+ var basis = 1
31
+ for (j in xs.indices) {
32
+ if (i == j) continue
33
+ basis = mult(basis, div(add(x, xs[j]), add(xs[i], xs[j])))
34
+ }
35
+ result = add(result, mult(ys[i], basis))
36
+ }
37
+ return result
38
+ }
39
+
40
+ private fun evaluate(coefficients: IntArray, x: Int, degree: Int): Int {
41
+ require(x != 0) { "cannot evaluate secret polynomial at zero" }
42
+ var result = coefficients[degree]
43
+ for (i in degree - 1 downTo 0) result = add(mult(result, x), coefficients[i])
44
+ return result
45
+ }
46
+
47
+ private fun newCoordinates(): IntArray {
48
+ val coordinates = IntArray(255) { it + 1 }
49
+ val randomIndices = ByteArray(255).also { random.nextBytes(it) }
50
+ for (i in 0 until 255) {
51
+ val j = (randomIndices[i].toInt() and 0xff) % 255
52
+ val tmp = coordinates[i]
53
+ coordinates[i] = coordinates[j]
54
+ coordinates[j] = tmp
55
+ }
56
+ return coordinates
57
+ }
58
+
59
+ fun split(secret: ByteArray, shares: Int, threshold: Int): List<ByteArray> {
60
+ require(secret.isNotEmpty()) { "secret cannot be empty" }
61
+ require(shares in 2..255) { "shares must be at least 2 and at most 255" }
62
+ require(threshold in 2..255) { "threshold must be at least 2 and at most 255" }
63
+ require(shares >= threshold) { "shares cannot be less than threshold" }
64
+ val xs = newCoordinates()
65
+ val result = List(shares) { i -> ByteArray(secret.size + 1).also { it[secret.size] = xs[i].toByte() } }
66
+ val degree = threshold - 1
67
+ val coefficients = IntArray(degree + 1)
68
+ val randomCoefficients = ByteArray(degree)
69
+ try {
70
+ for (i in secret.indices) {
71
+ coefficients[0] = secret[i].toInt() and 0xff
72
+ random.nextBytes(randomCoefficients)
73
+ for (k in 0 until degree) coefficients[k + 1] = randomCoefficients[k].toInt() and 0xff
74
+ for (j in 0 until shares) result[j][i] = evaluate(coefficients, xs[j], degree).toByte()
75
+ }
76
+ } finally {
77
+ coefficients.fill(0)
78
+ randomCoefficients.fill(0)
79
+ }
80
+ return result
81
+ }
82
+
83
+ fun combine(shares: List<ByteArray>): ByteArray {
84
+ require(shares.size in 2..255) { "shares must have at least 2 and at most 255 elements" }
85
+ val shareLength = shares[0].size
86
+ for (s in shares) {
87
+ require(s.size >= 2) { "each share must be at least 2 bytes" }
88
+ require(s.size == shareLength) { "all shares must have the same byte length" }
89
+ }
90
+ val xs = IntArray(shares.size)
91
+ val seen = HashSet<Int>()
92
+ for (i in shares.indices) {
93
+ val x = shares[i][shareLength - 1].toInt() and 0xff
94
+ require(seen.add(x)) { "shares must contain unique values but a duplicate was found" }
95
+ xs[i] = x
96
+ }
97
+ val secret = ByteArray(shareLength - 1)
98
+ val ys = IntArray(shares.size)
99
+ for (i in secret.indices) {
100
+ for (j in shares.indices) ys[j] = shares[j][i].toInt() and 0xff
101
+ secret[i] = interpolate(xs, ys, 0).toByte()
102
+ }
103
+ ys.fill(0)
104
+ return secret
105
+ }
106
+ }