ds-01 1.0.11 → 1.0.13

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.
@@ -0,0 +1,272 @@
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
+ // Find the existing Secure Enclave private key.
12
+ func findPrivateKey() -> SecKey? {
13
+ let query: [String: Any] = [
14
+ kSecClass as String: kSecClassKey,
15
+ kSecAttrApplicationTag as String:
16
+ keyTag.data(using: .utf8)!,
17
+ kSecAttrKeyType as String:
18
+ kSecAttrKeyTypeECSECPrimeRandom,
19
+ kSecAttrTokenID as String:
20
+ kSecAttrTokenIDSecureEnclave,
21
+ kSecReturnRef as String: true
22
+ ]
23
+
24
+ var result: CFTypeRef?
25
+
26
+ let status = SecItemCopyMatching(
27
+ query as CFDictionary,
28
+ &result
29
+ )
30
+
31
+ guard status == errSecSuccess else {
32
+ return nil
33
+ }
34
+
35
+ return (result as! SecKey)
36
+ }
37
+
38
+ func createKey() {
39
+
40
+ // If the key already exists, don't create another one.
41
+ if findPrivateKey() != nil {
42
+ print("EXISTS")
43
+ return
44
+ }
45
+
46
+ // Keychain access control.
47
+ //
48
+ // WhenUnlockedThisDeviceOnly means:
49
+ // - available only while the device is unlocked
50
+ // - cannot be restored onto another device
51
+ //
52
+ // privateKeyUsage means the key can be used for signing.
53
+ var accessError: Unmanaged<CFError>?
54
+
55
+ guard let access = SecAccessControlCreateWithFlags(
56
+ nil,
57
+ kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
58
+ .privateKeyUsage,
59
+ &accessError
60
+ ) else {
61
+
62
+ if let error = accessError {
63
+ fail(
64
+ "Unable to create Keychain access control: " +
65
+ error.takeRetainedValue().localizedDescription
66
+ )
67
+ }
68
+
69
+ fail("Unable to create Keychain access control.")
70
+ }
71
+
72
+ let privateAttributes: [String: Any] = [
73
+
74
+ // Permanently store the private key.
75
+ kSecAttrIsPermanent as String: true,
76
+
77
+ // Identifier used to find the key later.
78
+ kSecAttrApplicationTag as String:
79
+ keyTag.data(using: .utf8)!,
80
+
81
+ // Access rules for the private key.
82
+ kSecAttrAccessControl as String:
83
+ access
84
+ ]
85
+
86
+ let attributes: [String: Any] = [
87
+
88
+ // EC P-256 key.
89
+ kSecAttrKeyType as String:
90
+ kSecAttrKeyTypeECSECPrimeRandom,
91
+
92
+ kSecAttrKeySizeInBits as String:
93
+ 256,
94
+
95
+ // IMPORTANT:
96
+ // The private key lives inside the Secure Enclave.
97
+ kSecAttrTokenID as String:
98
+ kSecAttrTokenIDSecureEnclave,
99
+
100
+ // Private key configuration.
101
+ kSecPrivateKeyAttrs as String:
102
+ privateAttributes
103
+ ]
104
+
105
+ var error: Unmanaged<CFError>?
106
+
107
+ guard SecKeyCreateRandomKey(
108
+ attributes as CFDictionary,
109
+ &error
110
+ ) != nil else {
111
+
112
+ if let error {
113
+ fail(
114
+ "Secure Enclave key creation failed: " +
115
+ error.takeRetainedValue().localizedDescription
116
+ )
117
+ }
118
+
119
+ fail("Secure Enclave key creation failed.")
120
+ }
121
+
122
+ print("CREATED")
123
+ }
124
+
125
+
126
+ // Return ONLY the public key.
127
+ //
128
+ // The private key is never exported.
129
+ func publicKey() {
130
+
131
+ guard let privateKey = findPrivateKey() else {
132
+ fail("DS01 Secure Enclave key not found.")
133
+ }
134
+
135
+ guard let publicKey = SecKeyCopyPublicKey(privateKey) else {
136
+ fail("Unable to obtain public key.")
137
+ }
138
+
139
+ var error: Unmanaged<CFError>?
140
+
141
+ guard let data = SecKeyCopyExternalRepresentation(
142
+ publicKey,
143
+ &error
144
+ ) else {
145
+
146
+ if let error {
147
+ fail(
148
+ "Unable to export public key: " +
149
+ error.takeRetainedValue().localizedDescription
150
+ )
151
+ }
152
+
153
+ fail("Unable to export public key.")
154
+ }
155
+
156
+ print(
157
+ (data as Data).base64EncodedString()
158
+ )
159
+ }
160
+
161
+
162
+ // Sign data using the Secure Enclave private key.
163
+ func sign(_ base64Data: String) {
164
+
165
+ guard let privateKey = findPrivateKey() else {
166
+ fail("DS01 Secure Enclave key not found.")
167
+ }
168
+
169
+ guard let data = Data(
170
+ base64Encoded: base64Data
171
+ ) else {
172
+ fail("Invalid input data.")
173
+ }
174
+
175
+ let algorithm =
176
+ SecKeyAlgorithm.ecdsaSignatureMessageX962SHA256
177
+
178
+ guard SecKeyIsAlgorithmSupported(
179
+ privateKey,
180
+ .sign,
181
+ algorithm
182
+ ) else {
183
+ fail(
184
+ "Secure Enclave key does not support signing."
185
+ )
186
+ }
187
+
188
+ var error: Unmanaged<CFError>?
189
+
190
+ guard let signature = SecKeyCreateSignature(
191
+ privateKey,
192
+ algorithm,
193
+ data as CFData,
194
+ &error
195
+ ) else {
196
+
197
+ if let error {
198
+ fail(
199
+ "Secure Enclave signing failed: " +
200
+ error.takeRetainedValue().localizedDescription
201
+ )
202
+ }
203
+
204
+ fail("Secure Enclave signing failed.")
205
+ }
206
+
207
+ print(
208
+ (signature as Data).base64EncodedString()
209
+ )
210
+ }
211
+
212
+
213
+ // Remove the DS01 key from this Mac.
214
+ func deleteKey() {
215
+
216
+ let query: [String: Any] = [
217
+ kSecClass as String:
218
+ kSecClassKey,
219
+
220
+ kSecAttrApplicationTag as String:
221
+ keyTag.data(using: .utf8)!,
222
+
223
+ kSecAttrKeyType as String:
224
+ kSecAttrKeyTypeECSECPrimeRandom,
225
+
226
+ kSecAttrTokenID as String:
227
+ kSecAttrTokenIDSecureEnclave
228
+ ]
229
+
230
+ let status = SecItemDelete(
231
+ query as CFDictionary
232
+ )
233
+
234
+ if status != errSecSuccess &&
235
+ status != errSecItemNotFound {
236
+
237
+ fail(
238
+ "Unable to delete DS01 Secure Enclave key."
239
+ )
240
+ }
241
+
242
+ print("DELETED")
243
+ }
244
+
245
+
246
+ // Command dispatcher.
247
+ guard CommandLine.arguments.count >= 2 else {
248
+ fail("Missing operation.")
249
+ }
250
+
251
+ switch CommandLine.arguments[1] {
252
+
253
+ case "create":
254
+ createKey()
255
+
256
+ case "public":
257
+ publicKey()
258
+
259
+ case "sign":
260
+
261
+ guard CommandLine.arguments.count >= 3 else {
262
+ fail("Missing data to sign.")
263
+ }
264
+
265
+ sign(CommandLine.arguments[2])
266
+
267
+ case "delete":
268
+ deleteKey()
269
+
270
+ default:
271
+ fail("Unknown operation.")
272
+ }
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "ds-01",
3
- "version": "1.0.11",
3
+ "version": "1.0.13",
4
4
  "description": "Make your site best with DS01",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "ds01": "dist/index.js"
8
8
  },
9
9
  "scripts": {
10
- "build": "tsc"
10
+ "build": "tsc && node scripts/copy-native.js"
11
11
  },
12
12
  "publishConfig": {
13
13
  "access": "public"
@@ -0,0 +1,15 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+
4
+ const source = path.resolve("src/native/macos-key-helper.swift");
5
+ const destinationDir = path.resolve("dist/native");
6
+ const destination = path.join(
7
+ destinationDir,
8
+ "macos-key-helper.swift",
9
+ );
10
+
11
+ fs.mkdirSync(destinationDir, { recursive: true });
12
+
13
+ fs.copyFileSync(source, destination);
14
+
15
+
@@ -8,30 +8,16 @@ func fail(_ message: String) -> Never {
8
8
  exit(1)
9
9
  }
10
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
-
11
+ // Find the existing Secure Enclave private key.
30
12
  func findPrivateKey() -> SecKey? {
31
13
  let query: [String: Any] = [
32
14
  kSecClass as String: kSecClassKey,
33
- kSecAttrApplicationTag as String: keyTag.data(using: .utf8)!,
34
- kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
15
+ kSecAttrApplicationTag as String:
16
+ keyTag.data(using: .utf8)!,
17
+ kSecAttrKeyType as String:
18
+ kSecAttrKeyTypeECSECPrimeRandom,
19
+ kSecAttrTokenID as String:
20
+ kSecAttrTokenIDSecureEnclave,
35
21
  kSecReturnRef as String: true
36
22
  ]
37
23
 
@@ -50,33 +36,70 @@ func findPrivateKey() -> SecKey? {
50
36
  }
51
37
 
52
38
  func createKey() {
39
+
40
+ // If the key already exists, don't create another one.
53
41
  if findPrivateKey() != nil {
54
42
  print("EXISTS")
55
43
  return
56
44
  }
57
45
 
58
- let access = getAccessControl()
46
+ // Keychain access control.
47
+ //
48
+ // WhenUnlockedThisDeviceOnly means:
49
+ // - available only while the device is unlocked
50
+ // - cannot be restored onto another device
51
+ //
52
+ // privateKeyUsage means the key can be used for signing.
53
+ var accessError: Unmanaged<CFError>?
54
+
55
+ guard let access = SecAccessControlCreateWithFlags(
56
+ nil,
57
+ kSecAttrAccessibleWhenUnlockedThisDeviceOnly,
58
+ .privateKeyUsage,
59
+ &accessError
60
+ ) else {
61
+
62
+ if let error = accessError {
63
+ fail(
64
+ "Unable to create Keychain access control: " +
65
+ error.takeRetainedValue().localizedDescription
66
+ )
67
+ }
68
+
69
+ fail("Unable to create Keychain access control.")
70
+ }
71
+
72
+ let privateAttributes: [String: Any] = [
73
+
74
+ // Permanently store the private key.
75
+ kSecAttrIsPermanent as String: true,
76
+
77
+ // Identifier used to find the key later.
78
+ kSecAttrApplicationTag as String:
79
+ keyTag.data(using: .utf8)!,
80
+
81
+ // Access rules for the private key.
82
+ kSecAttrAccessControl as String:
83
+ access
84
+ ]
59
85
 
60
86
  let attributes: [String: Any] = [
87
+
88
+ // EC P-256 key.
61
89
  kSecAttrKeyType as String:
62
90
  kSecAttrKeyTypeECSECPrimeRandom,
63
91
 
64
92
  kSecAttrKeySizeInBits as String:
65
93
  256,
66
94
 
95
+ // IMPORTANT:
96
+ // The private key lives inside the Secure Enclave.
67
97
  kSecAttrTokenID as String:
68
98
  kSecAttrTokenIDSecureEnclave,
69
99
 
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
- ]
100
+ // Private key configuration.
101
+ kSecPrivateKeyAttrs as String:
102
+ privateAttributes
80
103
  ]
81
104
 
82
105
  var error: Unmanaged<CFError>?
@@ -85,17 +108,26 @@ func createKey() {
85
108
  attributes as CFDictionary,
86
109
  &error
87
110
  ) != nil else {
111
+
88
112
  if let error {
89
- fail(error.takeRetainedValue().localizedDescription)
113
+ fail(
114
+ "Secure Enclave key creation failed: " +
115
+ error.takeRetainedValue().localizedDescription
116
+ )
90
117
  }
91
118
 
92
- fail("Unable to create Secure Enclave key.")
119
+ fail("Secure Enclave key creation failed.")
93
120
  }
94
121
 
95
122
  print("CREATED")
96
123
  }
97
124
 
125
+
126
+ // Return ONLY the public key.
127
+ //
128
+ // The private key is never exported.
98
129
  func publicKey() {
130
+
99
131
  guard let privateKey = findPrivateKey() else {
100
132
  fail("DS01 Secure Enclave key not found.")
101
133
  }
@@ -110,35 +142,47 @@ func publicKey() {
110
142
  publicKey,
111
143
  &error
112
144
  ) else {
145
+
113
146
  if let error {
114
- fail(error.takeRetainedValue().localizedDescription)
147
+ fail(
148
+ "Unable to export public key: " +
149
+ error.takeRetainedValue().localizedDescription
150
+ )
115
151
  }
116
152
 
117
153
  fail("Unable to export public key.")
118
154
  }
119
155
 
120
- let base64 = (data as Data).base64EncodedString()
121
-
122
- print(base64)
156
+ print(
157
+ (data as Data).base64EncodedString()
158
+ )
123
159
  }
124
160
 
161
+
162
+ // Sign data using the Secure Enclave private key.
125
163
  func sign(_ base64Data: String) {
164
+
126
165
  guard let privateKey = findPrivateKey() else {
127
166
  fail("DS01 Secure Enclave key not found.")
128
167
  }
129
168
 
130
- guard let data = Data(base64Encoded: base64Data) else {
169
+ guard let data = Data(
170
+ base64Encoded: base64Data
171
+ ) else {
131
172
  fail("Invalid input data.")
132
173
  }
133
174
 
134
- let algorithm = SecKeyAlgorithm.ecdsaSignatureMessageX962SHA256
175
+ let algorithm =
176
+ SecKeyAlgorithm.ecdsaSignatureMessageX962SHA256
135
177
 
136
178
  guard SecKeyIsAlgorithmSupported(
137
179
  privateKey,
138
180
  .sign,
139
181
  algorithm
140
182
  ) else {
141
- fail("Secure Enclave key does not support signing.")
183
+ fail(
184
+ "Secure Enclave key does not support signing."
185
+ )
142
186
  }
143
187
 
144
188
  var error: Unmanaged<CFError>?
@@ -149,23 +193,38 @@ func sign(_ base64Data: String) {
149
193
  data as CFData,
150
194
  &error
151
195
  ) else {
196
+
152
197
  if let error {
153
- fail(error.takeRetainedValue().localizedDescription)
198
+ fail(
199
+ "Secure Enclave signing failed: " +
200
+ error.takeRetainedValue().localizedDescription
201
+ )
154
202
  }
155
203
 
156
204
  fail("Secure Enclave signing failed.")
157
205
  }
158
206
 
159
- print((signature as Data).base64EncodedString())
207
+ print(
208
+ (signature as Data).base64EncodedString()
209
+ )
160
210
  }
161
211
 
212
+
213
+ // Remove the DS01 key from this Mac.
162
214
  func deleteKey() {
215
+
163
216
  let query: [String: Any] = [
164
- kSecClass as String: kSecClassKey,
217
+ kSecClass as String:
218
+ kSecClassKey,
219
+
165
220
  kSecAttrApplicationTag as String:
166
221
  keyTag.data(using: .utf8)!,
222
+
167
223
  kSecAttrKeyType as String:
168
- kSecAttrKeyTypeECSECPrimeRandom
224
+ kSecAttrKeyTypeECSECPrimeRandom,
225
+
226
+ kSecAttrTokenID as String:
227
+ kSecAttrTokenIDSecureEnclave
169
228
  ]
170
229
 
171
230
  let status = SecItemDelete(
@@ -174,17 +233,23 @@ func deleteKey() {
174
233
 
175
234
  if status != errSecSuccess &&
176
235
  status != errSecItemNotFound {
177
- fail("Unable to delete DS01 Secure Enclave key.")
236
+
237
+ fail(
238
+ "Unable to delete DS01 Secure Enclave key."
239
+ )
178
240
  }
179
241
 
180
242
  print("DELETED")
181
243
  }
182
244
 
245
+
246
+ // Command dispatcher.
183
247
  guard CommandLine.arguments.count >= 2 else {
184
248
  fail("Missing operation.")
185
249
  }
186
250
 
187
251
  switch CommandLine.arguments[1] {
252
+
188
253
  case "create":
189
254
  createKey()
190
255
 
@@ -192,6 +257,7 @@ case "public":
192
257
  publicKey()
193
258
 
194
259
  case "sign":
260
+
195
261
  guard CommandLine.arguments.count >= 3 else {
196
262
  fail("Missing data to sign.")
197
263
  }