@visa/cli 5.0.0-rc.364 → 5.0.0-rc.365

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.
@@ -1,367 +0,0 @@
1
- #import <Foundation/Foundation.h>
2
- #import <Security/Security.h>
3
- #import <LocalAuthentication/LocalAuthentication.h>
4
- #import <AppKit/AppKit.h>
5
-
6
- // SPKI DER header for P-256 (prime256v1) uncompressed public key
7
- static const unsigned char SPKI_HEADER[] = {
8
- 0x30, 0x59, 0x30, 0x13, 0x06, 0x07, 0x2A, 0x86,
9
- 0x48, 0xCE, 0x3D, 0x02, 0x01, 0x06, 0x08, 0x2A,
10
- 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07, 0x03,
11
- 0x42, 0x00
12
- };
13
-
14
- static NSString *kService = @"visa-cli";
15
- static NSString *kKeyAcct = @"attestation-key";
16
- static NSString *kTokenAcct = @"session-token";
17
-
18
- static NSString *spkiPublicKeyB64(SecKeyRef key) {
19
- SecKeyRef pubKey = SecKeyCopyPublicKey(key);
20
- if (!pubKey) return nil;
21
-
22
- CFErrorRef pubErr = NULL;
23
- CFDataRef rawPub = SecKeyCopyExternalRepresentation(pubKey, &pubErr);
24
- CFRelease(pubKey);
25
- if (!rawPub) {
26
- if (pubErr) CFRelease(pubErr);
27
- return nil;
28
- }
29
-
30
- NSMutableData *spki = [NSMutableData dataWithBytes:SPKI_HEADER length:sizeof(SPKI_HEADER)];
31
- [spki appendData:(__bridge NSData *)rawPub];
32
- CFRelease(rawPub);
33
- return [spki base64EncodedStringWithOptions:0];
34
- }
35
-
36
- // ---------------------------------------------------------------------------
37
- // Keychain helpers for generic password items
38
- // ---------------------------------------------------------------------------
39
-
40
- static OSStatus storeItem(NSString *account, NSData *data) {
41
- NSDictionary *delQ = @{
42
- (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
43
- (__bridge id)kSecAttrService: kService,
44
- (__bridge id)kSecAttrAccount: account,
45
- };
46
- SecItemDelete((__bridge CFDictionaryRef)delQ);
47
-
48
- NSDictionary *addQ = @{
49
- (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
50
- (__bridge id)kSecAttrService: kService,
51
- (__bridge id)kSecAttrAccount: account,
52
- (__bridge id)kSecValueData: data,
53
- };
54
- return SecItemAdd((__bridge CFDictionaryRef)addQ, NULL);
55
- }
56
-
57
- static NSData *retrieveItem(NSString *account) {
58
- NSDictionary *q = @{
59
- (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
60
- (__bridge id)kSecAttrService: kService,
61
- (__bridge id)kSecAttrAccount: account,
62
- (__bridge id)kSecReturnData: @YES,
63
- };
64
- CFTypeRef result = NULL;
65
- OSStatus st = SecItemCopyMatching((__bridge CFDictionaryRef)q, &result);
66
- if (st != errSecSuccess) return nil;
67
- return (__bridge NSData *)result;
68
- }
69
-
70
- static void deleteItem(NSString *account) {
71
- NSDictionary *q = @{
72
- (__bridge id)kSecClass: (__bridge id)kSecClassGenericPassword,
73
- (__bridge id)kSecAttrService: kService,
74
- (__bridge id)kSecAttrAccount: account,
75
- };
76
- SecItemDelete((__bridge CFDictionaryRef)q);
77
- }
78
-
79
- // ---------------------------------------------------------------------------
80
- // Key operations — raw EC key bytes stored as Keychain generic password,
81
- // imported as in-memory SecKeyRef for signing (avoids CDSA limitations)
82
- // ---------------------------------------------------------------------------
83
-
84
- int cmd_generate_key(void) {
85
- // Generate P-256 key pair in memory
86
- NSDictionary *attrs = @{
87
- (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
88
- (__bridge id)kSecAttrKeySizeInBits: @256,
89
- (__bridge id)kSecAttrIsPermanent: @NO,
90
- };
91
- CFErrorRef keyErr = NULL;
92
- SecKeyRef privKey = SecKeyCreateRandomKey((__bridge CFDictionaryRef)attrs, &keyErr);
93
- if (!privKey) {
94
- if (keyErr) CFRelease(keyErr);
95
- printf("ERROR:Key generation failed\n");
96
- return 1;
97
- }
98
-
99
- // Export raw private key bytes (caller stores via `security` CLI)
100
- CFErrorRef expErr = NULL;
101
- CFDataRef rawPriv = SecKeyCopyExternalRepresentation(privKey, &expErr);
102
- if (!rawPriv) {
103
- CFRelease(privKey);
104
- if (expErr) CFRelease(expErr);
105
- printf("ERROR:Failed to export private key\n");
106
- return 1;
107
- }
108
- NSString *privB64 = [(__bridge NSData *)rawPriv base64EncodedStringWithOptions:0];
109
- CFRelease(rawPriv);
110
-
111
- NSString *pubB64 = spkiPublicKeyB64(privKey);
112
- CFRelease(privKey);
113
- if (!pubB64) {
114
- printf("ERROR:Failed to export public key\n");
115
- return 1;
116
- }
117
-
118
- // Output both keys: OK:<private-b64>:<public-spki-b64>
119
- printf("OK:%s:%s\n", privB64.UTF8String, pubB64.UTF8String);
120
- return 0;
121
- }
122
-
123
- int cmd_public_key(void) {
124
- NSFileHandle *input = [NSFileHandle fileHandleWithStandardInput];
125
- NSData *stdinData = [input readDataToEndOfFile];
126
- NSString *keyB64 = [[NSString alloc] initWithData:stdinData encoding:NSUTF8StringEncoding];
127
- keyB64 = [keyB64 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
128
-
129
- if (!keyB64 || keyB64.length == 0) {
130
- printf("ERROR:No key provided on stdin\n");
131
- return 1;
132
- }
133
-
134
- NSData *rawKey = [[NSData alloc] initWithBase64EncodedString:keyB64 options:0];
135
- if (!rawKey) {
136
- printf("ERROR:Invalid base64 key data\n");
137
- return 1;
138
- }
139
-
140
- NSDictionary *keyAttrs = @{
141
- (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
142
- (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate,
143
- (__bridge id)kSecAttrKeySizeInBits: @256,
144
- };
145
- CFErrorRef importErr = NULL;
146
- SecKeyRef privKey = SecKeyCreateWithData(
147
- (__bridge CFDataRef)rawKey,
148
- (__bridge CFDictionaryRef)keyAttrs,
149
- &importErr);
150
- if (!privKey) {
151
- if (importErr) CFRelease(importErr);
152
- printf("ERROR:Failed to import key for public export\n");
153
- return 1;
154
- }
155
-
156
- NSString *pubB64 = spkiPublicKeyB64(privKey);
157
- CFRelease(privKey);
158
- if (!pubB64) {
159
- printf("ERROR:Failed to export public key\n");
160
- return 1;
161
- }
162
-
163
- printf("OK:%s\n", pubB64.UTF8String);
164
- return 0;
165
- }
166
-
167
- int cmd_sign(const char *challenge, const char *reason) {
168
- // Read base64-encoded private key from stdin (avoids Keychain ACL issues
169
- // and keeps the key out of the process argument list visible in `ps`)
170
- NSFileHandle *input = [NSFileHandle fileHandleWithStandardInput];
171
- NSData *stdinData = [input readDataToEndOfFile];
172
- NSString *keyB64 = [[NSString alloc] initWithData:stdinData encoding:NSUTF8StringEncoding];
173
- keyB64 = [keyB64 stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
174
-
175
- if (!keyB64 || keyB64.length == 0) {
176
- printf("ERROR:No key provided on stdin\n");
177
- return 1;
178
- }
179
-
180
- NSData *rawKey = [[NSData alloc] initWithBase64EncodedString:keyB64 options:0];
181
- if (!rawKey) {
182
- printf("ERROR:Invalid base64 key data\n");
183
- return 1;
184
- }
185
-
186
- // Authentication gate (Touch ID OR password — whichever the user has).
187
- //
188
- // We use LAPolicyDeviceOwnerAuthentication, which presents a single
189
- // system dialog: Touch ID on enrolled hardware, password fallback
190
- // everywhere else (including users who skipped biometric setup and
191
- // Macs without a Touch ID sensor). This front-focus fix applies to
192
- // BOTH dialogs — they are the same system-level window, just with
193
- // different input modes. If we only fixed the Touch ID path we would
194
- // still be burying the password prompt behind the terminal for every
195
- // non-biometric user.
196
- //
197
- // Front-focus fix: bare CLI binaries have no NSApplication, so the
198
- // system LAContext dialog gets parented to whichever process happens to
199
- // be frontmost (usually the terminal or Claude Code), which puts the
200
- // prompt *behind* the user's current window — users never see it, time
201
- // out, and their spend gets cancelled.
202
- //
203
- // Making the binary an Accessory-policy NSApplication and explicitly
204
- // calling activateIgnoringOtherApps: before evaluatePolicy tells the
205
- // window server this process is the new frontmost app, so the auth
206
- // prompt (biometric or password) is raised on top of all other
207
- // windows. Accessory policy means no Dock icon, no menu bar — the
208
- // binary stays invisible except for the dialog itself.
209
- [NSApplication sharedApplication];
210
- [NSApp setActivationPolicy:NSApplicationActivationPolicyAccessory];
211
- [NSApp activateIgnoringOtherApps:YES];
212
-
213
- LAContext *ctx = [[LAContext alloc] init];
214
- NSString *reasonStr = reason
215
- ? [NSString stringWithUTF8String:reason]
216
- : @"approve payment";
217
-
218
- __block BOOL done = NO;
219
- __block BOOL authOk = NO;
220
- [ctx evaluatePolicy:LAPolicyDeviceOwnerAuthentication
221
- localizedReason:reasonStr
222
- reply:^(BOOL success, NSError *err) {
223
- authOk = success;
224
- done = YES;
225
- }];
226
-
227
- // Pump the main run loop while waiting for the reply block. Using
228
- // dispatch_semaphore_wait here would freeze the main thread and
229
- // prevent the WindowServer from delivering the activation we just
230
- // requested — the dialog would come up, but not on top.
231
- NSDate *timeout = [NSDate dateWithTimeIntervalSinceNow:55.0];
232
- while (!done) {
233
- @autoreleasepool {
234
- [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
235
- beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
236
- if ([[NSDate date] compare:timeout] == NSOrderedDescending) {
237
- printf("ERROR:Authentication timed out\n");
238
- return 1;
239
- }
240
- }
241
- }
242
-
243
- if (!authOk) {
244
- printf("ERROR:Authentication cancelled by user\n");
245
- return 1;
246
- }
247
-
248
- // Import as in-memory SecKeyRef (modern API, not CDSA)
249
- NSDictionary *keyAttrs = @{
250
- (__bridge id)kSecAttrKeyType: (__bridge id)kSecAttrKeyTypeECSECPrimeRandom,
251
- (__bridge id)kSecAttrKeyClass: (__bridge id)kSecAttrKeyClassPrivate,
252
- (__bridge id)kSecAttrKeySizeInBits: @256,
253
- };
254
- CFErrorRef importErr = NULL;
255
- SecKeyRef privKey = SecKeyCreateWithData(
256
- (__bridge CFDataRef)rawKey,
257
- (__bridge CFDictionaryRef)keyAttrs,
258
- &importErr);
259
- if (!privKey) {
260
- if (importErr) CFRelease(importErr);
261
- printf("ERROR:Failed to import key for signing\n");
262
- return 1;
263
- }
264
-
265
- // Sign (ECDSA-SHA256, same format as Node.js crypto.createSign('SHA256'))
266
- NSData *data = [[NSString stringWithUTF8String:challenge] dataUsingEncoding:NSUTF8StringEncoding];
267
- CFErrorRef sigErr = NULL;
268
- CFDataRef sig = SecKeyCreateSignature(
269
- privKey,
270
- kSecKeyAlgorithmECDSASignatureMessageX962SHA256,
271
- (__bridge CFDataRef)data,
272
- &sigErr);
273
- CFRelease(privKey);
274
-
275
- if (!sig) {
276
- if (sigErr) CFRelease(sigErr);
277
- printf("ERROR:Signing failed\n");
278
- return 1;
279
- }
280
-
281
- printf("OK:%s\n", [(__bridge NSData *)sig base64EncodedStringWithOptions:0].UTF8String);
282
- CFRelease(sig);
283
- return 0;
284
- }
285
-
286
- int cmd_delete_key(void) {
287
- // Legacy: also clean up any old keychain item from previous versions
288
- deleteItem(kKeyAcct);
289
- printf("OK\n");
290
- return 0;
291
- }
292
-
293
- // ---------------------------------------------------------------------------
294
- // Session token operations
295
- // ---------------------------------------------------------------------------
296
-
297
- int cmd_store_token(const char *token) {
298
- NSData *val = [[NSString stringWithUTF8String:token] dataUsingEncoding:NSUTF8StringEncoding];
299
- OSStatus st = storeItem(kTokenAcct, val);
300
- if (st != errSecSuccess) {
301
- printf("ERROR:Failed to store token (status %d)\n", (int)st);
302
- return 1;
303
- }
304
- printf("OK\n");
305
- return 0;
306
- }
307
-
308
- int cmd_get_token(void) {
309
- NSData *d = retrieveItem(kTokenAcct);
310
- if (!d) {
311
- printf("ERROR:not_found\n");
312
- return 1;
313
- }
314
- NSString *tok = [[NSString alloc] initWithData:d encoding:NSUTF8StringEncoding];
315
- printf("OK:%s\n", tok.UTF8String);
316
- return 0;
317
- }
318
-
319
- int cmd_delete_token(void) {
320
- deleteItem(kTokenAcct);
321
- printf("OK\n");
322
- return 0;
323
- }
324
-
325
- // ---------------------------------------------------------------------------
326
- // Check
327
- // ---------------------------------------------------------------------------
328
-
329
- int cmd_check(void) {
330
- LAContext *ctx = [[LAContext alloc] init];
331
- NSError *err = nil;
332
- if (![ctx canEvaluatePolicy:LAPolicyDeviceOwnerAuthentication error:&err]) {
333
- printf("ERROR:Touch ID unavailable\n");
334
- return 1;
335
- }
336
- printf("OK\n");
337
- return 0;
338
- }
339
-
340
- // ---------------------------------------------------------------------------
341
- // Main
342
- // ---------------------------------------------------------------------------
343
-
344
- int main(int argc, const char *argv[]) {
345
- @autoreleasepool {
346
- if (argc < 2) {
347
- fprintf(stderr, "Usage: visa-keychain <command> [args...]\n");
348
- return 1;
349
- }
350
- const char *cmd = argv[1];
351
- if (strcmp(cmd, "generate-key") == 0) return cmd_generate_key();
352
- else if (strcmp(cmd, "public-key") == 0) return cmd_public_key();
353
- else if (strcmp(cmd, "sign") == 0) {
354
- if (argc < 3) { fprintf(stderr, "Usage: visa-keychain sign <challenge> [reason]\n"); return 1; }
355
- return cmd_sign(argv[2], argc >= 4 ? argv[3] : NULL);
356
- }
357
- else if (strcmp(cmd, "delete-key") == 0) return cmd_delete_key();
358
- else if (strcmp(cmd, "store-token") == 0) {
359
- if (argc < 3) { fprintf(stderr, "Usage: visa-keychain store-token <token>\n"); return 1; }
360
- return cmd_store_token(argv[2]);
361
- }
362
- else if (strcmp(cmd, "get-token") == 0) return cmd_get_token();
363
- else if (strcmp(cmd, "delete-token") == 0) return cmd_delete_token();
364
- else if (strcmp(cmd, "check") == 0) return cmd_check();
365
- else { fprintf(stderr, "Unknown command: %s\n", cmd); return 1; }
366
- }
367
- }