ds-01 1.0.4 → 1.0.6
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/.turbo/turbo-build.log +1 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +57 -31
- package/dist/commands/login.d.ts.map +1 -1
- package/dist/commands/login.js +109 -29
- package/dist/commands/logout.d.ts.map +1 -1
- package/dist/commands/logout.js +27 -7
- package/dist/index.js +2 -0
- package/dist/test-key.d.ts +2 -0
- package/dist/test-key.d.ts.map +1 -0
- package/dist/test-key.js +7 -0
- package/dist/utils/config.d.ts +0 -1
- package/dist/utils/config.d.ts.map +1 -1
- package/dist/utils/config.js +0 -13
- package/dist/utils/deviceKey.d.ts +3 -0
- package/dist/utils/deviceKey.d.ts.map +1 -0
- package/dist/utils/deviceKey.js +13 -0
- package/dist/utils/secureKeyStore.d.ts +10 -0
- package/dist/utils/secureKeyStore.d.ts.map +1 -0
- package/dist/utils/secureKeyStore.js +266 -0
- package/package.json +2 -1
- package/src/commands/add.ts +231 -76
- package/src/commands/login.ts +285 -52
- package/src/commands/logout.ts +39 -10
- package/src/index.ts +2 -0
- package/src/native/macos-key-helper.swift +206 -0
- package/src/test-key.ts +20 -0
- package/src/utils/config.ts +1 -12
- package/src/utils/deviceKey.ts +19 -0
- package/src/utils/secureKeyStore.ts +361 -0
package/src/commands/login.ts
CHANGED
|
@@ -1,103 +1,336 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
intro,
|
|
4
|
+
outro,
|
|
5
|
+
spinner,
|
|
6
|
+
note,
|
|
7
|
+
cancel,
|
|
8
|
+
} from "@clack/prompts";
|
|
3
9
|
import pc from "picocolors";
|
|
4
10
|
import open from "open";
|
|
5
11
|
import machineIdPkg from "node-machine-id";
|
|
12
|
+
|
|
6
13
|
import { saveToken } from "../utils/config.js";
|
|
14
|
+
import {
|
|
15
|
+
createDeviceKey,
|
|
16
|
+
getDevicePublicKey,
|
|
17
|
+
} from "../utils/secureKeyStore.js";
|
|
18
|
+
|
|
19
|
+
const API_BASE_URL =
|
|
20
|
+
process.env.DS01_API_URL ||
|
|
21
|
+
"https://ds-01.vercel.app";
|
|
7
22
|
|
|
8
|
-
const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
|
|
9
23
|
const { machineIdSync } = machineIdPkg;
|
|
10
24
|
|
|
11
25
|
export const login = new Command()
|
|
12
26
|
.name("login")
|
|
13
|
-
.description(
|
|
27
|
+
.description(
|
|
28
|
+
"Authenticate your terminal with DS01 via browser",
|
|
29
|
+
)
|
|
14
30
|
.action(async () => {
|
|
15
|
-
console.log();
|
|
31
|
+
console.log();
|
|
16
32
|
|
|
17
|
-
// 1. Header banner
|
|
18
33
|
intro(
|
|
19
|
-
`${pc.bgWhite(
|
|
34
|
+
`${pc.bgWhite(
|
|
35
|
+
pc.black(
|
|
36
|
+
pc.bold(" DS01 "),
|
|
37
|
+
),
|
|
38
|
+
)} ${pc.gray(
|
|
39
|
+
"v1.0.0 — Terminal Authentication",
|
|
40
|
+
)}`,
|
|
20
41
|
);
|
|
21
42
|
|
|
22
43
|
const s = spinner();
|
|
23
44
|
|
|
24
45
|
try {
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
46
|
+
// --------------------------------------------------
|
|
47
|
+
// 1. Create / load the device's secure key
|
|
48
|
+
// --------------------------------------------------
|
|
49
|
+
|
|
50
|
+
s.start(
|
|
51
|
+
"Preparing secure device identity...",
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
await createDeviceKey();
|
|
55
|
+
|
|
56
|
+
// Only the PUBLIC key/certificate leaves
|
|
57
|
+
// the user's machine.
|
|
58
|
+
const publicKey =
|
|
59
|
+
await getDevicePublicKey();
|
|
60
|
+
|
|
61
|
+
// Existing machine fingerprint.
|
|
62
|
+
const hardwareId =
|
|
63
|
+
machineIdSync();
|
|
64
|
+
|
|
65
|
+
// Detect the cryptographic key type.
|
|
66
|
+
const publicKeyType =
|
|
67
|
+
process.platform === "win32"
|
|
68
|
+
? "windows-rsa"
|
|
69
|
+
: "macos-ec";
|
|
70
|
+
|
|
71
|
+
// --------------------------------------------------
|
|
72
|
+
// 2. Start device authorization
|
|
73
|
+
// --------------------------------------------------
|
|
74
|
+
|
|
75
|
+
s.start(
|
|
76
|
+
"Requesting pairing code...",
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const initResponse =
|
|
80
|
+
await fetch(
|
|
81
|
+
`${API_BASE_URL}/api/auth/device/code`,
|
|
82
|
+
{
|
|
83
|
+
method: "POST",
|
|
84
|
+
|
|
85
|
+
headers: {
|
|
86
|
+
"Content-Type":
|
|
87
|
+
"application/json",
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
body: JSON.stringify({
|
|
91
|
+
machineId:
|
|
92
|
+
hardwareId,
|
|
28
93
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
94
|
+
publicKey,
|
|
95
|
+
|
|
96
|
+
publicKeyType,
|
|
97
|
+
}),
|
|
98
|
+
},
|
|
99
|
+
);
|
|
34
100
|
|
|
35
101
|
if (!initResponse.ok) {
|
|
36
|
-
s.stop(
|
|
37
|
-
|
|
102
|
+
s.stop(
|
|
103
|
+
pc.red(
|
|
104
|
+
"Failed to reach DS01 authorization server.",
|
|
105
|
+
),
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
cancel(
|
|
109
|
+
`Server responded with HTTP ${initResponse.status}`,
|
|
110
|
+
);
|
|
111
|
+
|
|
38
112
|
process.exit(1);
|
|
39
113
|
}
|
|
40
114
|
|
|
41
|
-
const {
|
|
42
|
-
|
|
115
|
+
const {
|
|
116
|
+
deviceCode,
|
|
117
|
+
userCode,
|
|
118
|
+
verificationUrl,
|
|
119
|
+
interval,
|
|
120
|
+
} = await initResponse.json();
|
|
43
121
|
|
|
44
|
-
|
|
122
|
+
if (
|
|
123
|
+
!deviceCode ||
|
|
124
|
+
!userCode ||
|
|
125
|
+
!verificationUrl
|
|
126
|
+
) {
|
|
127
|
+
throw new Error(
|
|
128
|
+
"Authorization server returned an invalid device response.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
s.stop(
|
|
133
|
+
pc.green(
|
|
134
|
+
"Secure device identity registered.",
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// --------------------------------------------------
|
|
139
|
+
// 3. Ask user to approve in browser
|
|
140
|
+
// --------------------------------------------------
|
|
45
141
|
|
|
46
|
-
// 3. Highlighted Box for User Action
|
|
47
142
|
note(
|
|
48
|
-
`${pc.bold(
|
|
49
|
-
|
|
50
|
-
|
|
143
|
+
`${pc.bold(
|
|
144
|
+
"Pairing Code:",
|
|
145
|
+
)} ${pc.cyan(
|
|
146
|
+
pc.bold(
|
|
147
|
+
` ${userCode} `,
|
|
148
|
+
),
|
|
149
|
+
)}\n` +
|
|
150
|
+
`${pc.bold(
|
|
151
|
+
"Verification URL:",
|
|
152
|
+
)} ${pc.underline(
|
|
153
|
+
verificationUrl,
|
|
154
|
+
)}`,
|
|
155
|
+
"Action Required",
|
|
51
156
|
);
|
|
52
157
|
|
|
53
|
-
// 4. Auto-launch Browser
|
|
54
158
|
try {
|
|
55
|
-
await open(
|
|
159
|
+
await open(
|
|
160
|
+
verificationUrl,
|
|
161
|
+
);
|
|
56
162
|
} catch {
|
|
57
|
-
//
|
|
163
|
+
// Browser launch failure
|
|
164
|
+
// is non-fatal.
|
|
58
165
|
}
|
|
59
166
|
|
|
60
|
-
//
|
|
61
|
-
|
|
167
|
+
// --------------------------------------------------
|
|
168
|
+
// 4. Poll for authorization
|
|
169
|
+
// --------------------------------------------------
|
|
62
170
|
|
|
63
|
-
|
|
64
|
-
|
|
171
|
+
s.start(
|
|
172
|
+
"Waiting for web authorization...",
|
|
173
|
+
);
|
|
65
174
|
|
|
66
|
-
|
|
67
|
-
|
|
175
|
+
const pollInterval =
|
|
176
|
+
(interval || 5) * 1000;
|
|
68
177
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
178
|
+
let token: string | null =
|
|
179
|
+
null;
|
|
180
|
+
|
|
181
|
+
while (true) {
|
|
182
|
+
await new Promise(
|
|
183
|
+
(resolve) =>
|
|
184
|
+
setTimeout(
|
|
185
|
+
resolve,
|
|
186
|
+
pollInterval,
|
|
187
|
+
),
|
|
76
188
|
);
|
|
77
189
|
|
|
78
|
-
const
|
|
190
|
+
const tokenResponse =
|
|
191
|
+
await fetch(
|
|
192
|
+
`${API_BASE_URL}/api/auth/device/token`,
|
|
193
|
+
{
|
|
194
|
+
method: "POST",
|
|
195
|
+
|
|
196
|
+
headers: {
|
|
197
|
+
"Content-Type":
|
|
198
|
+
"application/json",
|
|
199
|
+
},
|
|
200
|
+
|
|
201
|
+
body: JSON.stringify({
|
|
202
|
+
deviceCode,
|
|
203
|
+
machineId:
|
|
204
|
+
hardwareId,
|
|
205
|
+
}),
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
209
|
+
let tokenData: any;
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
tokenData =
|
|
213
|
+
await tokenResponse.json();
|
|
214
|
+
} catch {
|
|
215
|
+
s.stop(
|
|
216
|
+
pc.red(
|
|
217
|
+
"Authorization server returned invalid JSON.",
|
|
218
|
+
),
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
cancel(
|
|
222
|
+
"Unable to read authorization response.",
|
|
223
|
+
);
|
|
79
224
|
|
|
80
|
-
if (tokenResponse.ok && tokenData.accessToken) {
|
|
81
|
-
token = tokenData.accessToken;
|
|
82
|
-
} else if (tokenData.error !== "authorization_pending") {
|
|
83
|
-
s.stop(pc.red("Authorization failed or expired."));
|
|
84
|
-
cancel(`Reason: ${tokenData.error || "Unknown authorization error"}`);
|
|
85
225
|
process.exit(1);
|
|
86
226
|
}
|
|
227
|
+
|
|
228
|
+
// --------------------------------------------------
|
|
229
|
+
// SUCCESS
|
|
230
|
+
// --------------------------------------------------
|
|
231
|
+
|
|
232
|
+
if (
|
|
233
|
+
tokenResponse.ok &&
|
|
234
|
+
typeof tokenData.accessToken ===
|
|
235
|
+
"string" &&
|
|
236
|
+
tokenData.accessToken.length > 0
|
|
237
|
+
) {
|
|
238
|
+
token =
|
|
239
|
+
tokenData.accessToken;
|
|
240
|
+
|
|
241
|
+
// IMPORTANT:
|
|
242
|
+
// Stop the spinner immediately.
|
|
243
|
+
s.stop(
|
|
244
|
+
pc.green(
|
|
245
|
+
"Hardware signature linked & token verified!",
|
|
246
|
+
),
|
|
247
|
+
);
|
|
248
|
+
|
|
249
|
+
// IMPORTANT:
|
|
250
|
+
// Leave the polling loop immediately.
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// --------------------------------------------------
|
|
255
|
+
// STILL WAITING
|
|
256
|
+
// --------------------------------------------------
|
|
257
|
+
|
|
258
|
+
if (
|
|
259
|
+
tokenData.error ===
|
|
260
|
+
"authorization_pending"
|
|
261
|
+
) {
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// --------------------------------------------------
|
|
266
|
+
// FAILED / EXPIRED
|
|
267
|
+
// --------------------------------------------------
|
|
268
|
+
|
|
269
|
+
s.stop(
|
|
270
|
+
pc.red(
|
|
271
|
+
"Authorization failed or expired.",
|
|
272
|
+
),
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
cancel(
|
|
276
|
+
`Reason: ${
|
|
277
|
+
tokenData.error ||
|
|
278
|
+
"Unknown authorization error"
|
|
279
|
+
}`,
|
|
280
|
+
);
|
|
281
|
+
|
|
282
|
+
process.exit(1);
|
|
87
283
|
}
|
|
88
284
|
|
|
89
|
-
//
|
|
90
|
-
|
|
285
|
+
// --------------------------------------------------
|
|
286
|
+
// 5. Make absolutely sure token exists
|
|
287
|
+
// --------------------------------------------------
|
|
288
|
+
|
|
289
|
+
if (!token) {
|
|
290
|
+
throw new Error(
|
|
291
|
+
"Authorization completed without receiving an access token.",
|
|
292
|
+
);
|
|
293
|
+
}
|
|
91
294
|
|
|
92
|
-
|
|
295
|
+
// --------------------------------------------------
|
|
296
|
+
// 6. Save CLI token + machine ID
|
|
297
|
+
// --------------------------------------------------
|
|
298
|
+
|
|
299
|
+
saveToken(
|
|
300
|
+
token,
|
|
301
|
+
hardwareId,
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// --------------------------------------------------
|
|
305
|
+
// 7. Success
|
|
306
|
+
// --------------------------------------------------
|
|
93
307
|
|
|
94
|
-
// 7. Clean Success Outro
|
|
95
308
|
outro(
|
|
96
|
-
`${pc.white(
|
|
309
|
+
`${pc.white(
|
|
310
|
+
"✔",
|
|
311
|
+
)} ${pc.bold(
|
|
312
|
+
"Terminal synced successfully",
|
|
313
|
+
)}\n` +
|
|
314
|
+
pc.gray(
|
|
315
|
+
"Secure device key and session token are ready.",
|
|
316
|
+
),
|
|
97
317
|
);
|
|
98
318
|
} catch (error: any) {
|
|
99
|
-
|
|
100
|
-
|
|
319
|
+
// Only stop the spinner if it is
|
|
320
|
+
// still running.
|
|
321
|
+
s.stop(
|
|
322
|
+
pc.red(
|
|
323
|
+
"An error occurred during login.",
|
|
324
|
+
),
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
cancel(
|
|
328
|
+
pc.gray(
|
|
329
|
+
error?.message ||
|
|
330
|
+
"Unknown CLI Error",
|
|
331
|
+
),
|
|
332
|
+
);
|
|
333
|
+
|
|
101
334
|
process.exit(1);
|
|
102
335
|
}
|
|
103
336
|
});
|
package/src/commands/logout.ts
CHANGED
|
@@ -2,28 +2,57 @@ import { Command } from "commander";
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import ora from "ora";
|
|
4
4
|
import { deleteToken, getToken } from "../utils/config.js";
|
|
5
|
+
import machineIdPkg from "node-machine-id";
|
|
6
|
+
const { machineIdSync } = machineIdPkg;
|
|
7
|
+
const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
|
|
5
8
|
|
|
6
9
|
export const logout = new Command()
|
|
7
10
|
.name("logout")
|
|
8
|
-
.description("Log out of your DS01 account and
|
|
11
|
+
.description("Log out of your DS01 account and revoke the CLI session")
|
|
9
12
|
.action(async () => {
|
|
10
13
|
const token = getToken();
|
|
14
|
+
const machineId = machineIdSync();
|
|
11
15
|
|
|
12
|
-
if (!token) {
|
|
16
|
+
if (!token || !machineId) {
|
|
13
17
|
console.log(chalk.yellow("You are already logged out."));
|
|
14
18
|
return;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
const spinner = ora("
|
|
21
|
+
const spinner = ora("Revoking CLI session...").start();
|
|
18
22
|
|
|
19
23
|
try {
|
|
20
|
-
|
|
24
|
+
const response = await fetch(`${API_BASE_URL}/api/auth/cli/logout`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: {
|
|
27
|
+
Authorization: `Bearer ${token}`,
|
|
28
|
+
"X-Machine-ID": machineId,
|
|
29
|
+
},
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// NEW: Do not silently delete local credentials if the server
|
|
33
|
+
// failed to revoke the session.
|
|
34
|
+
if (!response.ok) {
|
|
35
|
+
const data = await response.json().catch(() => null);
|
|
36
|
+
|
|
37
|
+
throw new Error(
|
|
38
|
+
data?.error || `Logout failed (HTTP ${response.status})`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// NEW: Delete local credentials only after the server confirms
|
|
43
|
+
// that the CLI session has been revoked.
|
|
44
|
+
console.log("✅ Server confirmed CLI session was revoked.");
|
|
21
45
|
deleteToken();
|
|
22
|
-
|
|
46
|
+
console.log("✅ Local credentials deleted.");
|
|
23
47
|
spinner.succeed(chalk.green("Successfully logged out."));
|
|
24
|
-
console.log(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
48
|
+
console.log(
|
|
49
|
+
chalk.gray(
|
|
50
|
+
"Your CLI session has been revoked and local credentials have been cleared.",
|
|
51
|
+
),
|
|
52
|
+
);
|
|
53
|
+
} catch (error: any) {
|
|
54
|
+
spinner.fail(chalk.red("Failed to log out."));
|
|
55
|
+
|
|
56
|
+
console.error(chalk.red(error.message || "Unknown logout error."));
|
|
28
57
|
}
|
|
29
|
-
});
|
|
58
|
+
});
|
package/src/index.ts
CHANGED
|
@@ -22,4 +22,6 @@ program.addCommand(init);
|
|
|
22
22
|
program.addCommand(add); // <-- 2. Register the add command!
|
|
23
23
|
|
|
24
24
|
// Parse the arguments from the terminal
|
|
25
|
+
// It is the point where Commander reads the user's terminal input and decides
|
|
26
|
+
// which registered command's .action() function should execute.
|
|
25
27
|
program.parse(process.argv);
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Security
|
|
3
|
+
|
|
4
|
+
let keyTag = "com.ds01.cli.device-key"
|
|
5
|
+
|
|
6
|
+
func fail(_ message: String) -> Never {
|
|
7
|
+
fputs(message + "\n", stderr)
|
|
8
|
+
exit(1)
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
func getAccessControl() -> SecAccessControl {
|
|
12
|
+
var error: Unmanaged<CFError>?
|
|
13
|
+
|
|
14
|
+
guard let access = SecAccessControlCreateWithFlags(
|
|
15
|
+
nil,
|
|
16
|
+
kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
|
|
17
|
+
.privateKeyUsage,
|
|
18
|
+
&error
|
|
19
|
+
) else {
|
|
20
|
+
if let error {
|
|
21
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
fail("Unable to create Secure Enclave access control.")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return access
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
func findPrivateKey() -> SecKey? {
|
|
31
|
+
let query: [String: Any] = [
|
|
32
|
+
kSecClass as String: kSecClassKey,
|
|
33
|
+
kSecAttrApplicationTag as String: keyTag.data(using: .utf8)!,
|
|
34
|
+
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
|
|
35
|
+
kSecReturnRef as String: true
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
var result: CFTypeRef?
|
|
39
|
+
|
|
40
|
+
let status = SecItemCopyMatching(
|
|
41
|
+
query as CFDictionary,
|
|
42
|
+
&result
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
guard status == errSecSuccess else {
|
|
46
|
+
return nil
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (result as! SecKey)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
func createKey() {
|
|
53
|
+
if findPrivateKey() != nil {
|
|
54
|
+
print("EXISTS")
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let access = getAccessControl()
|
|
59
|
+
|
|
60
|
+
let attributes: [String: Any] = [
|
|
61
|
+
kSecAttrKeyType as String:
|
|
62
|
+
kSecAttrKeyTypeECSECPrimeRandom,
|
|
63
|
+
|
|
64
|
+
kSecAttrKeySizeInBits as String:
|
|
65
|
+
256,
|
|
66
|
+
|
|
67
|
+
kSecAttrTokenID as String:
|
|
68
|
+
kSecAttrTokenIDSecureEnclave,
|
|
69
|
+
|
|
70
|
+
kSecPrivateKeyAttrs as String: [
|
|
71
|
+
kSecAttrIsPermanent as String:
|
|
72
|
+
true,
|
|
73
|
+
|
|
74
|
+
kSecAttrApplicationTag as String:
|
|
75
|
+
keyTag.data(using: .utf8)!,
|
|
76
|
+
|
|
77
|
+
kSecAttrAccessControl as String:
|
|
78
|
+
access
|
|
79
|
+
]
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
var error: Unmanaged<CFError>?
|
|
83
|
+
|
|
84
|
+
guard SecKeyCreateRandomKey(
|
|
85
|
+
attributes as CFDictionary,
|
|
86
|
+
&error
|
|
87
|
+
) != nil else {
|
|
88
|
+
if let error {
|
|
89
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
fail("Unable to create Secure Enclave key.")
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
print("CREATED")
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
func publicKey() {
|
|
99
|
+
guard let privateKey = findPrivateKey() else {
|
|
100
|
+
fail("DS01 Secure Enclave key not found.")
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
|
|
104
|
+
fail("Unable to obtain public key.")
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
var error: Unmanaged<CFError>?
|
|
108
|
+
|
|
109
|
+
guard let data = SecKeyCopyExternalRepresentation(
|
|
110
|
+
publicKey,
|
|
111
|
+
&error
|
|
112
|
+
) else {
|
|
113
|
+
if let error {
|
|
114
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
fail("Unable to export public key.")
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
let base64 = (data as Data).base64EncodedString()
|
|
121
|
+
|
|
122
|
+
print(base64)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
func sign(_ base64Data: String) {
|
|
126
|
+
guard let privateKey = findPrivateKey() else {
|
|
127
|
+
fail("DS01 Secure Enclave key not found.")
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
guard let data = Data(base64Encoded: base64Data) else {
|
|
131
|
+
fail("Invalid input data.")
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
let algorithm = SecKeyAlgorithm.ecdsaSignatureMessageX962SHA256
|
|
135
|
+
|
|
136
|
+
guard SecKeyIsAlgorithmSupported(
|
|
137
|
+
privateKey,
|
|
138
|
+
.sign,
|
|
139
|
+
algorithm
|
|
140
|
+
) else {
|
|
141
|
+
fail("Secure Enclave key does not support signing.")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
var error: Unmanaged<CFError>?
|
|
145
|
+
|
|
146
|
+
guard let signature = SecKeyCreateSignature(
|
|
147
|
+
privateKey,
|
|
148
|
+
algorithm,
|
|
149
|
+
data as CFData,
|
|
150
|
+
&error
|
|
151
|
+
) else {
|
|
152
|
+
if let error {
|
|
153
|
+
fail(error.takeRetainedValue().localizedDescription)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
fail("Secure Enclave signing failed.")
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
print((signature as Data).base64EncodedString())
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
func deleteKey() {
|
|
163
|
+
let query: [String: Any] = [
|
|
164
|
+
kSecClass as String: kSecClassKey,
|
|
165
|
+
kSecAttrApplicationTag as String:
|
|
166
|
+
keyTag.data(using: .utf8)!,
|
|
167
|
+
kSecAttrKeyType as String:
|
|
168
|
+
kSecAttrKeyTypeECSECPrimeRandom
|
|
169
|
+
]
|
|
170
|
+
|
|
171
|
+
let status = SecItemDelete(
|
|
172
|
+
query as CFDictionary
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if status != errSecSuccess &&
|
|
176
|
+
status != errSecItemNotFound {
|
|
177
|
+
fail("Unable to delete DS01 Secure Enclave key.")
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
print("DELETED")
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
guard CommandLine.arguments.count >= 2 else {
|
|
184
|
+
fail("Missing operation.")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
switch CommandLine.arguments[1] {
|
|
188
|
+
case "create":
|
|
189
|
+
createKey()
|
|
190
|
+
|
|
191
|
+
case "public":
|
|
192
|
+
publicKey()
|
|
193
|
+
|
|
194
|
+
case "sign":
|
|
195
|
+
guard CommandLine.arguments.count >= 3 else {
|
|
196
|
+
fail("Missing data to sign.")
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
sign(CommandLine.arguments[2])
|
|
200
|
+
|
|
201
|
+
case "delete":
|
|
202
|
+
deleteKey()
|
|
203
|
+
|
|
204
|
+
default:
|
|
205
|
+
fail("Unknown operation.")
|
|
206
|
+
}
|
package/src/test-key.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createDeviceKey,
|
|
3
|
+
getDevicePublicKey,
|
|
4
|
+
signWithDeviceKey,
|
|
5
|
+
} from "./utils/secureKeyStore.js";
|
|
6
|
+
|
|
7
|
+
await createDeviceKey();
|
|
8
|
+
|
|
9
|
+
const publicKey = await getDevicePublicKey();
|
|
10
|
+
|
|
11
|
+
console.log("Public key:", publicKey);
|
|
12
|
+
|
|
13
|
+
const message = new TextEncoder().encode("hello DS01");
|
|
14
|
+
|
|
15
|
+
const signature = await signWithDeviceKey(message);
|
|
16
|
+
|
|
17
|
+
console.log(
|
|
18
|
+
"Signature:",
|
|
19
|
+
Buffer.from(signature).toString("base64"),
|
|
20
|
+
);
|