@vercel/cli-auth 0.3.12 → 0.3.14-snapshot-dd15927e8ded

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/tpm.ts ADDED
@@ -0,0 +1,221 @@
1
+ // Seals a secret with an RSA key that lives in this PC's TPM, through CNG's
2
+ // Platform Crypto Provider (called with koffi, so there's no native code to
3
+ // build). The TPM keeps the private key and only hands out what it unseals:
4
+ // what we save is the key's name, which is useless on any other PC.
5
+
6
+ import { type Koffi, loadKoffi } from './koffi';
7
+
8
+ export type Handle = number | bigint;
9
+
10
+ // String constants from `ncrypt.h` and `bcrypt.h`.
11
+ const PLATFORM_CRYPTO_PROVIDER = 'Microsoft Platform Crypto Provider';
12
+ const NCRYPT_RSA_ALGORITHM = 'RSA';
13
+ const NCRYPT_LENGTH_PROPERTY = 'Length';
14
+ const NCRYPT_KEY_USAGE_PROPERTY = 'Key Usage';
15
+ const BCRYPT_SHA256_ALGORITHM = 'SHA256';
16
+ const RSA_KEY_BITS = 2048;
17
+ // `ncrypt.h`: the key may only decrypt.
18
+ const NCRYPT_ALLOW_DECRYPT_FLAG = 0x1;
19
+ // `ncrypt.h`: RSA-OAEP, with a `BCRYPT_OAEP_PADDING_INFO`.
20
+ const NCRYPT_PAD_OAEP_FLAG = 0x4;
21
+ // `ncrypt.h`: fail instead of showing UI, since the CLI may not be
22
+ // interactive.
23
+ const NCRYPT_SILENT_FLAG = 0x40;
24
+ // `winerror.h` NTE_BAD_KEYSET: there's no key with that name.
25
+ const NTE_BAD_KEYSET = 0x80090016;
26
+
27
+ let api: ReturnType<typeof loadApi> | undefined;
28
+
29
+ function loadApi(koffi: Koffi) {
30
+ const ncrypt = koffi.load('ncrypt.dll');
31
+ koffi.struct('BCRYPT_OAEP_PADDING_INFO', {
32
+ pszAlgId: 'str16',
33
+ pbLabel: 'void *',
34
+ cbLabel: 'uint32_t',
35
+ });
36
+ return {
37
+ NCryptOpenStorageProvider: ncrypt.func(
38
+ 'int32_t __stdcall NCryptOpenStorageProvider(_Out_ uintptr_t *phProvider, const char16_t *pszProviderName, uint32_t dwFlags)'
39
+ ),
40
+ NCryptCreatePersistedKey: ncrypt.func(
41
+ 'int32_t __stdcall NCryptCreatePersistedKey(uintptr_t hProvider, _Out_ uintptr_t *phKey, const char16_t *pszAlgId, const char16_t *pszKeyName, uint32_t dwLegacyKeySpec, uint32_t dwFlags)'
42
+ ),
43
+ NCryptSetProperty: ncrypt.func(
44
+ 'int32_t __stdcall NCryptSetProperty(uintptr_t hObject, const char16_t *pszProperty, const uint8_t *pbInput, uint32_t cbInput, uint32_t dwFlags)'
45
+ ),
46
+ NCryptFinalizeKey: ncrypt.func(
47
+ 'int32_t __stdcall NCryptFinalizeKey(uintptr_t hKey, uint32_t dwFlags)'
48
+ ),
49
+ NCryptOpenKey: ncrypt.func(
50
+ 'int32_t __stdcall NCryptOpenKey(uintptr_t hProvider, _Out_ uintptr_t *phKey, const char16_t *pszKeyName, uint32_t dwLegacyKeySpec, uint32_t dwFlags)'
51
+ ),
52
+ NCryptEncrypt: ncrypt.func(
53
+ 'int32_t __stdcall NCryptEncrypt(uintptr_t hKey, const uint8_t *pbInput, uint32_t cbInput, const BCRYPT_OAEP_PADDING_INFO *pPaddingInfo, _Out_ uint8_t *pbOutput, uint32_t cbOutput, _Out_ uint32_t *pcbResult, uint32_t dwFlags)'
54
+ ),
55
+ NCryptDecrypt: ncrypt.func(
56
+ 'int32_t __stdcall NCryptDecrypt(uintptr_t hKey, const uint8_t *pbInput, uint32_t cbInput, const BCRYPT_OAEP_PADDING_INFO *pPaddingInfo, _Out_ uint8_t *pbOutput, uint32_t cbOutput, _Out_ uint32_t *pcbResult, uint32_t dwFlags)'
57
+ ),
58
+ NCryptDeleteKey: ncrypt.func(
59
+ 'int32_t __stdcall NCryptDeleteKey(uintptr_t hKey, uint32_t dwFlags)'
60
+ ),
61
+ NCryptFreeObject: ncrypt.func(
62
+ 'int32_t __stdcall NCryptFreeObject(uintptr_t hObject)'
63
+ ),
64
+ };
65
+ }
66
+
67
+ type Api = ReturnType<typeof loadApi>;
68
+
69
+ class TpmError extends Error {
70
+ constructor(
71
+ what: string,
72
+ readonly status: number
73
+ ) {
74
+ super(`${what} failed with 0x${(status >>> 0).toString(16)}.`);
75
+ }
76
+ }
77
+
78
+ function check(what: string, status: number): void {
79
+ if (status !== 0) {
80
+ throw new TpmError(what, status);
81
+ }
82
+ }
83
+
84
+ function withProvider<T>(fn: (a: Api, provider: Handle) => T): T {
85
+ const a = (api ??= loadApi(loadKoffi()));
86
+ const provider: [Handle] = [0];
87
+ check(
88
+ 'NCryptOpenStorageProvider',
89
+ a.NCryptOpenStorageProvider(provider, PLATFORM_CRYPTO_PROVIDER, 0)
90
+ );
91
+ try {
92
+ return fn(a, provider[0]);
93
+ } finally {
94
+ a.NCryptFreeObject(provider[0]);
95
+ }
96
+ }
97
+
98
+ function openKey(a: Api, provider: Handle, name: string): Handle {
99
+ const key: [Handle] = [0];
100
+ check(
101
+ 'NCryptOpenKey',
102
+ a.NCryptOpenKey(provider, key, name, 0, NCRYPT_SILENT_FLAG)
103
+ );
104
+ return key[0];
105
+ }
106
+
107
+ /**
108
+ * Create an RSA key named `name` in this PC's TPM, which may only decrypt,
109
+ * and seal `secret` with it. Returns the sealed secret.
110
+ */
111
+ export function createTpmKey(name: string, secret: Buffer): Buffer {
112
+ return withProvider((a, provider) => {
113
+ const handle: [Handle] = [0];
114
+ check(
115
+ 'NCryptCreatePersistedKey',
116
+ a.NCryptCreatePersistedKey(
117
+ provider,
118
+ handle,
119
+ NCRYPT_RSA_ALGORITHM,
120
+ name,
121
+ 0,
122
+ 0
123
+ )
124
+ );
125
+ const key = handle[0];
126
+ let finalized = false;
127
+ try {
128
+ setDword(a, key, NCRYPT_LENGTH_PROPERTY, RSA_KEY_BITS);
129
+ setDword(a, key, NCRYPT_KEY_USAGE_PROPERTY, NCRYPT_ALLOW_DECRYPT_FLAG);
130
+ check('NCryptFinalizeKey', a.NCryptFinalizeKey(key, NCRYPT_SILENT_FLAG));
131
+ finalized = true;
132
+ const sealed = oaep(a, 'NCryptEncrypt', key, secret);
133
+ a.NCryptFreeObject(key);
134
+ return sealed;
135
+ } catch (error) {
136
+ if (finalized) {
137
+ // Also frees the handle.
138
+ a.NCryptDeleteKey(key, NCRYPT_SILENT_FLAG);
139
+ } else {
140
+ a.NCryptFreeObject(key);
141
+ }
142
+ throw error;
143
+ }
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Unseal what `createTpmKey` sealed, with the key named `name` in this PC's
149
+ * TPM. The TPM's private key never leaves it.
150
+ */
151
+ export function unsealWithTpmKey(name: string, sealed: Buffer): Buffer {
152
+ return withProvider((a, provider) => {
153
+ const key = openKey(a, provider, name);
154
+ try {
155
+ return oaep(a, 'NCryptDecrypt', key, sealed);
156
+ } finally {
157
+ a.NCryptFreeObject(key);
158
+ }
159
+ });
160
+ }
161
+
162
+ function setDword(a: Api, key: Handle, property: string, value: number) {
163
+ const bytes = Buffer.alloc(4);
164
+ bytes.writeUInt32LE(value);
165
+ check(
166
+ `NCryptSetProperty(${property})`,
167
+ a.NCryptSetProperty(key, property, bytes, bytes.length, 0)
168
+ );
169
+ }
170
+
171
+ // Encrypt or decrypt with RSA-OAEP and SHA-256.
172
+ function oaep(
173
+ a: Api,
174
+ what: 'NCryptEncrypt' | 'NCryptDecrypt',
175
+ key: Handle,
176
+ input: Buffer
177
+ ): Buffer {
178
+ const output = Buffer.alloc(RSA_KEY_BITS / 8);
179
+ const size: [number] = [0];
180
+ check(
181
+ what,
182
+ a[what](
183
+ key,
184
+ input,
185
+ input.length,
186
+ { pszAlgId: BCRYPT_SHA256_ALGORITHM, pbLabel: null, cbLabel: 0 },
187
+ output,
188
+ output.length,
189
+ size,
190
+ // Only decrypting uses the private key, which may want UI.
191
+ what === 'NCryptDecrypt'
192
+ ? NCRYPT_PAD_OAEP_FLAG | NCRYPT_SILENT_FLAG
193
+ : NCRYPT_PAD_OAEP_FLAG
194
+ )
195
+ );
196
+ const result = Buffer.from(new Uint8Array(output.subarray(0, size[0])));
197
+ // Don't leave an unsealed secret behind.
198
+ output.fill(0);
199
+ return result;
200
+ }
201
+
202
+ /** Delete the key named `name` from this PC's TPM, if it's there. */
203
+ export function deleteTpmKey(name: string): void {
204
+ withProvider((a, provider) => {
205
+ let key: Handle;
206
+ try {
207
+ key = openKey(a, provider, name);
208
+ } catch (error) {
209
+ if (error instanceof TpmError && error.status >>> 0 === NTE_BAD_KEYSET) {
210
+ return;
211
+ }
212
+ throw error;
213
+ }
214
+ // Frees the handle when it succeeds.
215
+ const status = a.NCryptDeleteKey(key, NCRYPT_SILENT_FLAG);
216
+ if (status !== 0) {
217
+ a.NCryptFreeObject(key);
218
+ }
219
+ check('NCryptDeleteKey', status);
220
+ });
221
+ }