ds-01 1.0.7 → 1.0.9

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 +1 @@
1
- {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAgEpC,eAAO,MAAM,GAAG,SAkRZ,CAAC"}
1
+ {"version":3,"file":"add.d.ts","sourceRoot":"","sources":["../../src/commands/add.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAuDpC,eAAO,MAAM,GAAG,SA0MZ,CAAC"}
@@ -84,8 +84,9 @@ export const add = new Command()
84
84
  },
85
85
  });
86
86
  if (!response.ok) {
87
+ const errorData = await response.json().catch(() => null);
87
88
  s.stop(pc.red("Component fetch failed."));
88
- cancel(`Component not found or server error (HTTP ${response.status})`);
89
+ cancel(errorData?.error || `Server returned HTTP ${response.status}`);
89
90
  process.exit(1);
90
91
  }
91
92
  const componentData = await response.json();
@@ -118,8 +119,7 @@ export const add = new Command()
118
119
  // --------------------------------------------------
119
120
  // 7. Install dependencies
120
121
  // --------------------------------------------------
121
- if (componentData.dependencies &&
122
- componentData.dependencies.length > 0) {
122
+ if (componentData.dependencies && componentData.dependencies.length > 0) {
123
123
  const depsToInstall = componentData.dependencies.join(" ");
124
124
  s.start(`Installing missing dependencies: ${pc.cyan(depsToInstall)}...`);
125
125
  try {
@@ -136,16 +136,14 @@ export const add = new Command()
136
136
  // --------------------------------------------------
137
137
  // 8. Success
138
138
  // --------------------------------------------------
139
- const mainFile = bundledFileNames.find((name) => name.toLowerCase() ===
140
- componentName.toLowerCase()) || bundledFileNames[0];
139
+ const mainFile = bundledFileNames.find((name) => name.toLowerCase() === componentName.toLowerCase()) || bundledFileNames[0];
141
140
  outro(`${pc.white("✔")} ${pc.bold(`Component <${componentName} /> is ready!`)}\n` +
142
141
  pc.gray("Import it: ") +
143
142
  pc.cyan(`import { ${mainFile} } from "@/${config.componentsPath}/${componentName}/${mainFile}"`));
144
143
  }
145
144
  catch (error) {
146
145
  s.stop(pc.red("An error occurred during injection."));
147
- cancel(pc.gray(error?.message ||
148
- "Unknown CLI Error"));
146
+ cancel(pc.gray(error?.message || "Unknown CLI Error"));
149
147
  process.exit(1);
150
148
  }
151
149
  });
@@ -1 +1 @@
1
- {"version":3,"file":"secureKeyStore.d.ts","sourceRoot":"","sources":["../../src/utils/secureKeyStore.ts"],"names":[],"mappings":"AAwTA;;;;GAIG;AAEH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAQ1D;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,UAAU,CAAC,CAQrB;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD"}
1
+ {"version":3,"file":"secureKeyStore.d.ts","sourceRoot":"","sources":["../../src/utils/secureKeyStore.ts"],"names":[],"mappings":"AAmZA;;;;GAIG;AAEH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD;AAED,wBAAsB,kBAAkB,IAAI,OAAO,CAAC,MAAM,CAAC,CAQ1D;AAED,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,UAAU,GACf,OAAO,CAAC,UAAU,CAAC,CAQrB;AAED,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CASrD"}
@@ -23,24 +23,66 @@ function ensureSupportedOS() {
23
23
  * ---------------------------------------------------------
24
24
  * WINDOWS
25
25
  * ---------------------------------------------------------
26
+ *
27
+ * Windows uses:
28
+ *
29
+ * Microsoft Platform Crypto Provider
30
+ * RSA 2048
31
+ * NonExportable private key
32
+ *
33
+ * The private key never enters Node.js.
34
+ *
35
+ * Node -> PowerShell -> Windows crypto provider
36
+ * ↓
37
+ * private key
38
+ * ↓
39
+ * signature
40
+ * ↓
41
+ * Node
42
+ *
43
+ * IMPORTANT:
44
+ *
45
+ * We identify the certificate using its properties rather
46
+ * than blindly taking the first certificate with the same
47
+ * subject.
26
48
  */
49
+ /**
50
+ * PowerShell expression used to identify a valid DS01
51
+ * certificate.
52
+ *
53
+ * Requirements:
54
+ *
55
+ * - Correct subject
56
+ * - Has private key
57
+ * - RSA certificate
58
+ * - Platform Crypto Provider
59
+ */
60
+ const WINDOWS_CERT_FILTER = `
61
+ $_.Subject -eq "CN=${KEY_NAME}" -and
62
+ $_.HasPrivateKey -and
63
+ $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
64
+ `;
27
65
  async function createWindowsDeviceKey() {
28
66
  const script = `
29
67
  $ErrorActionPreference = "Stop"
30
68
 
31
- $existing = Get-ChildItem Cert:\\CurrentUser\\My |
69
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
32
70
  Where-Object {
33
- $_.Subject -eq "CN=${KEY_NAME}"
34
- } |
35
- Select-Object -First 1
71
+ ${WINDOWS_CERT_FILTER}
72
+ })
73
+
74
+ if ($certificates.Count -gt 1) {
75
+ throw "Multiple valid DS01 device certificates found. Remove old DS01 device certificates before continuing."
76
+ }
36
77
 
37
- if ($existing) {
78
+ if ($certificates.Count -eq 1) {
38
79
  Write-Output "EXISTS"
39
80
  exit 0
40
81
  }
41
82
 
42
83
  $params = @{
43
84
  Type = "Custom"
85
+
44
86
  Subject = "CN=${KEY_NAME}"
45
87
 
46
88
  Provider = "Microsoft Platform Crypto Provider"
@@ -48,6 +90,7 @@ $params = @{
48
90
  KeyAlgorithm = "RSA"
49
91
  KeyLength = 2048
50
92
 
93
+ # Private key cannot be exported.
51
94
  KeyExportPolicy = "NonExportable"
52
95
 
53
96
  KeyUsage = "DigitalSignature"
@@ -58,7 +101,11 @@ $params = @{
58
101
  NotAfter = (Get-Date).AddYears(10)
59
102
  }
60
103
 
61
- New-SelfSignedCertificate @params | Out-Null
104
+ $certificate = New-SelfSignedCertificate @params
105
+
106
+ if (-not $certificate) {
107
+ throw "Windows failed to create the DS01 device certificate."
108
+ }
62
109
 
63
110
  Write-Output "CREATED"
64
111
  `;
@@ -77,20 +124,42 @@ Write-Output "CREATED"
77
124
  throw new Error("Failed to create Windows DS01 device key.");
78
125
  }
79
126
  }
80
- async function getWindowsPublicKey() {
81
- const script = `
82
- $ErrorActionPreference = "Stop"
83
-
84
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
127
+ /**
128
+ * Find the exact DS01 certificate.
129
+ *
130
+ * We deliberately DO NOT use Select-Object -First 1.
131
+ *
132
+ * If multiple matching certificates exist, we fail instead
133
+ * of potentially using the wrong private key.
134
+ */
135
+ function windowsCertificateLookupScript() {
136
+ return `
137
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
85
138
  Where-Object {
86
- $_.Subject -eq "CN=${KEY_NAME}"
87
- } |
88
- Select-Object -First 1
139
+ ${WINDOWS_CERT_FILTER}
140
+ })
89
141
 
90
- if (-not $cert) {
142
+ if ($certificates.Count -eq 0) {
91
143
  throw "DS01 device key not found."
92
144
  }
93
145
 
146
+ if ($certificates.Count -gt 1) {
147
+ throw "Multiple valid DS01 device certificates found."
148
+ }
149
+
150
+ $certificates[0]
151
+ `;
152
+ }
153
+ async function getWindowsPublicKey() {
154
+ const script = `
155
+ $ErrorActionPreference = "Stop"
156
+
157
+ $cert = ${windowsCertificateLookupScript()}
158
+
159
+ # Return the complete public certificate.
160
+ #
161
+ # The certificate contains the public key and certificate
162
+ # metadata, but never the private key.
94
163
  [Convert]::ToBase64String($cert.RawData)
95
164
  `;
96
165
  const { stdout } = await execFileAsync("powershell.exe", [
@@ -114,21 +183,14 @@ async function signWindowsDeviceKey(data) {
114
183
  const script = `
115
184
  $ErrorActionPreference = "Stop"
116
185
 
117
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
118
- Where-Object {
119
- $_.Subject -eq "CN=${KEY_NAME}"
120
- } |
121
- Select-Object -First 1
122
-
123
- if (-not $cert) {
124
- throw "DS01 device key not found."
125
- }
186
+ $cert = ${windowsCertificateLookupScript()}
126
187
 
127
188
  if (-not $cert.HasPrivateKey) {
128
189
  throw "DS01 device certificate has no private key."
129
190
  }
130
191
 
131
- $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
192
+ $rsa =
193
+ [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
132
194
 
133
195
  if (-not $rsa) {
134
196
  throw "Unable to access DS01 private signing key."
@@ -169,15 +231,20 @@ async function deleteWindowsDeviceKey() {
169
231
  const script = `
170
232
  $ErrorActionPreference = "Stop"
171
233
 
172
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
234
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
173
235
  Where-Object {
174
- $_.Subject -eq "CN=${KEY_NAME}"
175
- } |
176
- Select-Object -First 1
236
+ ${WINDOWS_CERT_FILTER}
237
+ })
177
238
 
178
- if ($cert) {
179
- Remove-Item $cert.PSPath
239
+ if ($certificates.Count -gt 1) {
240
+ throw "Multiple valid DS01 device certificates found."
180
241
  }
242
+
243
+ if ($certificates.Count -eq 1) {
244
+ Remove-Item $certificates[0].PSPath
245
+ }
246
+
247
+ Write-Output "DELETED"
181
248
  `;
182
249
  await execFileAsync("powershell.exe", [
183
250
  "-NoProfile",
@@ -208,8 +275,10 @@ async function runMacHelper(operation, data) {
208
275
  }
209
276
  async function createMacDeviceKey() {
210
277
  const result = await runMacHelper("create");
211
- if (result !== "CREATED" && result !== "EXISTS") {
212
- throw new Error(result || "Failed to create macOS Secure Enclave device key.");
278
+ if (result !== "CREATED" &&
279
+ result !== "EXISTS") {
280
+ throw new Error(result ||
281
+ "Failed to create macOS Secure Enclave device key.");
213
282
  }
214
283
  }
215
284
  async function getMacPublicKey() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ds-01",
3
- "version": "1.0.7",
3
+ "version": "1.0.9",
4
4
  "description": "Make your site best with DS01",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,8 +12,7 @@ import machineIdPkg from "node-machine-id";
12
12
 
13
13
  const { machineIdSync } = machineIdPkg;
14
14
 
15
- const API_BASE_URL =
16
- process.env.DS01_API_URL || "https://ds-01.vercel.app";
15
+ const API_BASE_URL = process.env.DS01_API_URL || "https://ds-01.vercel.app";
17
16
 
18
17
  // Helper to verify Tailwind exists before doing anything
19
18
  function checkTailwindInstallation() {
@@ -29,9 +28,7 @@ function checkTailwindInstallation() {
29
28
  process.exit(1);
30
29
  }
31
30
 
32
- const pkgJson = JSON.parse(
33
- fs.readFileSync(pkgJsonPath, "utf-8"),
34
- );
31
+ const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8"));
35
32
 
36
33
  const allDeps = {
37
34
  ...pkgJson.dependencies,
@@ -40,22 +37,16 @@ function checkTailwindInstallation() {
40
37
 
41
38
  if (!allDeps["tailwindcss"]) {
42
39
  cancel(
43
- pc.red(
44
- "Tailwind CSS is missing from your project dependencies.\n",
45
- ) +
40
+ pc.red("Tailwind CSS is missing from your project dependencies.\n") +
46
41
  pc.gray(
47
42
  "DS01 components rely strictly on Tailwind CSS for styling.\n\n",
48
43
  ) +
49
44
  pc.white(
50
45
  "Kindly install it and configure your project, then retry:\n",
51
46
  ) +
52
- pc.cyan(
53
- " npm install tailwindcss @tailwindcss/postcss postcss\n\n",
54
- ) +
47
+ pc.cyan(" npm install tailwindcss @tailwindcss/postcss postcss\n\n") +
55
48
  pc.gray("Official Setup Guide: ") +
56
- pc.underline(
57
- "https://tailwindcss.com/docs/installation",
58
- ),
49
+ pc.underline("https://tailwindcss.com/docs/installation"),
59
50
  );
60
51
 
61
52
  process.exit(1);
@@ -65,10 +56,7 @@ function checkTailwindInstallation() {
65
56
  export const add = new Command()
66
57
  .name("add")
67
58
  .description("Add a component from DS01 to your project")
68
- .argument(
69
- "<component>",
70
- "The name of the component (e.g., premium-section)",
71
- )
59
+ .argument("<component>", "The name of the component (e.g., premium-section)")
72
60
  .action(async (componentName: string) => {
73
61
  console.log();
74
62
 
@@ -82,45 +70,28 @@ export const add = new Command()
82
70
 
83
71
  const cwd = process.cwd();
84
72
 
85
- const configPath = path.join(
86
- cwd,
87
- "ds01.config.json",
88
- );
73
+ const configPath = path.join(cwd, "ds01.config.json");
89
74
 
90
75
  if (!fs.existsSync(configPath)) {
91
- cancel(
92
- pc.red(
93
- "ds01.config.json not found. Run 'npx ds-01 init' first.",
94
- ),
95
- );
76
+ cancel(pc.red("ds01.config.json not found. Run 'npx ds-01 init' first."));
96
77
 
97
78
  process.exit(1);
98
79
  }
99
80
 
100
- const config = JSON.parse(
101
- fs.readFileSync(configPath, "utf-8"),
102
- );
81
+ const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
103
82
 
104
83
  const token = getToken();
105
84
  const machineId = machineIdSync();
106
85
 
107
86
  if (!token || !machineId) {
108
- cancel(
109
- pc.red(
110
- "You are not authenticated. Run 'npx ds-01 login' first.",
111
- ),
112
- );
87
+ cancel(pc.red("You are not authenticated. Run 'npx ds-01 login' first."));
113
88
 
114
89
  process.exit(1);
115
90
  }
116
91
 
117
92
  const s = spinner();
118
93
 
119
- s.start(
120
- `Fetching ${pc.cyan(
121
- `<${componentName} />`,
122
- )} from registry...`,
123
- );
94
+ s.start(`Fetching ${pc.cyan(`<${componentName} />`)} from registry...`);
124
95
 
125
96
  try {
126
97
  // --------------------------------------------------
@@ -144,8 +115,7 @@ export const add = new Command()
144
115
  new TextEncoder().encode(message),
145
116
  );
146
117
 
147
- const signatureBase64 =
148
- Buffer.from(signature).toString("base64");
118
+ const signatureBase64 = Buffer.from(signature).toString("base64");
149
119
 
150
120
  // --------------------------------------------------
151
121
  // 3. Send token + machine ID + signature
@@ -169,29 +139,22 @@ export const add = new Command()
169
139
  );
170
140
 
171
141
  if (!response.ok) {
172
- s.stop(
173
- pc.red("Component fetch failed."),
174
- );
142
+ const errorData = await response.json().catch(() => null);
175
143
 
176
- cancel(
177
- `Component not found or server error (HTTP ${response.status})`,
178
- );
144
+ s.stop(pc.red("Component fetch failed."));
145
+
146
+ cancel(errorData?.error || `Server returned HTTP ${response.status}`);
179
147
 
180
148
  process.exit(1);
181
149
  }
182
150
 
183
- const componentData =
184
- await response.json();
151
+ const componentData = await response.json();
185
152
 
186
153
  // --------------------------------------------------
187
154
  // 4. Create component folder
188
155
  // --------------------------------------------------
189
156
 
190
- const targetDir = path.join(
191
- cwd,
192
- config.componentsPath,
193
- componentName,
194
- );
157
+ const targetDir = path.join(cwd, config.componentsPath, componentName);
195
158
 
196
159
  if (!fs.existsSync(targetDir)) {
197
160
  fs.mkdirSync(targetDir, {
@@ -203,45 +166,29 @@ export const add = new Command()
203
166
  // 5. Extract bundled file names
204
167
  // --------------------------------------------------
205
168
 
206
- const bundledFileNames =
207
- componentData.files.map(
208
- (file: any) =>
209
- file.name.replace(
210
- /\.[^/.]+$/,
211
- "",
212
- ),
213
- );
169
+ const bundledFileNames = componentData.files.map((file: any) =>
170
+ file.name.replace(/\.[^/.]+$/, ""),
171
+ );
214
172
 
215
173
  // --------------------------------------------------
216
174
  // 6. Write component files
217
175
  // --------------------------------------------------
218
176
 
219
177
  for (const file of componentData.files) {
220
- const filePath = path.join(
221
- targetDir,
222
- file.name,
223
- );
178
+ const filePath = path.join(targetDir, file.name);
224
179
 
225
180
  let content = file.content;
226
181
 
227
- bundledFileNames.forEach(
228
- (fileName: string) => {
229
- const regex = new RegExp(
230
- `from\\s+["']\\.[^"']*?\\/${fileName}["']`,
231
- "g",
232
- );
233
-
234
- content = content.replace(
235
- regex,
236
- `from "./${fileName}"`,
237
- );
238
- },
239
- );
182
+ bundledFileNames.forEach((fileName: string) => {
183
+ const regex = new RegExp(
184
+ `from\\s+["']\\.[^"']*?\\/${fileName}["']`,
185
+ "g",
186
+ );
240
187
 
241
- fs.writeFileSync(
242
- filePath,
243
- content,
244
- );
188
+ content = content.replace(regex, `from "./${fileName}"`);
189
+ });
190
+
191
+ fs.writeFileSync(filePath, content);
245
192
  }
246
193
 
247
194
  s.stop(
@@ -256,45 +203,28 @@ export const add = new Command()
256
203
  // 7. Install dependencies
257
204
  // --------------------------------------------------
258
205
 
259
- if (
260
- componentData.dependencies &&
261
- componentData.dependencies.length > 0
262
- ) {
263
- const depsToInstall =
264
- componentData.dependencies.join(" ");
206
+ if (componentData.dependencies && componentData.dependencies.length > 0) {
207
+ const depsToInstall = componentData.dependencies.join(" ");
265
208
 
266
209
  s.start(
267
- `Installing missing dependencies: ${pc.cyan(
268
- depsToInstall,
269
- )}...`,
210
+ `Installing missing dependencies: ${pc.cyan(depsToInstall)}...`,
270
211
  );
271
212
 
272
213
  try {
273
- execSync(
274
- `npm install ${depsToInstall}`,
275
- {
276
- stdio: "ignore",
277
- },
278
- );
214
+ execSync(`npm install ${depsToInstall}`, {
215
+ stdio: "ignore",
216
+ });
279
217
 
280
218
  s.stop(
281
219
  pc.green(
282
- `Dependencies installed successfully: ${pc.gray(
283
- depsToInstall,
284
- )}`,
220
+ `Dependencies installed successfully: ${pc.gray(depsToInstall)}`,
285
221
  ),
286
222
  );
287
223
  } catch {
288
- s.stop(
289
- pc.red(
290
- "Failed to auto-install dependencies.",
291
- ),
292
- );
224
+ s.stop(pc.red("Failed to auto-install dependencies."));
293
225
 
294
226
  note(
295
- `Please run: ${pc.cyan(
296
- `npm install ${depsToInstall}`,
297
- )} manually.`,
227
+ `Please run: ${pc.cyan(`npm install ${depsToInstall}`)} manually.`,
298
228
  "Manual Action Required",
299
229
  );
300
230
  }
@@ -306,9 +236,7 @@ export const add = new Command()
306
236
 
307
237
  const mainFile =
308
238
  bundledFileNames.find(
309
- (name: string) =>
310
- name.toLowerCase() ===
311
- componentName.toLowerCase(),
239
+ (name: string) => name.toLowerCase() === componentName.toLowerCase(),
312
240
  ) || bundledFileNames[0];
313
241
 
314
242
  outro(
@@ -321,19 +249,10 @@ export const add = new Command()
321
249
  ),
322
250
  );
323
251
  } catch (error: any) {
324
- s.stop(
325
- pc.red(
326
- "An error occurred during injection.",
327
- ),
328
- );
252
+ s.stop(pc.red("An error occurred during injection."));
329
253
 
330
- cancel(
331
- pc.gray(
332
- error?.message ||
333
- "Unknown CLI Error",
334
- ),
335
- );
254
+ cancel(pc.gray(error?.message || "Unknown CLI Error"));
336
255
 
337
256
  process.exit(1);
338
257
  }
339
- });
258
+ });
@@ -22,7 +22,7 @@ function getMacHelperPath(): string {
22
22
  );
23
23
  }
24
24
 
25
- function ensureSupportedOS() {
25
+ function ensureSupportedOS(): void {
26
26
  const platform = os.platform();
27
27
 
28
28
  if (platform !== "win32" && platform !== "darwin") {
@@ -36,25 +36,68 @@ function ensureSupportedOS() {
36
36
  * ---------------------------------------------------------
37
37
  * WINDOWS
38
38
  * ---------------------------------------------------------
39
+ *
40
+ * Windows uses:
41
+ *
42
+ * Microsoft Platform Crypto Provider
43
+ * RSA 2048
44
+ * NonExportable private key
45
+ *
46
+ * The private key never enters Node.js.
47
+ *
48
+ * Node -> PowerShell -> Windows crypto provider
49
+ * ↓
50
+ * private key
51
+ * ↓
52
+ * signature
53
+ * ↓
54
+ * Node
55
+ *
56
+ * IMPORTANT:
57
+ *
58
+ * We identify the certificate using its properties rather
59
+ * than blindly taking the first certificate with the same
60
+ * subject.
39
61
  */
40
62
 
63
+ /**
64
+ * PowerShell expression used to identify a valid DS01
65
+ * certificate.
66
+ *
67
+ * Requirements:
68
+ *
69
+ * - Correct subject
70
+ * - Has private key
71
+ * - RSA certificate
72
+ * - Platform Crypto Provider
73
+ */
74
+ const WINDOWS_CERT_FILTER = `
75
+ $_.Subject -eq "CN=${KEY_NAME}" -and
76
+ $_.HasPrivateKey -and
77
+ $_.PublicKey.Oid.Value -eq "1.2.840.113549.1.1.1"
78
+ `;
79
+
41
80
  async function createWindowsDeviceKey(): Promise<void> {
42
81
  const script = `
43
82
  $ErrorActionPreference = "Stop"
44
83
 
45
- $existing = Get-ChildItem Cert:\\CurrentUser\\My |
84
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
46
85
  Where-Object {
47
- $_.Subject -eq "CN=${KEY_NAME}"
48
- } |
49
- Select-Object -First 1
86
+ ${WINDOWS_CERT_FILTER}
87
+ })
88
+
89
+ if ($certificates.Count -gt 1) {
90
+ throw "Multiple valid DS01 device certificates found. Remove old DS01 device certificates before continuing."
91
+ }
50
92
 
51
- if ($existing) {
93
+ if ($certificates.Count -eq 1) {
52
94
  Write-Output "EXISTS"
53
95
  exit 0
54
96
  }
55
97
 
56
98
  $params = @{
57
99
  Type = "Custom"
100
+
58
101
  Subject = "CN=${KEY_NAME}"
59
102
 
60
103
  Provider = "Microsoft Platform Crypto Provider"
@@ -62,6 +105,7 @@ $params = @{
62
105
  KeyAlgorithm = "RSA"
63
106
  KeyLength = 2048
64
107
 
108
+ # Private key cannot be exported.
65
109
  KeyExportPolicy = "NonExportable"
66
110
 
67
111
  KeyUsage = "DigitalSignature"
@@ -72,7 +116,11 @@ $params = @{
72
116
  NotAfter = (Get-Date).AddYears(10)
73
117
  }
74
118
 
75
- New-SelfSignedCertificate @params | Out-Null
119
+ $certificate = New-SelfSignedCertificate @params
120
+
121
+ if (-not $certificate) {
122
+ throw "Windows failed to create the DS01 device certificate."
123
+ }
76
124
 
77
125
  Write-Output "CREATED"
78
126
  `;
@@ -95,24 +143,49 @@ Write-Output "CREATED"
95
143
  const result = stdout.trim();
96
144
 
97
145
  if (result !== "CREATED" && result !== "EXISTS") {
98
- throw new Error("Failed to create Windows DS01 device key.");
146
+ throw new Error(
147
+ "Failed to create Windows DS01 device key.",
148
+ );
99
149
  }
100
150
  }
101
151
 
102
- async function getWindowsPublicKey(): Promise<string> {
103
- const script = `
104
- $ErrorActionPreference = "Stop"
105
-
106
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
152
+ /**
153
+ * Find the exact DS01 certificate.
154
+ *
155
+ * We deliberately DO NOT use Select-Object -First 1.
156
+ *
157
+ * If multiple matching certificates exist, we fail instead
158
+ * of potentially using the wrong private key.
159
+ */
160
+ function windowsCertificateLookupScript(): string {
161
+ return `
162
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
107
163
  Where-Object {
108
- $_.Subject -eq "CN=${KEY_NAME}"
109
- } |
110
- Select-Object -First 1
164
+ ${WINDOWS_CERT_FILTER}
165
+ })
111
166
 
112
- if (-not $cert) {
167
+ if ($certificates.Count -eq 0) {
113
168
  throw "DS01 device key not found."
114
169
  }
115
170
 
171
+ if ($certificates.Count -gt 1) {
172
+ throw "Multiple valid DS01 device certificates found."
173
+ }
174
+
175
+ $certificates[0]
176
+ `;
177
+ }
178
+
179
+ async function getWindowsPublicKey(): Promise<string> {
180
+ const script = `
181
+ $ErrorActionPreference = "Stop"
182
+
183
+ $cert = ${windowsCertificateLookupScript()}
184
+
185
+ # Return the complete public certificate.
186
+ #
187
+ # The certificate contains the public key and certificate
188
+ # metadata, but never the private key.
116
189
  [Convert]::ToBase64String($cert.RawData)
117
190
  `;
118
191
 
@@ -134,7 +207,9 @@ if (-not $cert) {
134
207
  const certificate = stdout.trim();
135
208
 
136
209
  if (!certificate) {
137
- throw new Error("Unable to read Windows DS01 public certificate.");
210
+ throw new Error(
211
+ "Unable to read Windows DS01 public certificate.",
212
+ );
138
213
  }
139
214
 
140
215
  return certificate;
@@ -143,26 +218,20 @@ if (-not $cert) {
143
218
  async function signWindowsDeviceKey(
144
219
  data: Uint8Array,
145
220
  ): Promise<Uint8Array> {
146
- const dataBase64 = Buffer.from(data).toString("base64");
221
+ const dataBase64 =
222
+ Buffer.from(data).toString("base64");
147
223
 
148
224
  const script = `
149
225
  $ErrorActionPreference = "Stop"
150
226
 
151
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
152
- Where-Object {
153
- $_.Subject -eq "CN=${KEY_NAME}"
154
- } |
155
- Select-Object -First 1
156
-
157
- if (-not $cert) {
158
- throw "DS01 device key not found."
159
- }
227
+ $cert = ${windowsCertificateLookupScript()}
160
228
 
161
229
  if (-not $cert.HasPrivateKey) {
162
230
  throw "DS01 device certificate has no private key."
163
231
  }
164
232
 
165
- $rsa = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
233
+ $rsa =
234
+ [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
166
235
 
167
236
  if (-not $rsa) {
168
237
  throw "Unable to access DS01 private signing key."
@@ -202,25 +271,34 @@ finally {
202
271
  const signature = stdout.trim();
203
272
 
204
273
  if (!signature) {
205
- throw new Error("Failed to sign data with Windows device key.");
274
+ throw new Error(
275
+ "Failed to sign data with Windows device key.",
276
+ );
206
277
  }
207
278
 
208
- return new Uint8Array(Buffer.from(signature, "base64"));
279
+ return new Uint8Array(
280
+ Buffer.from(signature, "base64"),
281
+ );
209
282
  }
210
283
 
211
284
  async function deleteWindowsDeviceKey(): Promise<void> {
212
285
  const script = `
213
286
  $ErrorActionPreference = "Stop"
214
287
 
215
- $cert = Get-ChildItem Cert:\\CurrentUser\\My |
288
+ $certificates = @(Get-ChildItem Cert:\\CurrentUser\\My |
216
289
  Where-Object {
217
- $_.Subject -eq "CN=${KEY_NAME}"
218
- } |
219
- Select-Object -First 1
290
+ ${WINDOWS_CERT_FILTER}
291
+ })
220
292
 
221
- if ($cert) {
222
- Remove-Item $cert.PSPath
293
+ if ($certificates.Count -gt 1) {
294
+ throw "Multiple valid DS01 device certificates found."
223
295
  }
296
+
297
+ if ($certificates.Count -eq 1) {
298
+ Remove-Item $certificates[0].PSPath
299
+ }
300
+
301
+ Write-Output "DELETED"
224
302
  `;
225
303
 
226
304
  await execFileAsync(
@@ -246,7 +324,11 @@ if ($cert) {
246
324
  */
247
325
 
248
326
  async function runMacHelper(
249
- operation: "create" | "public" | "sign" | "delete",
327
+ operation:
328
+ | "create"
329
+ | "public"
330
+ | "sign"
331
+ | "delete",
250
332
  data?: Uint8Array,
251
333
  ): Promise<string> {
252
334
  const helper = getMacHelperPath();
@@ -254,7 +336,9 @@ async function runMacHelper(
254
336
  const args = [helper, operation];
255
337
 
256
338
  if (data) {
257
- args.push(Buffer.from(data).toString("base64"));
339
+ args.push(
340
+ Buffer.from(data).toString("base64"),
341
+ );
258
342
  }
259
343
 
260
344
  const { stdout } = await execFileAsync(
@@ -269,17 +353,23 @@ async function runMacHelper(
269
353
  }
270
354
 
271
355
  async function createMacDeviceKey(): Promise<void> {
272
- const result = await runMacHelper("create");
356
+ const result =
357
+ await runMacHelper("create");
273
358
 
274
- if (result !== "CREATED" && result !== "EXISTS") {
359
+ if (
360
+ result !== "CREATED" &&
361
+ result !== "EXISTS"
362
+ ) {
275
363
  throw new Error(
276
- result || "Failed to create macOS Secure Enclave device key.",
364
+ result ||
365
+ "Failed to create macOS Secure Enclave device key.",
277
366
  );
278
367
  }
279
368
  }
280
369
 
281
370
  async function getMacPublicKey(): Promise<string> {
282
- const result = await runMacHelper("public");
371
+ const result =
372
+ await runMacHelper("public");
283
373
 
284
374
  if (!result) {
285
375
  throw new Error(
@@ -293,7 +383,8 @@ async function getMacPublicKey(): Promise<string> {
293
383
  async function signMacDeviceKey(
294
384
  data: Uint8Array,
295
385
  ): Promise<Uint8Array> {
296
- const result = await runMacHelper("sign", data);
386
+ const result =
387
+ await runMacHelper("sign", data);
297
388
 
298
389
  if (!result) {
299
390
  throw new Error(