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
|
@@ -0,0 +1,895 @@
|
|
|
1
|
+
#import "WalletKeystore.h"
|
|
2
|
+
|
|
3
|
+
#import <LocalAuthentication/LocalAuthentication.h>
|
|
4
|
+
#import <Security/Security.h>
|
|
5
|
+
|
|
6
|
+
#import <secp256k1.h>
|
|
7
|
+
#import <secp256k1_recovery.h>
|
|
8
|
+
|
|
9
|
+
static NSString *const WKPolicyBiometricOnly = @"biometricOnly";
|
|
10
|
+
static NSString *const WKInvalidationOnEnrollmentChange = @"onEnrollmentChange";
|
|
11
|
+
|
|
12
|
+
static NSString *const WKCodeNotAvailable = @"NOT_AVAILABLE";
|
|
13
|
+
static NSString *const WKCodeNotEnrolled = @"NOT_ENROLLED";
|
|
14
|
+
static NSString *const WKCodeUserCanceled = @"USER_CANCELED";
|
|
15
|
+
static NSString *const WKCodeUserFallback = @"USER_FALLBACK";
|
|
16
|
+
static NSString *const WKCodeLockout = @"LOCKOUT";
|
|
17
|
+
static NSString *const WKCodeSystemCancel = @"SYSTEM_CANCEL";
|
|
18
|
+
static NSString *const WKCodeKeyNotFound = @"KEY_NOT_FOUND";
|
|
19
|
+
static NSString *const WKCodeKeyAlreadyExists = @"KEY_ALREADY_EXISTS";
|
|
20
|
+
static NSString *const WKCodeKeyInvalidated = @"KEY_INVALIDATED";
|
|
21
|
+
static NSString *const WKCodeStorageError = @"STORAGE_ERROR";
|
|
22
|
+
static NSString *const WKCodeUnknown = @"UNKNOWN";
|
|
23
|
+
|
|
24
|
+
static NSString *const WKCodeInvalidKey = @"INVALID_KEY";
|
|
25
|
+
|
|
26
|
+
static NSString *const WKKeychainService = @"com.walletkeystore.secret";
|
|
27
|
+
static NSString *const WKPublicKeyService = @"com.walletkeystore.publickey";
|
|
28
|
+
static NSString *const WKKeyTagPrefix = @"com.walletkeystore.wrap.";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Zeroes a buffer through a volatile pointer. A plain memset over memory that is
|
|
32
|
+
* never read again is dead-store-eliminated at -O3, leaving the key in place.
|
|
33
|
+
*/
|
|
34
|
+
static void WKSecureZero(void *buffer, size_t length)
|
|
35
|
+
{
|
|
36
|
+
if (buffer == NULL || length == 0) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
volatile unsigned char *p = (volatile unsigned char *)buffer;
|
|
40
|
+
while (length--) {
|
|
41
|
+
*p++ = 0;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Shared libsecp256k1 context — expensive to build, safe to share once
|
|
47
|
+
* randomized. Randomizing blinds against side-channel key recovery.
|
|
48
|
+
*/
|
|
49
|
+
static secp256k1_context *WKSecpContext(void)
|
|
50
|
+
{
|
|
51
|
+
static secp256k1_context *context = NULL;
|
|
52
|
+
static dispatch_once_t onceToken;
|
|
53
|
+
dispatch_once(&onceToken, ^{
|
|
54
|
+
context = secp256k1_context_create(SECP256K1_CONTEXT_SIGN | SECP256K1_CONTEXT_VERIFY);
|
|
55
|
+
uint8_t seed[32];
|
|
56
|
+
if (SecRandomCopyBytes(kSecRandomDefault, sizeof(seed), seed) == errSecSuccess) {
|
|
57
|
+
// Hardening only; the context still works if this fails.
|
|
58
|
+
(void)secp256k1_context_randomize(context, seed);
|
|
59
|
+
}
|
|
60
|
+
WKSecureZero(seed, sizeof(seed));
|
|
61
|
+
});
|
|
62
|
+
return context;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
@implementation WalletKeystore
|
|
66
|
+
|
|
67
|
+
#pragma mark - Error mapping
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Maps an LAError onto the cross-platform code set. No LOCKOUT_PERMANENT case:
|
|
71
|
+
* iOS resolves permanent lockout inside the prompt by demanding the passcode,
|
|
72
|
+
* so it never reaches the app.
|
|
73
|
+
*/
|
|
74
|
+
static NSString *WKCodeFromLAError(NSError *_Nullable error)
|
|
75
|
+
{
|
|
76
|
+
if (error == nil) {
|
|
77
|
+
return WKCodeUnknown;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
switch (error.code) {
|
|
81
|
+
case LAErrorBiometryNotAvailable:
|
|
82
|
+
return WKCodeNotAvailable;
|
|
83
|
+
|
|
84
|
+
// No passcode set means device-owner auth cannot succeed at all, which is
|
|
85
|
+
// an enrollment problem from the caller's point of view, not a fault.
|
|
86
|
+
case LAErrorBiometryNotEnrolled:
|
|
87
|
+
case LAErrorPasscodeNotSet:
|
|
88
|
+
return WKCodeNotEnrolled;
|
|
89
|
+
|
|
90
|
+
// appCancel is the app being backgrounded mid-prompt. Grouped with an
|
|
91
|
+
// explicit tap on Cancel because both mean "no answer, safe to re-prompt".
|
|
92
|
+
case LAErrorUserCancel:
|
|
93
|
+
case LAErrorAppCancel:
|
|
94
|
+
return WKCodeUserCanceled;
|
|
95
|
+
|
|
96
|
+
case LAErrorUserFallback:
|
|
97
|
+
return WKCodeUserFallback;
|
|
98
|
+
|
|
99
|
+
case LAErrorBiometryLockout:
|
|
100
|
+
return WKCodeLockout;
|
|
101
|
+
|
|
102
|
+
// systemCancel is the OS tearing down the prompt; notInteractive means no
|
|
103
|
+
// UI could be presented. Neither is user intent, so neither is a cancel.
|
|
104
|
+
case LAErrorSystemCancel:
|
|
105
|
+
case LAErrorNotInteractive:
|
|
106
|
+
return WKCodeSystemCancel;
|
|
107
|
+
|
|
108
|
+
default:
|
|
109
|
+
return WKCodeUnknown;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Maps a Keychain/SecKey OSStatus onto the cross-platform code set. */
|
|
114
|
+
static NSString *WKCodeFromOSStatus(OSStatus status)
|
|
115
|
+
{
|
|
116
|
+
switch (status) {
|
|
117
|
+
case errSecItemNotFound:
|
|
118
|
+
return WKCodeKeyNotFound;
|
|
119
|
+
|
|
120
|
+
case errSecDuplicateItem:
|
|
121
|
+
return WKCodeKeyAlreadyExists;
|
|
122
|
+
|
|
123
|
+
case errSecUserCanceled:
|
|
124
|
+
return WKCodeUserCanceled;
|
|
125
|
+
|
|
126
|
+
// No enrolled credential can satisfy the access control. Against a
|
|
127
|
+
// .biometryCurrentSet key this is permanent, not a retryable failure.
|
|
128
|
+
case errSecAuthFailed:
|
|
129
|
+
return WKCodeKeyInvalidated;
|
|
130
|
+
|
|
131
|
+
case errSecInteractionNotAllowed:
|
|
132
|
+
return WKCodeSystemCancel;
|
|
133
|
+
|
|
134
|
+
default:
|
|
135
|
+
return WKCodeStorageError;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* A SecKey CFError may carry an LAError or an OSStatus depending on whether
|
|
141
|
+
* auth or the keychain failed. Unwrap both, or a cancel reads as a storage bug.
|
|
142
|
+
*/
|
|
143
|
+
static NSString *WKCodeFromSecError(NSError *_Nullable error)
|
|
144
|
+
{
|
|
145
|
+
if (error == nil) {
|
|
146
|
+
return WKCodeUnknown;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if ([error.domain isEqualToString:LAErrorDomain]) {
|
|
150
|
+
return WKCodeFromLAError(error);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if ([error.domain isEqualToString:NSOSStatusErrorDomain]) {
|
|
154
|
+
return WKCodeFromOSStatus((OSStatus)error.code);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// The underlying error is where LocalAuthentication surfaces when the
|
|
158
|
+
// failure happened inside a SecKey operation rather than an LAContext.
|
|
159
|
+
NSError *underlying = error.userInfo[NSUnderlyingErrorKey];
|
|
160
|
+
if (underlying != nil && [underlying.domain isEqualToString:LAErrorDomain]) {
|
|
161
|
+
return WKCodeFromLAError(underlying);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return WKCodeUnknown;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
#pragma mark - Hex
|
|
168
|
+
|
|
169
|
+
static NSData *_Nullable WKDataFromHex(NSString *hex)
|
|
170
|
+
{
|
|
171
|
+
if (hex.length % 2 != 0) {
|
|
172
|
+
return nil;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
NSMutableData *data = [NSMutableData dataWithCapacity:hex.length / 2];
|
|
176
|
+
for (NSUInteger i = 0; i < hex.length; i += 2) {
|
|
177
|
+
unsigned int byte = 0;
|
|
178
|
+
NSString *pair = [hex substringWithRange:NSMakeRange(i, 2)];
|
|
179
|
+
if (![[NSScanner scannerWithString:pair] scanHexInt:&byte]) {
|
|
180
|
+
return nil;
|
|
181
|
+
}
|
|
182
|
+
uint8_t value = (uint8_t)byte;
|
|
183
|
+
[data appendBytes:&value length:1];
|
|
184
|
+
}
|
|
185
|
+
return data;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
static NSString *WKHexFromData(NSData *data)
|
|
189
|
+
{
|
|
190
|
+
const uint8_t *bytes = (const uint8_t *)data.bytes;
|
|
191
|
+
NSMutableString *hex = [NSMutableString stringWithCapacity:data.length * 2];
|
|
192
|
+
for (NSUInteger i = 0; i < data.length; i++) {
|
|
193
|
+
[hex appendFormat:@"%02x", bytes[i]];
|
|
194
|
+
}
|
|
195
|
+
return hex;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
#pragma mark - Key and item helpers
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Picks an ECIES variant the key accepts, preferring the variable-IV form that
|
|
202
|
+
* Secure Enclave documents. Driven by the private key: the public half accepts
|
|
203
|
+
* more, so choosing on it could store data that never decrypts.
|
|
204
|
+
*/
|
|
205
|
+
static SecKeyAlgorithm _Nullable WKECIESAlgorithm(SecKeyRef privateKey)
|
|
206
|
+
{
|
|
207
|
+
SecKeyAlgorithm candidates[] = {
|
|
208
|
+
kSecKeyAlgorithmECIESEncryptionCofactorVariableIVX963SHA256AESGCM,
|
|
209
|
+
kSecKeyAlgorithmECIESEncryptionCofactorX963SHA256AESGCM,
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
for (size_t i = 0; i < sizeof(candidates) / sizeof(candidates[0]); i++) {
|
|
213
|
+
if (SecKeyIsAlgorithmSupported(privateKey, kSecKeyOperationTypeDecrypt,
|
|
214
|
+
candidates[i])) {
|
|
215
|
+
return candidates[i];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return NULL;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
static NSData *WKKeyTag(NSString *keyId)
|
|
222
|
+
{
|
|
223
|
+
return [[WKKeyTagPrefix stringByAppendingString:keyId]
|
|
224
|
+
dataUsingEncoding:NSUTF8StringEncoding];
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Translates the two orthogonal policies into Secure Enclave access control.
|
|
229
|
+
*
|
|
230
|
+
* The invalidation axis is kept separate from the authenticator axis on
|
|
231
|
+
* purpose. .biometryCurrentSet destroys the key on any enrollment change, so
|
|
232
|
+
* letting 'biometricOnly' imply it would mean a user adding a fingerprint
|
|
233
|
+
* silently loses their wallet. It is opt-in via `invalidation` alone.
|
|
234
|
+
*/
|
|
235
|
+
static SecAccessControlCreateFlags WKAccessControlFlags(NSString *policy,
|
|
236
|
+
NSString *invalidation)
|
|
237
|
+
{
|
|
238
|
+
BOOL biometricOnly = [policy isEqualToString:WKPolicyBiometricOnly];
|
|
239
|
+
BOOL invalidates = [invalidation isEqualToString:WKInvalidationOnEnrollmentChange];
|
|
240
|
+
|
|
241
|
+
// Required for any Secure Enclave key that will perform private-key
|
|
242
|
+
// operations. Omitting it still creates the key and still allows public-key
|
|
243
|
+
// encryption, so the mistake only surfaces later, as "Operation is not
|
|
244
|
+
// allowed" on the first decrypt.
|
|
245
|
+
SecAccessControlCreateFlags flags = kSecAccessControlPrivateKeyUsage;
|
|
246
|
+
|
|
247
|
+
if (biometricOnly) {
|
|
248
|
+
return flags | (invalidates ? kSecAccessControlBiometryCurrentSet
|
|
249
|
+
: kSecAccessControlBiometryAny);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (invalidates) {
|
|
253
|
+
// Biometrics pinned to the current enrollment, but the passcode remains a
|
|
254
|
+
// route in — otherwise this would be indistinguishable from biometricOnly.
|
|
255
|
+
return flags | kSecAccessControlBiometryCurrentSet | kSecAccessControlOr |
|
|
256
|
+
kSecAccessControlDevicePasscode;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return flags | kSecAccessControlUserPresence;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
static SecKeyRef _Nullable WKCopyPrivateKey(NSString *keyId,
|
|
263
|
+
LAContext *_Nullable context,
|
|
264
|
+
OSStatus *outStatus)
|
|
265
|
+
{
|
|
266
|
+
NSMutableDictionary *query = [@{
|
|
267
|
+
(__bridge id)kSecClass : (__bridge id)kSecClassKey,
|
|
268
|
+
(__bridge id)kSecAttrApplicationTag : WKKeyTag(keyId),
|
|
269
|
+
(__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
|
|
270
|
+
(__bridge id)kSecReturnRef : @YES,
|
|
271
|
+
} mutableCopy];
|
|
272
|
+
|
|
273
|
+
if (context != nil) {
|
|
274
|
+
// Carries the prompt's reason, and is what makes the decryption below
|
|
275
|
+
// raise the system prompt rather than failing with interactionNotAllowed.
|
|
276
|
+
query[(__bridge id)kSecUseAuthenticationContext] = context;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
CFTypeRef result = NULL;
|
|
280
|
+
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
|
281
|
+
if (outStatus != NULL) {
|
|
282
|
+
*outStatus = status;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return status == errSecSuccess ? (SecKeyRef)result : NULL;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
static NSDictionary *WKCiphertextQuery(NSString *keyId)
|
|
289
|
+
{
|
|
290
|
+
return @{
|
|
291
|
+
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
|
|
292
|
+
(__bridge id)kSecAttrService : WKKeychainService,
|
|
293
|
+
(__bridge id)kSecAttrAccount : keyId,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
static NSDictionary *WKPublicKeyQuery(NSString *keyId)
|
|
298
|
+
{
|
|
299
|
+
return @{
|
|
300
|
+
(__bridge id)kSecClass : (__bridge id)kSecClassGenericPassword,
|
|
301
|
+
(__bridge id)kSecAttrService : WKPublicKeyService,
|
|
302
|
+
(__bridge id)kSecAttrAccount : keyId,
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
#pragma mark - v0.1 authentication
|
|
307
|
+
|
|
308
|
+
- (void)getBiometryType:(RCTPromiseResolveBlock)resolve
|
|
309
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
310
|
+
{
|
|
311
|
+
LAContext *context = [LAContext new];
|
|
312
|
+
|
|
313
|
+
// `biometryType` is unset until a policy has been evaluated, so this call is
|
|
314
|
+
// required even though the result is ignored: we want the hardware modality
|
|
315
|
+
// regardless of enrollment, and canEvaluatePolicy populates it either way.
|
|
316
|
+
NSError *error = nil;
|
|
317
|
+
[context canEvaluatePolicy:LAPolicyDeviceOwnerAuthenticationWithBiometrics
|
|
318
|
+
error:&error];
|
|
319
|
+
|
|
320
|
+
if (@available(iOS 17.0, *)) {
|
|
321
|
+
if (context.biometryType == LABiometryTypeOpticID) {
|
|
322
|
+
resolve(@"opticId");
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
switch (context.biometryType) {
|
|
328
|
+
case LABiometryTypeFaceID:
|
|
329
|
+
resolve(@"faceId");
|
|
330
|
+
return;
|
|
331
|
+
case LABiometryTypeTouchID:
|
|
332
|
+
resolve(@"touchId");
|
|
333
|
+
return;
|
|
334
|
+
default:
|
|
335
|
+
resolve(@"none");
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
- (void)authenticate:(NSString *)reason
|
|
341
|
+
policy:(NSString *)policy
|
|
342
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
343
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
344
|
+
{
|
|
345
|
+
// evaluatePolicy raises NSInvalidArgumentException on an empty reason rather
|
|
346
|
+
// than failing gracefully, so it is rejected before we get there.
|
|
347
|
+
if (reason.length == 0) {
|
|
348
|
+
reject(WKCodeUnknown, @"A non-empty `reason` is required to authenticate.", nil);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
LAPolicy laPolicy = [policy isEqualToString:WKPolicyBiometricOnly]
|
|
353
|
+
? LAPolicyDeviceOwnerAuthenticationWithBiometrics
|
|
354
|
+
: LAPolicyDeviceOwnerAuthentication;
|
|
355
|
+
|
|
356
|
+
// Fresh context per call: a reused LAContext can satisfy a later evaluation
|
|
357
|
+
// from a cached success, which would mean signing without authenticating.
|
|
358
|
+
LAContext *context = [LAContext new];
|
|
359
|
+
context.touchIDAuthenticationAllowableReuseDuration = 0;
|
|
360
|
+
|
|
361
|
+
NSError *availabilityError = nil;
|
|
362
|
+
if (![context canEvaluatePolicy:laPolicy error:&availabilityError]) {
|
|
363
|
+
// This is the only place NOT_AVAILABLE and NOT_ENROLLED can be told apart.
|
|
364
|
+
NSString *code = availabilityError ? WKCodeFromLAError(availabilityError)
|
|
365
|
+
: WKCodeNotAvailable;
|
|
366
|
+
NSString *message = availabilityError.localizedDescription
|
|
367
|
+
?: @"Authentication is not available on this device.";
|
|
368
|
+
reject(code, message, availabilityError);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
[context evaluatePolicy:laPolicy
|
|
373
|
+
localizedReason:reason
|
|
374
|
+
reply:^(BOOL success, NSError *_Nullable evaluateError) {
|
|
375
|
+
// This block runs on a private LocalAuthentication queue.
|
|
376
|
+
// The promise blocks are thread-safe, so settle directly
|
|
377
|
+
// rather than hopping to the main queue and stalling it
|
|
378
|
+
// behind the prompt.
|
|
379
|
+
if (success) {
|
|
380
|
+
resolve(@YES);
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
NSString *message = evaluateError.localizedDescription
|
|
385
|
+
?: @"Authentication failed.";
|
|
386
|
+
reject(WKCodeFromLAError(evaluateError), message, evaluateError);
|
|
387
|
+
}];
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
#pragma mark - v0.2 secret storage
|
|
391
|
+
|
|
392
|
+
- (void)storeSecret:(NSString *)keyId
|
|
393
|
+
secretHex:(NSString *)secretHex
|
|
394
|
+
policy:(NSString *)policy
|
|
395
|
+
invalidation:(NSString *)invalidation
|
|
396
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
397
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
398
|
+
{
|
|
399
|
+
[self storeSecretInternal:keyId
|
|
400
|
+
secretHex:secretHex
|
|
401
|
+
policy:policy
|
|
402
|
+
invalidation:invalidation
|
|
403
|
+
onSuccess:^{ resolve(nil); }
|
|
404
|
+
onError:^(NSString *code, NSString *message) {
|
|
405
|
+
reject(code, message, nil);
|
|
406
|
+
}];
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* Reused by generateKey and importPrivateKey, which need the outcome rather
|
|
411
|
+
* than a settled promise. Reporting through blocks keeps one copy of the
|
|
412
|
+
* wrapping logic instead of two that can drift.
|
|
413
|
+
*/
|
|
414
|
+
- (void)storeSecretInternal:(NSString *)keyId
|
|
415
|
+
secretHex:(NSString *)secretHex
|
|
416
|
+
policy:(NSString *)policy
|
|
417
|
+
invalidation:(NSString *)invalidation
|
|
418
|
+
onSuccess:(void (^)(void))onSuccess
|
|
419
|
+
onError:(void (^)(NSString *code, NSString *message))onError
|
|
420
|
+
{
|
|
421
|
+
NSData *secret = WKDataFromHex(secretHex);
|
|
422
|
+
if (secret == nil || secret.length == 0) {
|
|
423
|
+
onError(WKCodeUnknown, @"`secretHex` must be a non-empty hex string.");
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Overwriting a wallet key has to be deliberate, so an existing id is an
|
|
428
|
+
// error rather than a silent replace.
|
|
429
|
+
OSStatus existing = errSecSuccess;
|
|
430
|
+
SecKeyRef existingKey = WKCopyPrivateKey(keyId, nil, &existing);
|
|
431
|
+
if (existingKey != NULL) {
|
|
432
|
+
CFRelease(existingKey);
|
|
433
|
+
onError(WKCodeKeyAlreadyExists, @"A secret is already stored under this keyId.");
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
CFErrorRef acError = NULL;
|
|
438
|
+
SecAccessControlRef access = SecAccessControlCreateWithFlags(
|
|
439
|
+
kCFAllocatorDefault,
|
|
440
|
+
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
441
|
+
WKAccessControlFlags(policy, invalidation),
|
|
442
|
+
&acError);
|
|
443
|
+
|
|
444
|
+
if (access == NULL) {
|
|
445
|
+
NSError *error = CFBridgingRelease(acError);
|
|
446
|
+
onError(WKCodeFromSecError(error),
|
|
447
|
+
error.localizedDescription ?: @"Could not build access control.");
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
NSDictionary *attributes = @{
|
|
452
|
+
(__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
|
|
453
|
+
(__bridge id)kSecAttrKeySizeInBits : @256,
|
|
454
|
+
(__bridge id)kSecAttrTokenID : (__bridge id)kSecAttrTokenIDSecureEnclave,
|
|
455
|
+
(__bridge id)kSecPrivateKeyAttrs : @{
|
|
456
|
+
(__bridge id)kSecAttrIsPermanent : @YES,
|
|
457
|
+
(__bridge id)kSecAttrApplicationTag : WKKeyTag(keyId),
|
|
458
|
+
(__bridge id)kSecAttrAccessControl : (__bridge id)access,
|
|
459
|
+
},
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
CFErrorRef keyError = NULL;
|
|
463
|
+
SecKeyRef privateKey =
|
|
464
|
+
SecKeyCreateRandomKey((__bridge CFDictionaryRef)attributes, &keyError);
|
|
465
|
+
CFRelease(access);
|
|
466
|
+
|
|
467
|
+
if (privateKey == NULL) {
|
|
468
|
+
NSError *error = CFBridgingRelease(keyError);
|
|
469
|
+
onError(WKCodeFromSecError(error),
|
|
470
|
+
error.localizedDescription ?: @"Could not create the wrapping key.");
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
SecKeyAlgorithm algorithm = WKECIESAlgorithm(privateKey);
|
|
475
|
+
SecKeyRef publicKey = SecKeyCopyPublicKey(privateKey);
|
|
476
|
+
CFRelease(privateKey);
|
|
477
|
+
|
|
478
|
+
if (publicKey == NULL || algorithm == NULL) {
|
|
479
|
+
if (publicKey != NULL) CFRelease(publicKey);
|
|
480
|
+
[self deleteKeyMaterial:keyId];
|
|
481
|
+
onError(WKCodeStorageError,
|
|
482
|
+
algorithm == NULL
|
|
483
|
+
? @"This device's key does not support any known ECIES variant."
|
|
484
|
+
: @"Could not derive the wrapping public key.");
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Encryption uses only the public half, so it needs no authentication. Only
|
|
489
|
+
// reading the secret back prompts the user.
|
|
490
|
+
CFErrorRef encryptError = NULL;
|
|
491
|
+
NSData *ciphertext = CFBridgingRelease(SecKeyCreateEncryptedData(
|
|
492
|
+
publicKey,
|
|
493
|
+
algorithm,
|
|
494
|
+
(__bridge CFDataRef)secret,
|
|
495
|
+
&encryptError));
|
|
496
|
+
CFRelease(publicKey);
|
|
497
|
+
|
|
498
|
+
if (ciphertext == nil) {
|
|
499
|
+
NSError *error = CFBridgingRelease(encryptError);
|
|
500
|
+
[self deleteKeyMaterial:keyId];
|
|
501
|
+
onError(WKCodeFromSecError(error),
|
|
502
|
+
error.localizedDescription ?: @"Could not encrypt the secret.");
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
NSMutableDictionary *item = [WKCiphertextQuery(keyId) mutableCopy];
|
|
507
|
+
item[(__bridge id)kSecValueData] = ciphertext;
|
|
508
|
+
// The enclave key is the gate; this item needs no access control of its own,
|
|
509
|
+
// and adding one would prompt twice.
|
|
510
|
+
item[(__bridge id)kSecAttrAccessible] =
|
|
511
|
+
(__bridge id)kSecAttrAccessibleWhenUnlockedThisDeviceOnly;
|
|
512
|
+
|
|
513
|
+
SecItemDelete((__bridge CFDictionaryRef)WKCiphertextQuery(keyId));
|
|
514
|
+
OSStatus addStatus = SecItemAdd((__bridge CFDictionaryRef)item, NULL);
|
|
515
|
+
|
|
516
|
+
if (addStatus != errSecSuccess) {
|
|
517
|
+
// Leaving an enclave key behind with no ciphertext would make the id look
|
|
518
|
+
// taken forever, so roll it back.
|
|
519
|
+
[self deleteKeyMaterial:keyId];
|
|
520
|
+
onError(WKCodeFromOSStatus(addStatus),
|
|
521
|
+
[NSString stringWithFormat:@"Could not store the ciphertext (%d).",
|
|
522
|
+
(int)addStatus]);
|
|
523
|
+
return;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
onSuccess();
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
- (void)getSecret:(NSString *)keyId
|
|
530
|
+
reason:(NSString *)reason
|
|
531
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
532
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
533
|
+
{
|
|
534
|
+
[self getSecretInternal:keyId
|
|
535
|
+
reason:reason
|
|
536
|
+
onSuccess:^(NSData *plaintext) {
|
|
537
|
+
NSString *hex = WKHexFromData(plaintext);
|
|
538
|
+
resolve(hex);
|
|
539
|
+
}
|
|
540
|
+
onError:^(NSString *code, NSString *message) {
|
|
541
|
+
reject(code, message, nil);
|
|
542
|
+
}];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Hands back raw bytes rather than hex so signDigest never materializes the
|
|
547
|
+
* key as an NSString, which is immutable and cannot be wiped. The buffer is
|
|
548
|
+
* zeroed once the caller's block returns.
|
|
549
|
+
*/
|
|
550
|
+
- (void)getSecretInternal:(NSString *)keyId
|
|
551
|
+
reason:(NSString *)reason
|
|
552
|
+
onSuccess:(void (^)(NSData *plaintext))onSuccess
|
|
553
|
+
onError:(void (^)(NSString *code, NSString *message))onError
|
|
554
|
+
{
|
|
555
|
+
NSMutableDictionary *query = [WKCiphertextQuery(keyId) mutableCopy];
|
|
556
|
+
query[(__bridge id)kSecReturnData] = @YES;
|
|
557
|
+
|
|
558
|
+
CFTypeRef stored = NULL;
|
|
559
|
+
OSStatus readStatus =
|
|
560
|
+
SecItemCopyMatching((__bridge CFDictionaryRef)query, &stored);
|
|
561
|
+
|
|
562
|
+
if (readStatus != errSecSuccess) {
|
|
563
|
+
onError(WKCodeFromOSStatus(readStatus),
|
|
564
|
+
readStatus == errSecItemNotFound
|
|
565
|
+
? @"No secret is stored under this keyId."
|
|
566
|
+
: @"Could not read the stored ciphertext.");
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
NSData *ciphertext = CFBridgingRelease(stored);
|
|
571
|
+
|
|
572
|
+
LAContext *context = [LAContext new];
|
|
573
|
+
context.touchIDAuthenticationAllowableReuseDuration = 0;
|
|
574
|
+
context.localizedReason = reason;
|
|
575
|
+
|
|
576
|
+
// Decryption runs off the main thread: SecKeyCreateDecryptedData blocks
|
|
577
|
+
// until the user answers the prompt, which would deadlock the UI thread.
|
|
578
|
+
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INITIATED, 0), ^{
|
|
579
|
+
OSStatus keyStatus = errSecSuccess;
|
|
580
|
+
SecKeyRef privateKey = WKCopyPrivateKey(keyId, context, &keyStatus);
|
|
581
|
+
|
|
582
|
+
if (privateKey == NULL) {
|
|
583
|
+
// Reaching here means the ciphertext was found but its enclave key was
|
|
584
|
+
// not, so the secret is unrecoverable rather than absent. KEY_NOT_FOUND
|
|
585
|
+
// would send the user to store a new key instead of starting recovery.
|
|
586
|
+
onError(keyStatus == errSecItemNotFound ? WKCodeKeyInvalidated
|
|
587
|
+
: WKCodeFromOSStatus(keyStatus),
|
|
588
|
+
keyStatus == errSecItemNotFound
|
|
589
|
+
? @"The wrapping key no longer exists; this secret cannot be "
|
|
590
|
+
"recovered."
|
|
591
|
+
: @"Could not load the wrapping key.");
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
SecKeyAlgorithm algorithm = WKECIESAlgorithm(privateKey);
|
|
596
|
+
if (algorithm == NULL) {
|
|
597
|
+
CFRelease(privateKey);
|
|
598
|
+
onError(WKCodeStorageError,
|
|
599
|
+
@"The wrapping key does not support any known ECIES variant.");
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
CFErrorRef decryptError = NULL;
|
|
604
|
+
NSData *plaintext = CFBridgingRelease(SecKeyCreateDecryptedData(
|
|
605
|
+
privateKey,
|
|
606
|
+
algorithm,
|
|
607
|
+
(__bridge CFDataRef)ciphertext,
|
|
608
|
+
&decryptError));
|
|
609
|
+
CFRelease(privateKey);
|
|
610
|
+
|
|
611
|
+
if (plaintext == nil) {
|
|
612
|
+
NSError *error = CFBridgingRelease(decryptError);
|
|
613
|
+
onError(WKCodeFromSecError(error),
|
|
614
|
+
[NSString stringWithFormat:@"%@ (%@ %ld)",
|
|
615
|
+
error.localizedDescription
|
|
616
|
+
?: @"Could not decrypt the secret.",
|
|
617
|
+
error.domain ?: @"?", (long)error.code]);
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
onSuccess(plaintext);
|
|
622
|
+
|
|
623
|
+
// Best effort only, and only over the buffer we own. Anything the callback
|
|
624
|
+
// derived — an NSString, or the JS string it becomes — cannot be wiped.
|
|
625
|
+
WKSecureZero((void *)plaintext.bytes, plaintext.length);
|
|
626
|
+
});
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
- (void)hasSecret:(NSString *)keyId
|
|
630
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
631
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
632
|
+
{
|
|
633
|
+
// Deliberately checks only for the ciphertext item, which needs no
|
|
634
|
+
// authentication — probing the enclave key would prompt the user.
|
|
635
|
+
NSMutableDictionary *query = [WKCiphertextQuery(keyId) mutableCopy];
|
|
636
|
+
query[(__bridge id)kSecReturnData] = @NO;
|
|
637
|
+
|
|
638
|
+
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, NULL);
|
|
639
|
+
resolve(status == errSecSuccess ? @YES : @NO);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
- (void)deleteSecret:(NSString *)keyId
|
|
643
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
644
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
645
|
+
{
|
|
646
|
+
// Idempotent: errSecItemNotFound is success for a teardown path.
|
|
647
|
+
SecItemDelete((__bridge CFDictionaryRef)WKCiphertextQuery(keyId));
|
|
648
|
+
SecItemDelete((__bridge CFDictionaryRef)WKPublicKeyQuery(keyId));
|
|
649
|
+
[self deleteKeyMaterial:keyId];
|
|
650
|
+
resolve(nil);
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
- (void)deleteKeyMaterial:(NSString *)keyId
|
|
654
|
+
{
|
|
655
|
+
NSDictionary *query = @{
|
|
656
|
+
(__bridge id)kSecClass : (__bridge id)kSecClassKey,
|
|
657
|
+
(__bridge id)kSecAttrApplicationTag : WKKeyTag(keyId),
|
|
658
|
+
(__bridge id)kSecAttrKeyType : (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
|
|
659
|
+
};
|
|
660
|
+
SecItemDelete((__bridge CFDictionaryRef)query);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
#pragma mark - v0.3 secp256k1
|
|
665
|
+
|
|
666
|
+
/** Uncompressed SEC1: 0x04 || X || Y, 65 bytes. */
|
|
667
|
+
static NSData *_Nullable WKPublicKeyFromPrivate(NSData *privateKey)
|
|
668
|
+
{
|
|
669
|
+
secp256k1_context *ctx = WKSecpContext();
|
|
670
|
+
secp256k1_pubkey pubkey;
|
|
671
|
+
|
|
672
|
+
if (!secp256k1_ec_pubkey_create(ctx, &pubkey, (const unsigned char *)privateKey.bytes)) {
|
|
673
|
+
return nil;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
uint8_t serialized[65];
|
|
677
|
+
size_t length = sizeof(serialized);
|
|
678
|
+
if (!secp256k1_ec_pubkey_serialize(ctx, serialized, &length, &pubkey,
|
|
679
|
+
SECP256K1_EC_UNCOMPRESSED)) {
|
|
680
|
+
return nil;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return [NSData dataWithBytes:serialized length:length];
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
- (void)persistPublicKey:(NSData *)publicKey forKeyId:(NSString *)keyId
|
|
687
|
+
{
|
|
688
|
+
NSMutableDictionary *item = [WKPublicKeyQuery(keyId) mutableCopy];
|
|
689
|
+
item[(__bridge id)kSecValueData] = publicKey;
|
|
690
|
+
// No access control: a public key is not secret, and prompting to read your
|
|
691
|
+
// own address would be hostile. Device-only so it does not sync.
|
|
692
|
+
item[(__bridge id)kSecAttrAccessible] =
|
|
693
|
+
(__bridge id)kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly;
|
|
694
|
+
|
|
695
|
+
SecItemDelete((__bridge CFDictionaryRef)WKPublicKeyQuery(keyId));
|
|
696
|
+
SecItemAdd((__bridge CFDictionaryRef)item, NULL);
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** Shared tail of generateKey and importPrivateKey. */
|
|
700
|
+
- (void)wrapPrivateKey:(NSData *)privateKey
|
|
701
|
+
keyId:(NSString *)keyId
|
|
702
|
+
policy:(NSString *)policy
|
|
703
|
+
invalidation:(NSString *)invalidation
|
|
704
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
705
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
706
|
+
{
|
|
707
|
+
NSData *publicKey = WKPublicKeyFromPrivate(privateKey);
|
|
708
|
+
if (publicKey == nil) {
|
|
709
|
+
reject(WKCodeInvalidKey, @"Could not derive a public key from this private key.", nil);
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
NSString *secretHex = WKHexFromData(privateKey);
|
|
714
|
+
NSString *publicKeyHex = WKHexFromData(publicKey);
|
|
715
|
+
|
|
716
|
+
[self storeSecretInternal:keyId
|
|
717
|
+
secretHex:secretHex
|
|
718
|
+
policy:policy
|
|
719
|
+
invalidation:invalidation
|
|
720
|
+
onSuccess:^{
|
|
721
|
+
// Recorded only after wrapping succeeded, so a stored
|
|
722
|
+
// public key always implies a retrievable private one.
|
|
723
|
+
[self persistPublicKey:publicKey forKeyId:keyId];
|
|
724
|
+
resolve(publicKeyHex);
|
|
725
|
+
}
|
|
726
|
+
onError:^(NSString *code, NSString *message) {
|
|
727
|
+
reject(code, message, nil);
|
|
728
|
+
}];
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
- (void)generateKey:(NSString *)keyId
|
|
732
|
+
policy:(NSString *)policy
|
|
733
|
+
invalidation:(NSString *)invalidation
|
|
734
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
735
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
736
|
+
{
|
|
737
|
+
secp256k1_context *ctx = WKSecpContext();
|
|
738
|
+
uint8_t seckey[32];
|
|
739
|
+
|
|
740
|
+
// Rejection sampling against the curve order rather than reduction, which
|
|
741
|
+
// would bias the distribution toward small keys. Entropy is the platform
|
|
742
|
+
// CSPRNG — never JS, whose PRNG is not cryptographically secure.
|
|
743
|
+
BOOL valid = NO;
|
|
744
|
+
for (int attempt = 0; attempt < 256 && !valid; attempt++) {
|
|
745
|
+
if (SecRandomCopyBytes(kSecRandomDefault, sizeof(seckey), seckey) != errSecSuccess) {
|
|
746
|
+
WKSecureZero(seckey, sizeof(seckey));
|
|
747
|
+
reject(WKCodeStorageError, @"The system random number generator failed.", nil);
|
|
748
|
+
return;
|
|
749
|
+
}
|
|
750
|
+
valid = secp256k1_ec_seckey_verify(ctx, seckey) == 1;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
if (!valid) {
|
|
754
|
+
WKSecureZero(seckey, sizeof(seckey));
|
|
755
|
+
reject(WKCodeStorageError, @"Could not generate a valid private key.", nil);
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
NSData *privateKey = [NSData dataWithBytes:seckey length:sizeof(seckey)];
|
|
760
|
+
WKSecureZero(seckey, sizeof(seckey));
|
|
761
|
+
|
|
762
|
+
[self wrapPrivateKey:privateKey
|
|
763
|
+
keyId:keyId
|
|
764
|
+
policy:policy
|
|
765
|
+
invalidation:invalidation
|
|
766
|
+
resolve:resolve
|
|
767
|
+
reject:reject];
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
- (void)importPrivateKey:(NSString *)keyId
|
|
771
|
+
privateKeyHex:(NSString *)privateKeyHex
|
|
772
|
+
policy:(NSString *)policy
|
|
773
|
+
invalidation:(NSString *)invalidation
|
|
774
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
775
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
776
|
+
{
|
|
777
|
+
NSData *privateKey = WKDataFromHex(privateKeyHex);
|
|
778
|
+
if (privateKey == nil || privateKey.length != 32) {
|
|
779
|
+
reject(WKCodeInvalidKey, @"A private key must be exactly 32 bytes of hex.", nil);
|
|
780
|
+
return;
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Zero and anything at or above the curve order are not merely malformed —
|
|
784
|
+
// they yield signatures that verify against nothing. Rejected, not clamped.
|
|
785
|
+
if (secp256k1_ec_seckey_verify(WKSecpContext(), (const unsigned char *)privateKey.bytes) != 1) {
|
|
786
|
+
reject(WKCodeInvalidKey, @"The private key must be in [1, n-1].", nil);
|
|
787
|
+
return;
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
[self wrapPrivateKey:privateKey
|
|
791
|
+
keyId:keyId
|
|
792
|
+
policy:policy
|
|
793
|
+
invalidation:invalidation
|
|
794
|
+
resolve:resolve
|
|
795
|
+
reject:reject];
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
- (void)getPublicKey:(NSString *)keyId
|
|
799
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
800
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
801
|
+
{
|
|
802
|
+
NSMutableDictionary *query = [WKPublicKeyQuery(keyId) mutableCopy];
|
|
803
|
+
query[(__bridge id)kSecReturnData] = @YES;
|
|
804
|
+
|
|
805
|
+
CFTypeRef stored = NULL;
|
|
806
|
+
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &stored);
|
|
807
|
+
|
|
808
|
+
if (status != errSecSuccess) {
|
|
809
|
+
reject(WKCodeKeyNotFound, @"No key is stored under this keyId.", nil);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
resolve(WKHexFromData(CFBridgingRelease(stored)));
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
- (void)signDigest:(NSString *)keyId
|
|
817
|
+
digestHex:(NSString *)digestHex
|
|
818
|
+
reason:(NSString *)reason
|
|
819
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
820
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
821
|
+
{
|
|
822
|
+
NSData *digest = WKDataFromHex(digestHex);
|
|
823
|
+
if (digest == nil || digest.length != 32) {
|
|
824
|
+
reject(WKCodeInvalidKey, @"A digest must be exactly 32 bytes of hex.", nil);
|
|
825
|
+
return;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
[self getSecretInternal:keyId
|
|
829
|
+
reason:reason
|
|
830
|
+
onSuccess:^(NSData *privateKey) {
|
|
831
|
+
secp256k1_context *ctx = WKSecpContext();
|
|
832
|
+
secp256k1_ecdsa_recoverable_signature signature;
|
|
833
|
+
|
|
834
|
+
// The nonce is RFC 6979 deterministic by default. A repeated
|
|
835
|
+
// or predictable nonce reveals the private key algebraically,
|
|
836
|
+
// so this must never be supplied by hand.
|
|
837
|
+
if (!secp256k1_ecdsa_sign_recoverable(
|
|
838
|
+
ctx, &signature,
|
|
839
|
+
(const unsigned char *)digest.bytes,
|
|
840
|
+
(const unsigned char *)privateKey.bytes, NULL, NULL)) {
|
|
841
|
+
reject(WKCodeStorageError, @"Could not sign the digest.", nil);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
uint8_t compact[64];
|
|
846
|
+
int recid = 0;
|
|
847
|
+
secp256k1_ecdsa_recoverable_signature_serialize_compact(
|
|
848
|
+
ctx, compact, &recid, &signature);
|
|
849
|
+
|
|
850
|
+
// libsecp256k1 already emits the low-s form required by
|
|
851
|
+
// EIP-2, negating s and flipping recid when needed, so no
|
|
852
|
+
// separate normalization step is correct here.
|
|
853
|
+
uint8_t result[65];
|
|
854
|
+
memcpy(result, compact, 64);
|
|
855
|
+
result[64] = (uint8_t)(recid + 27);
|
|
856
|
+
|
|
857
|
+
NSData *serialized = [NSData dataWithBytes:result length:sizeof(result)];
|
|
858
|
+
WKSecureZero(compact, sizeof(compact));
|
|
859
|
+
|
|
860
|
+
resolve(WKHexFromData(serialized));
|
|
861
|
+
}
|
|
862
|
+
onError:^(NSString *code, NSString *message) {
|
|
863
|
+
reject(code, message, nil);
|
|
864
|
+
}];
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
- (void)exportPrivateKey:(NSString *)keyId
|
|
868
|
+
reason:(NSString *)reason
|
|
869
|
+
resolve:(RCTPromiseResolveBlock)resolve
|
|
870
|
+
reject:(RCTPromiseRejectBlock)reject
|
|
871
|
+
{
|
|
872
|
+
[self getSecretInternal:keyId
|
|
873
|
+
reason:reason
|
|
874
|
+
onSuccess:^(NSData *privateKey) {
|
|
875
|
+
resolve(WKHexFromData(privateKey));
|
|
876
|
+
}
|
|
877
|
+
onError:^(NSString *code, NSString *message) {
|
|
878
|
+
reject(code, message, nil);
|
|
879
|
+
}];
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
#pragma mark - TurboModule
|
|
883
|
+
|
|
884
|
+
- (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
|
|
885
|
+
(const facebook::react::ObjCTurboModule::InitParams &)params
|
|
886
|
+
{
|
|
887
|
+
return std::make_shared<facebook::react::NativeWalletKeystoreSpecJSI>(params);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
+ (NSString *)moduleName
|
|
891
|
+
{
|
|
892
|
+
return @"WalletKeystore";
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
@end
|