react-native-wallet-keystore 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.
- package/LICENSE +20 -0
- package/README.md +376 -0
- package/WalletKeystore.podspec +29 -0
- package/android/build.gradle +62 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/walletkeystore/Secp256k1.kt +158 -0
- package/android/src/main/java/com/walletkeystore/WalletKeystoreCrypto.kt +233 -0
- package/android/src/main/java/com/walletkeystore/WalletKeystoreModule.kt +627 -0
- package/android/src/main/java/com/walletkeystore/WalletKeystorePackage.kt +31 -0
- package/ios/WalletKeystore.h +5 -0
- package/ios/WalletKeystore.mm +895 -0
- package/lib/module/NativeWalletKeystore.js +15 -0
- package/lib/module/NativeWalletKeystore.js.map +1 -0
- package/lib/module/errors.js +57 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +9 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/keystore.js +220 -0
- package/lib/module/keystore.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/viem.js +65 -0
- package/lib/module/viem.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeWalletKeystore.d.ts +26 -0
- package/lib/typescript/src/NativeWalletKeystore.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +29 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +3 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/keystore.d.ts +100 -0
- package/lib/typescript/src/keystore.d.ts.map +1 -0
- package/lib/typescript/src/viem.d.ts +24 -0
- package/lib/typescript/src/viem.d.ts.map +1 -0
- package/package.json +210 -0
- package/src/NativeWalletKeystore.ts +43 -0
- package/src/errors.ts +94 -0
- package/src/index.tsx +6 -0
- package/src/keystore.ts +317 -0
- package/src/viem.ts +99 -0
package/package.json
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-wallet-keystore",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Hardware-protected secp256k1 wallet keys for React Native \u2014 Secure Enclave and Android Keystore, with a viem adapter",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"react-native-wallet-keystore-source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./viem": {
|
|
14
|
+
"react-native-wallet-keystore-source": "./src/viem.ts",
|
|
15
|
+
"types": "./lib/typescript/src/viem.d.ts",
|
|
16
|
+
"default": "./lib/module/viem.js"
|
|
17
|
+
},
|
|
18
|
+
"./package.json": "./package.json"
|
|
19
|
+
},
|
|
20
|
+
"typesVersions": {
|
|
21
|
+
"*": {
|
|
22
|
+
"viem": [
|
|
23
|
+
"./lib/typescript/src/viem.d.ts"
|
|
24
|
+
]
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src",
|
|
29
|
+
"lib",
|
|
30
|
+
"android",
|
|
31
|
+
"ios",
|
|
32
|
+
"cpp",
|
|
33
|
+
"*.podspec",
|
|
34
|
+
"react-native.config.js",
|
|
35
|
+
"!ios/build",
|
|
36
|
+
"!android/build",
|
|
37
|
+
"!android/gradle",
|
|
38
|
+
"!android/gradlew",
|
|
39
|
+
"!android/gradlew.bat",
|
|
40
|
+
"!android/local.properties",
|
|
41
|
+
"!**/__tests__",
|
|
42
|
+
"!**/__fixtures__",
|
|
43
|
+
"!**/__mocks__",
|
|
44
|
+
"!**/.*"
|
|
45
|
+
],
|
|
46
|
+
"scripts": {
|
|
47
|
+
"example": "yarn workspace react-native-wallet-keystore-example",
|
|
48
|
+
"clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib",
|
|
49
|
+
"prepare": "bob build",
|
|
50
|
+
"typecheck": "tsc",
|
|
51
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
52
|
+
"test": "jest",
|
|
53
|
+
"release": "release-it --only-version"
|
|
54
|
+
},
|
|
55
|
+
"keywords": [
|
|
56
|
+
"react-native",
|
|
57
|
+
"ios",
|
|
58
|
+
"android",
|
|
59
|
+
"ethereum",
|
|
60
|
+
"wallet",
|
|
61
|
+
"secp256k1",
|
|
62
|
+
"viem",
|
|
63
|
+
"secure-enclave",
|
|
64
|
+
"android-keystore",
|
|
65
|
+
"biometrics",
|
|
66
|
+
"signing",
|
|
67
|
+
"keychain",
|
|
68
|
+
"web3"
|
|
69
|
+
],
|
|
70
|
+
"repository": {
|
|
71
|
+
"type": "git",
|
|
72
|
+
"url": "git+https://github.com/miroosama/react-native-wallet-keystore.git"
|
|
73
|
+
},
|
|
74
|
+
"author": "Osama Miro <o.miro650@gmail.com> (https://github.com/miroosama)",
|
|
75
|
+
"license": "MIT",
|
|
76
|
+
"bugs": {
|
|
77
|
+
"url": "https://github.com/miroosama/react-native-wallet-keystore/issues"
|
|
78
|
+
},
|
|
79
|
+
"homepage": "https://github.com/miroosama/react-native-wallet-keystore#readme",
|
|
80
|
+
"publishConfig": {
|
|
81
|
+
"registry": "https://registry.npmjs.org/"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@commitlint/config-conventional": "^21.2.0",
|
|
85
|
+
"@eslint/compat": "^2.1.0",
|
|
86
|
+
"@eslint/eslintrc": "^3.3.6",
|
|
87
|
+
"@eslint/js": "^9.39.5",
|
|
88
|
+
"@jest/globals": "^29.7.0",
|
|
89
|
+
"@react-native/babel-preset": "0.86.2",
|
|
90
|
+
"@react-native/eslint-config": "0.86.2",
|
|
91
|
+
"@react-native/jest-preset": "0.86.2",
|
|
92
|
+
"@release-it/conventional-changelog": "^12.0.0",
|
|
93
|
+
"@types/react": "^19.2.0",
|
|
94
|
+
"commitlint": "^21.2.1",
|
|
95
|
+
"del-cli": "^7.0.0",
|
|
96
|
+
"eslint": "^9.39.5",
|
|
97
|
+
"eslint-config-prettier": "^10.1.8",
|
|
98
|
+
"eslint-plugin-ft-flow": "^3.0.11",
|
|
99
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
100
|
+
"jest": "^29.7.0",
|
|
101
|
+
"lefthook": "^2.1.10",
|
|
102
|
+
"prettier": "^3.9.6",
|
|
103
|
+
"react": "19.2.3",
|
|
104
|
+
"react-native": "0.86.2",
|
|
105
|
+
"react-native-builder-bob": "^0.43.1",
|
|
106
|
+
"release-it": "^21.0.1",
|
|
107
|
+
"turbo": "^2.10.8",
|
|
108
|
+
"typescript": "^6.0.3",
|
|
109
|
+
"viem": "^2.56.8"
|
|
110
|
+
},
|
|
111
|
+
"peerDependencies": {
|
|
112
|
+
"react": "*",
|
|
113
|
+
"react-native": "*",
|
|
114
|
+
"viem": ">=2.0.0"
|
|
115
|
+
},
|
|
116
|
+
"workspaces": [
|
|
117
|
+
"example"
|
|
118
|
+
],
|
|
119
|
+
"engines": {
|
|
120
|
+
"node": ">=22.12"
|
|
121
|
+
},
|
|
122
|
+
"packageManager": "yarn@4.11.0",
|
|
123
|
+
"react-native-builder-bob": {
|
|
124
|
+
"source": "src",
|
|
125
|
+
"output": "lib",
|
|
126
|
+
"targets": [
|
|
127
|
+
[
|
|
128
|
+
"module",
|
|
129
|
+
{
|
|
130
|
+
"esm": true
|
|
131
|
+
}
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
"typescript",
|
|
135
|
+
{
|
|
136
|
+
"project": "tsconfig.build.json"
|
|
137
|
+
}
|
|
138
|
+
]
|
|
139
|
+
]
|
|
140
|
+
},
|
|
141
|
+
"codegenConfig": {
|
|
142
|
+
"name": "WalletKeystoreSpec",
|
|
143
|
+
"type": "modules",
|
|
144
|
+
"jsSrcsDir": "src",
|
|
145
|
+
"android": {
|
|
146
|
+
"javaPackageName": "com.walletkeystore"
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
"prettier": {
|
|
150
|
+
"quoteProps": "consistent",
|
|
151
|
+
"singleQuote": true,
|
|
152
|
+
"tabWidth": 2,
|
|
153
|
+
"trailingComma": "es5",
|
|
154
|
+
"useTabs": false
|
|
155
|
+
},
|
|
156
|
+
"jest": {
|
|
157
|
+
"preset": "@react-native/jest-preset",
|
|
158
|
+
"testEnvironmentOptions": {
|
|
159
|
+
"customExportConditions": [
|
|
160
|
+
"require",
|
|
161
|
+
"react-native",
|
|
162
|
+
"react-native-wallet-keystore-source"
|
|
163
|
+
]
|
|
164
|
+
},
|
|
165
|
+
"modulePathIgnorePatterns": [
|
|
166
|
+
"<rootDir>/example/node_modules",
|
|
167
|
+
"<rootDir>/lib/"
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
"commitlint": {
|
|
171
|
+
"extends": [
|
|
172
|
+
"@commitlint/config-conventional"
|
|
173
|
+
]
|
|
174
|
+
},
|
|
175
|
+
"release-it": {
|
|
176
|
+
"git": {
|
|
177
|
+
"commitMessage": "chore: release ${version}",
|
|
178
|
+
"tagName": "v${version}"
|
|
179
|
+
},
|
|
180
|
+
"npm": {
|
|
181
|
+
"publish": true
|
|
182
|
+
},
|
|
183
|
+
"github": {
|
|
184
|
+
"release": true
|
|
185
|
+
},
|
|
186
|
+
"plugins": {
|
|
187
|
+
"@release-it/conventional-changelog": {
|
|
188
|
+
"preset": {
|
|
189
|
+
"name": "angular"
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
"create-react-native-library": {
|
|
195
|
+
"type": "turbo-module",
|
|
196
|
+
"languages": "kotlin-objc",
|
|
197
|
+
"tools": [
|
|
198
|
+
"eslint",
|
|
199
|
+
"jest",
|
|
200
|
+
"lefthook",
|
|
201
|
+
"release-it"
|
|
202
|
+
],
|
|
203
|
+
"version": "0.63.1"
|
|
204
|
+
},
|
|
205
|
+
"peerDependenciesMeta": {
|
|
206
|
+
"viem": {
|
|
207
|
+
"optional": true
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { TurboModuleRegistry, type TurboModule } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Codegen contract.
|
|
5
|
+
*
|
|
6
|
+
* Everything crossing the bridge is a plain `string` on purpose: codegen
|
|
7
|
+
* supports a narrow set of types, and string-literal unions are handled
|
|
8
|
+
* inconsistently across React Native versions. The typed surface lives in
|
|
9
|
+
* `keystore.ts`, which narrows these values.
|
|
10
|
+
*/
|
|
11
|
+
export interface Spec extends TurboModule {
|
|
12
|
+
getBiometryType(): Promise<string>;
|
|
13
|
+
authenticate(reason: string, policy: string): Promise<boolean>;
|
|
14
|
+
|
|
15
|
+
storeSecret(
|
|
16
|
+
keyId: string,
|
|
17
|
+
secretHex: string,
|
|
18
|
+
policy: string,
|
|
19
|
+
invalidation: string
|
|
20
|
+
): Promise<void>;
|
|
21
|
+
getSecret(keyId: string, reason: string): Promise<string>;
|
|
22
|
+
hasSecret(keyId: string): Promise<boolean>;
|
|
23
|
+
deleteSecret(keyId: string): Promise<void>;
|
|
24
|
+
|
|
25
|
+
generateKey(
|
|
26
|
+
keyId: string,
|
|
27
|
+
policy: string,
|
|
28
|
+
invalidation: string
|
|
29
|
+
): Promise<string>;
|
|
30
|
+
importPrivateKey(
|
|
31
|
+
keyId: string,
|
|
32
|
+
privateKeyHex: string,
|
|
33
|
+
policy: string,
|
|
34
|
+
invalidation: string
|
|
35
|
+
): Promise<string>;
|
|
36
|
+
getPublicKey(keyId: string): Promise<string>;
|
|
37
|
+
|
|
38
|
+
/** Returns 65 bytes of hex: `r || s || v`, low-s normalized, `v` of 27/28. */
|
|
39
|
+
signDigest(keyId: string, digestHex: string, reason: string): Promise<string>;
|
|
40
|
+
exportPrivateKey(keyId: string, reason: string): Promise<string>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export default TurboModuleRegistry.getEnforcing<Spec>('WalletKeystore');
|
package/src/errors.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Stable, cross-platform error codes.
|
|
3
|
+
*
|
|
4
|
+
* The distinctions are load-bearing: `USER_CANCELED` means re-prompt,
|
|
5
|
+
* `NOT_ENROLLED` means send the user to Settings, and `KEY_INVALIDATED` means
|
|
6
|
+
* the secret is gone and recovery must start. Collapsing them makes callers
|
|
7
|
+
* guess.
|
|
8
|
+
*/
|
|
9
|
+
const CODES = [
|
|
10
|
+
'NOT_AVAILABLE',
|
|
11
|
+
'NOT_ENROLLED',
|
|
12
|
+
'USER_CANCELED',
|
|
13
|
+
'USER_FALLBACK',
|
|
14
|
+
'LOCKOUT',
|
|
15
|
+
'LOCKOUT_PERMANENT',
|
|
16
|
+
'SYSTEM_CANCEL',
|
|
17
|
+
'KEY_NOT_FOUND',
|
|
18
|
+
'KEY_ALREADY_EXISTS',
|
|
19
|
+
'KEY_INVALIDATED',
|
|
20
|
+
'STORAGE_ERROR',
|
|
21
|
+
'INVALID_KEY',
|
|
22
|
+
'UNKNOWN',
|
|
23
|
+
] as const;
|
|
24
|
+
|
|
25
|
+
export type KeystoreErrorCode = (typeof CODES)[number];
|
|
26
|
+
|
|
27
|
+
const KNOWN_CODES: ReadonlySet<string> = new Set(CODES);
|
|
28
|
+
|
|
29
|
+
export class KeystoreError extends Error {
|
|
30
|
+
readonly code: KeystoreErrorCode;
|
|
31
|
+
|
|
32
|
+
/** The raw native code, kept even when `code` has fallen back to `UNKNOWN`. */
|
|
33
|
+
readonly nativeCode?: string;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
code: KeystoreErrorCode,
|
|
37
|
+
message: string,
|
|
38
|
+
options?: { nativeCode?: string; cause?: unknown }
|
|
39
|
+
) {
|
|
40
|
+
super(message);
|
|
41
|
+
this.name = 'KeystoreError';
|
|
42
|
+
this.code = code;
|
|
43
|
+
this.nativeCode = options?.nativeCode;
|
|
44
|
+
|
|
45
|
+
if (options && 'cause' in options) {
|
|
46
|
+
// Assigned rather than passed to super() to support pre-ES2022 targets.
|
|
47
|
+
(this as { cause?: unknown }).cause = options.cause;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Without this, extending a built-in breaks `instanceof` once compiled to
|
|
51
|
+
// ES5 — silently, and only for consumers.
|
|
52
|
+
Object.setPrototypeOf(this, KeystoreError.prototype);
|
|
53
|
+
|
|
54
|
+
const capture = (
|
|
55
|
+
Error as unknown as {
|
|
56
|
+
captureStackTrace?: (t: object, c?: unknown) => void;
|
|
57
|
+
}
|
|
58
|
+
).captureStackTrace;
|
|
59
|
+
if (typeof capture === 'function') {
|
|
60
|
+
capture(this, KeystoreError);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function isKeystoreErrorCode(value: unknown): value is KeystoreErrorCode {
|
|
66
|
+
return typeof value === 'string' && KNOWN_CODES.has(value);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Normalizes any rejection into a `KeystoreError`.
|
|
71
|
+
*
|
|
72
|
+
* React Native surfaces `reject(code, message)` as an `Error` carrying `code`,
|
|
73
|
+
* but a JS-side failure can produce any shape at all, so everything degrades to
|
|
74
|
+
* `UNKNOWN` rather than throwing while building the error.
|
|
75
|
+
*/
|
|
76
|
+
export function toKeystoreError(value: unknown): KeystoreError {
|
|
77
|
+
if (value instanceof KeystoreError) {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const raw = (value as { code?: unknown } | null | undefined)?.code;
|
|
82
|
+
const message =
|
|
83
|
+
(value as { message?: unknown } | null | undefined)?.message ??
|
|
84
|
+
'The keystore operation failed.';
|
|
85
|
+
|
|
86
|
+
return new KeystoreError(
|
|
87
|
+
isKeystoreErrorCode(raw) ? raw : 'UNKNOWN',
|
|
88
|
+
typeof message === 'string' ? message : String(message),
|
|
89
|
+
{
|
|
90
|
+
nativeCode: typeof raw === 'string' ? raw : undefined,
|
|
91
|
+
cause: value,
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from './keystore';
|
|
2
|
+
export { KeystoreError, type KeystoreErrorCode } from './errors';
|
|
3
|
+
|
|
4
|
+
// The viem adapter is intentionally not re-exported. It ships as the
|
|
5
|
+
// `react-native-wallet-keystore/viem` subpath with viem as an optional peer, so
|
|
6
|
+
// importing the core API never requires viem to be installed.
|
package/src/keystore.ts
ADDED
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
import NativeWalletKeystore from './NativeWalletKeystore';
|
|
2
|
+
import { KeystoreError, toKeystoreError } from './errors';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Android reports which biometric *hardware* exists, not which modality is
|
|
6
|
+
* enrolled, so it returns `'biometric'` when more than one is present.
|
|
7
|
+
*/
|
|
8
|
+
export type BiometryType =
|
|
9
|
+
| 'faceId'
|
|
10
|
+
| 'touchId'
|
|
11
|
+
| 'opticId'
|
|
12
|
+
| 'fingerprint'
|
|
13
|
+
| 'face'
|
|
14
|
+
| 'iris'
|
|
15
|
+
| 'biometric'
|
|
16
|
+
| 'none';
|
|
17
|
+
|
|
18
|
+
export type AuthPolicy = 'biometricOnly' | 'biometricOrPasscode';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether the wrapping key is destroyed when biometric enrollment changes.
|
|
22
|
+
*
|
|
23
|
+
* Orthogonal to {@link AuthPolicy} on purpose: `'biometricOnly'` must never
|
|
24
|
+
* imply invalidation, or a user adding a fingerprint silently destroys their
|
|
25
|
+
* wallet. Opting in is explicit.
|
|
26
|
+
*/
|
|
27
|
+
export type InvalidationPolicy = 'onEnrollmentChange' | 'never';
|
|
28
|
+
|
|
29
|
+
export const DEFAULT_AUTH_POLICY: AuthPolicy = 'biometricOrPasscode';
|
|
30
|
+
export const DEFAULT_INVALIDATION_POLICY: InvalidationPolicy = 'never';
|
|
31
|
+
|
|
32
|
+
/** Uncompressed SEC1 public key: `0x04` followed by 64 bytes. */
|
|
33
|
+
export type PublicKeyHex = `0x${string}`;
|
|
34
|
+
|
|
35
|
+
/** 65-byte Ethereum signature: `r || s || v`, with `v` 27 or 28. */
|
|
36
|
+
export type SignatureHex = `0x${string}`;
|
|
37
|
+
|
|
38
|
+
type KeyOptions = {
|
|
39
|
+
policy?: AuthPolicy;
|
|
40
|
+
invalidation?: InvalidationPolicy;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const BIOMETRY_TYPES: ReadonlySet<string> = new Set<BiometryType>([
|
|
44
|
+
'faceId',
|
|
45
|
+
'touchId',
|
|
46
|
+
'opticId',
|
|
47
|
+
'fingerprint',
|
|
48
|
+
'face',
|
|
49
|
+
'iris',
|
|
50
|
+
'biometric',
|
|
51
|
+
'none',
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
const HEX_PATTERN = /^(?:[0-9a-fA-F]{2})+$/;
|
|
55
|
+
const HEX_32_BYTES = /^(?:0x)?[0-9a-fA-F]{64}$/;
|
|
56
|
+
|
|
57
|
+
function strip0x(value: string): string {
|
|
58
|
+
return value.startsWith('0x') || value.startsWith('0X')
|
|
59
|
+
? value.slice(2)
|
|
60
|
+
: value;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function prefix0x(value: string): `0x${string}` {
|
|
64
|
+
return (value.startsWith('0x') ? value : `0x${value}`) as `0x${string}`;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function assertKeyId(keyId: string): void {
|
|
68
|
+
if (typeof keyId !== 'string' || keyId.trim() === '') {
|
|
69
|
+
throw new KeystoreError('UNKNOWN', 'A non-empty `keyId` is required.');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function assertReason(reason: string, action: string): void {
|
|
74
|
+
// iOS raises on an empty localizedReason rather than failing gracefully, so
|
|
75
|
+
// this is checked before the bridge to keep both platforms consistent.
|
|
76
|
+
if (typeof reason !== 'string' || reason.trim() === '') {
|
|
77
|
+
throw new KeystoreError(
|
|
78
|
+
'UNKNOWN',
|
|
79
|
+
`A non-empty \`reason\` is required to ${action}.`
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Which biometric modality the hardware supports, whether or not anything is
|
|
86
|
+
* enrolled. Resolves `'none'` when there is no hardware; never rejects.
|
|
87
|
+
*
|
|
88
|
+
* Enrollment state comes from {@link authenticate} via `NOT_ENROLLED`.
|
|
89
|
+
*/
|
|
90
|
+
export async function getBiometryType(): Promise<BiometryType> {
|
|
91
|
+
const type = await NativeWalletKeystore.getBiometryType();
|
|
92
|
+
// A native layer newer than this JS could return a modality we don't know.
|
|
93
|
+
return (BIOMETRY_TYPES.has(type) ? type : 'biometric') as BiometryType;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Prompts for device-owner authentication.
|
|
98
|
+
*
|
|
99
|
+
* A UX gate, not a security boundary — the boolean can be faked by a
|
|
100
|
+
* compromised bundle. Use {@link signDigest} where it actually matters.
|
|
101
|
+
*
|
|
102
|
+
* Never resolves `false`; every failure rejects.
|
|
103
|
+
*
|
|
104
|
+
* @throws {KeystoreError}
|
|
105
|
+
*/
|
|
106
|
+
export async function authenticate(
|
|
107
|
+
reason: string,
|
|
108
|
+
policy: AuthPolicy = DEFAULT_AUTH_POLICY
|
|
109
|
+
): Promise<boolean> {
|
|
110
|
+
try {
|
|
111
|
+
assertReason(reason, 'authenticate');
|
|
112
|
+
return await NativeWalletKeystore.authenticate(reason, policy);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw toKeystoreError(error);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Stores a secret encrypted under a hardware-bound wrapping key.
|
|
120
|
+
*
|
|
121
|
+
* @throws {KeystoreError} `KEY_ALREADY_EXISTS` if `keyId` is taken; overwriting
|
|
122
|
+
* is always deliberate.
|
|
123
|
+
*/
|
|
124
|
+
export async function storeSecret(
|
|
125
|
+
keyId: string,
|
|
126
|
+
secretHex: string,
|
|
127
|
+
options: KeyOptions = {}
|
|
128
|
+
): Promise<void> {
|
|
129
|
+
try {
|
|
130
|
+
assertKeyId(keyId);
|
|
131
|
+
|
|
132
|
+
if (typeof secretHex !== 'string' || !HEX_PATTERN.test(secretHex)) {
|
|
133
|
+
throw new KeystoreError(
|
|
134
|
+
'UNKNOWN',
|
|
135
|
+
'`secretHex` must be a non-empty, even-length hex string.'
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
await NativeWalletKeystore.storeSecret(
|
|
140
|
+
keyId,
|
|
141
|
+
secretHex,
|
|
142
|
+
options.policy ?? DEFAULT_AUTH_POLICY,
|
|
143
|
+
options.invalidation ?? DEFAULT_INVALIDATION_POLICY
|
|
144
|
+
);
|
|
145
|
+
} catch (error) {
|
|
146
|
+
throw toKeystoreError(error);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Authenticates and returns the stored secret as hex.
|
|
152
|
+
*
|
|
153
|
+
* The prompt is raised by the keystore as a precondition of using the wrapping
|
|
154
|
+
* key, so it cannot be bypassed from JS.
|
|
155
|
+
*
|
|
156
|
+
* @throws {KeystoreError} `KEY_NOT_FOUND`, `KEY_INVALIDATED`, or any auth code.
|
|
157
|
+
*/
|
|
158
|
+
export async function getSecret(
|
|
159
|
+
keyId: string,
|
|
160
|
+
reason: string
|
|
161
|
+
): Promise<string> {
|
|
162
|
+
try {
|
|
163
|
+
assertKeyId(keyId);
|
|
164
|
+
assertReason(reason, 'read a secret');
|
|
165
|
+
return await NativeWalletKeystore.getSecret(keyId, reason);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw toKeystoreError(error);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** Whether a secret is stored under `keyId`. Does not authenticate. */
|
|
172
|
+
export async function hasSecret(keyId: string): Promise<boolean> {
|
|
173
|
+
try {
|
|
174
|
+
assertKeyId(keyId);
|
|
175
|
+
return await NativeWalletKeystore.hasSecret(keyId);
|
|
176
|
+
} catch (error) {
|
|
177
|
+
throw toKeystoreError(error);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Removes the secret and its wrapping key. Idempotent. */
|
|
182
|
+
export async function deleteSecret(keyId: string): Promise<void> {
|
|
183
|
+
try {
|
|
184
|
+
assertKeyId(keyId);
|
|
185
|
+
await NativeWalletKeystore.deleteSecret(keyId);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
throw toKeystoreError(error);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Generates a secp256k1 keypair in hardware-wrapped storage.
|
|
193
|
+
*
|
|
194
|
+
* Entropy comes from the platform CSPRNG, never from JavaScript, and the
|
|
195
|
+
* private key never crosses the bridge.
|
|
196
|
+
*
|
|
197
|
+
* @returns The uncompressed public key.
|
|
198
|
+
*/
|
|
199
|
+
export async function generateKey(
|
|
200
|
+
keyId: string,
|
|
201
|
+
options: KeyOptions = {}
|
|
202
|
+
): Promise<PublicKeyHex> {
|
|
203
|
+
try {
|
|
204
|
+
assertKeyId(keyId);
|
|
205
|
+
return prefix0x(
|
|
206
|
+
await NativeWalletKeystore.generateKey(
|
|
207
|
+
keyId,
|
|
208
|
+
options.policy ?? DEFAULT_AUTH_POLICY,
|
|
209
|
+
options.invalidation ?? DEFAULT_INVALIDATION_POLICY
|
|
210
|
+
)
|
|
211
|
+
);
|
|
212
|
+
} catch (error) {
|
|
213
|
+
throw toKeystoreError(error);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Imports an existing secp256k1 private key.
|
|
219
|
+
*
|
|
220
|
+
* @throws {KeystoreError} `INVALID_KEY` unless the key is in [1, n-1]. Keys
|
|
221
|
+
* outside that range are rejected rather than clamped.
|
|
222
|
+
*/
|
|
223
|
+
export async function importPrivateKey(
|
|
224
|
+
keyId: string,
|
|
225
|
+
privateKeyHex: string,
|
|
226
|
+
options: KeyOptions = {}
|
|
227
|
+
): Promise<PublicKeyHex> {
|
|
228
|
+
try {
|
|
229
|
+
assertKeyId(keyId);
|
|
230
|
+
|
|
231
|
+
if (
|
|
232
|
+
typeof privateKeyHex !== 'string' ||
|
|
233
|
+
!HEX_32_BYTES.test(privateKeyHex)
|
|
234
|
+
) {
|
|
235
|
+
throw new KeystoreError(
|
|
236
|
+
'INVALID_KEY',
|
|
237
|
+
'A private key must be exactly 32 bytes of hex.'
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// The range check stays native, where the curve order is already at hand.
|
|
242
|
+
return prefix0x(
|
|
243
|
+
await NativeWalletKeystore.importPrivateKey(
|
|
244
|
+
keyId,
|
|
245
|
+
strip0x(privateKeyHex),
|
|
246
|
+
options.policy ?? DEFAULT_AUTH_POLICY,
|
|
247
|
+
options.invalidation ?? DEFAULT_INVALIDATION_POLICY
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
} catch (error) {
|
|
251
|
+
throw toKeystoreError(error);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** The uncompressed public key. Does not authenticate. */
|
|
256
|
+
export async function getPublicKey(keyId: string): Promise<PublicKeyHex> {
|
|
257
|
+
try {
|
|
258
|
+
assertKeyId(keyId);
|
|
259
|
+
return prefix0x(await NativeWalletKeystore.getPublicKey(keyId));
|
|
260
|
+
} catch (error) {
|
|
261
|
+
throw toKeystoreError(error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Authenticates, then signs a 32-byte digest.
|
|
267
|
+
*
|
|
268
|
+
* Only a digest crosses the boundary — never a message or a transaction.
|
|
269
|
+
* Keccak and EIP-191/712/155 encoding stay in JS, which keeps this module
|
|
270
|
+
* curve-specific but chain-agnostic.
|
|
271
|
+
*
|
|
272
|
+
* @returns 65 bytes, `r || s || v`, low-s normalized per EIP-2 with `v` of
|
|
273
|
+
* 27/28 — byte-identical to viem.
|
|
274
|
+
*/
|
|
275
|
+
export async function signDigest(
|
|
276
|
+
keyId: string,
|
|
277
|
+
digestHex: string,
|
|
278
|
+
reason: string
|
|
279
|
+
): Promise<SignatureHex> {
|
|
280
|
+
try {
|
|
281
|
+
assertKeyId(keyId);
|
|
282
|
+
|
|
283
|
+
if (typeof digestHex !== 'string' || !HEX_32_BYTES.test(digestHex)) {
|
|
284
|
+
throw new KeystoreError(
|
|
285
|
+
'INVALID_KEY',
|
|
286
|
+
'A digest must be exactly 32 bytes of hex. Hash the message in JS first.'
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
assertReason(reason, 'sign');
|
|
291
|
+
|
|
292
|
+
return prefix0x(
|
|
293
|
+
await NativeWalletKeystore.signDigest(keyId, strip0x(digestHex), reason)
|
|
294
|
+
);
|
|
295
|
+
} catch (error) {
|
|
296
|
+
throw toKeystoreError(error);
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Authenticates, then returns the raw private key.
|
|
302
|
+
*
|
|
303
|
+
* For user-initiated backup only. The result lands in the JS heap where it
|
|
304
|
+
* cannot be zeroed, so prefer {@link signDigest} for everyday use.
|
|
305
|
+
*/
|
|
306
|
+
export async function exportPrivateKey(
|
|
307
|
+
keyId: string,
|
|
308
|
+
reason: string
|
|
309
|
+
): Promise<string> {
|
|
310
|
+
try {
|
|
311
|
+
assertKeyId(keyId);
|
|
312
|
+
assertReason(reason, 'export a private key');
|
|
313
|
+
return prefix0x(await NativeWalletKeystore.exportPrivateKey(keyId, reason));
|
|
314
|
+
} catch (error) {
|
|
315
|
+
throw toKeystoreError(error);
|
|
316
|
+
}
|
|
317
|
+
}
|