Skip to content
File

Blob: src/node/internal/crypto_dh.ts

typescript488 lines
1// Copyright (c) 2017-2022 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4//
5// Copyright Joyent, Inc. and other Node contributors.
6//
7// Permission is hereby granted, free of charge, to any person obtaining a
8// copy of this software and associated documentation files (the
9// "Software"), to deal in the Software without restriction, including
10// without limitation the rights to use, copy, modify, merge, publish,
11// distribute, sublicense, and/or sell copies of the Software, and to permit
12// persons to whom the Software is furnished to do so, subject to the
13// following conditions:
14//
15// The above copyright notice and this permission notice shall be included
16// in all copies or substantial portions of the Software.
17//
18// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
19// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
20// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
21// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
22// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
23// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
24// USE OR OTHER DEALINGS IN THE SOFTWARE.
25 
26import { Buffer } from 'node-internal:internal_buffer';
27 
28import { default as cryptoImpl, type ECDHFormat } from 'node-internal:crypto';
29type ArrayLike = cryptoImpl.ArrayLike;
30 
31import {
32 isKeyObject,
33 getKeyObjectHandle,
34 type PrivateKeyObject,
35 type PublicKeyObject,
36} from 'node-internal:crypto_keys';
37 
38import {
39 ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY,
40 ERR_INVALID_ARG_TYPE,
41 ERR_INVALID_ARG_VALUE,
42} from 'node-internal:internal_errors';
43 
44import {
45 validateInt32,
46 validateObject,
47 validateOneOf,
48 validateString,
49} from 'node-internal:validators';
50 
51import {
52 isArrayBufferView,
53 isAnyArrayBuffer,
54} from 'node-internal:internal_types';
55 
56import {
57 getArrayBufferOrView,
58 toBuf,
59 kHandle,
60} from 'node-internal:crypto_util';
61 
62const DH_GENERATOR = 2;
63 
64declare class SharedDiffieHellman {
65 generateKeys: typeof dhGenerateKeys;
66 computeSecret: typeof dhComputeSecret;
67 getPrime: typeof dhGetPrime;
68 getGenerator: typeof dhGetGenerator;
69 getPublicKey: typeof dhGetPublicKey;
70 getPrivateKey: typeof dhGetPrivateKey;
71 setPrivateKey: typeof dhSetPrivateKey;
72 setPublicKey: typeof dhSetPublicKey;
73}
74 
75declare class DiffieHellman extends SharedDiffieHellman {
76 [kHandle]: cryptoImpl.DiffieHellmanHandle;
77 
78 constructor(
79 sizeOrKey: number | ArrayLike,
80 keyEncoding?: number | string,
81 generator?: number | ArrayLike,
82 genEncoding?: string
83 );
84}
85 
86function DiffieHellman(
87 this: unknown,
88 sizeOrKey: number | ArrayLike,
89 keyEncoding?: number | string,
90 generator?: number | ArrayLike,
91 genEncoding?: string
92): DiffieHellman {
93 if (!(this instanceof DiffieHellman)) {
94 return new DiffieHellman(sizeOrKey, keyEncoding, generator, genEncoding);
95 }
96 if (
97 typeof sizeOrKey !== 'number' &&
98 typeof sizeOrKey !== 'string' &&
99 !isArrayBufferView(sizeOrKey) &&
100 !isAnyArrayBuffer(sizeOrKey)
101 ) {
102 throw new ERR_INVALID_ARG_TYPE(
103 'sizeOrKey',
104 ['number', 'string', 'ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'],
105 sizeOrKey
106 );
107 }
108 
109 // Sizes < 0 don't make sense but they _are_ accepted (and subsequently
110 // rejected with ERR_OSSL_BN_BITS_TOO_SMALL) by OpenSSL. The glue code
111 // in node_crypto.cc accepts values that are IsInt32() for that reason
112 // and that's why we do that here too.
113 if (typeof sizeOrKey === 'number') validateInt32(sizeOrKey, 'sizeOrKey');
114 
115 if (
116 keyEncoding &&
117 keyEncoding !== 'buffer' &&
118 !Buffer.isEncoding(keyEncoding)
119 ) {
120 genEncoding = generator as string;
121 generator = keyEncoding;
122 keyEncoding = 'utf-8'; // default encoding
123 }
124 
125 keyEncoding ??= 'utf-8';
126 genEncoding ??= 'utf-8';
127 
128 if (typeof sizeOrKey !== 'number')
129 sizeOrKey = toBuf(sizeOrKey, keyEncoding as string);
130 
131 if (!generator) {
132 generator = DH_GENERATOR;
133 } else if (typeof generator === 'number') {
134 validateInt32(generator, 'generator');
135 } else if (typeof generator === 'string') {
136 generator = toBuf(generator, genEncoding);
137 } else if (!isArrayBufferView(generator) && !isAnyArrayBuffer(generator)) {
138 throw new ERR_INVALID_ARG_TYPE(
139 'generator',
140 ['number', 'string', 'ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'],
141 generator
142 );
143 }
144 
145 this[kHandle] = new cryptoImpl.DiffieHellmanHandle(sizeOrKey, generator);
146 Object.defineProperty(DiffieHellman.prototype, 'verifyError', {
147 get: function (this: DiffieHellman) {
148 return this[kHandle].getVerifyError();
149 },
150 configurable: true,
151 enumerable: true,
152 });
153 return this;
154}
155 
156declare class DiffieHellmanGroup extends SharedDiffieHellman {
157 [kHandle]: cryptoImpl.DiffieHellmanHandle;
158 constructor(name: string);
159}
160 
161function DiffieHellmanGroup(this: unknown, name: string): DiffieHellmanGroup {
162 if (!(this instanceof DiffieHellmanGroup)) {
163 return new DiffieHellmanGroup(name);
164 }
165 
166 // The C++-based handle is shared between both classes, so DiffieHellmanGroupHandle() is merely
167 // a different constructor for a DiffieHellmanHandle.
168 this[kHandle] = cryptoImpl.DiffieHellmanGroupHandle(name);
169 Object.defineProperty(DiffieHellmanGroup.prototype, 'verifyError', {
170 get: function (this: DiffieHellmanGroup): number {
171 return this[kHandle].getVerifyError();
172 },
173 configurable: true,
174 enumerable: true,
175 });
176 return this;
177}
178 
179DiffieHellmanGroup.prototype.generateKeys =
180 DiffieHellman.prototype.generateKeys = dhGenerateKeys;
181DiffieHellmanGroup.prototype.computeSecret =
182 DiffieHellman.prototype.computeSecret = dhComputeSecret;
183DiffieHellmanGroup.prototype.getPrime = DiffieHellman.prototype.getPrime =
184 dhGetPrime;
185DiffieHellmanGroup.prototype.getGenerator =
186 DiffieHellman.prototype.getGenerator = dhGetGenerator;
187DiffieHellmanGroup.prototype.getPublicKey =
188 DiffieHellman.prototype.getPublicKey = dhGetPublicKey;
189DiffieHellmanGroup.prototype.getPrivateKey =
190 DiffieHellman.prototype.getPrivateKey = dhGetPrivateKey;
191DiffieHellman.prototype.setPublicKey = dhSetPublicKey;
192DiffieHellman.prototype.setPrivateKey = dhSetPrivateKey;
193 
194export { DiffieHellman, DiffieHellmanGroup };
195 
196type DHLike = DiffieHellman | DiffieHellmanGroup;
197function dhGenerateKeys(this: DHLike, encoding?: string): Buffer | string {
198 const keys = this[kHandle].generateKeys();
199 return encode(keys, encoding);
200}
201 
202function dhComputeSecret(
203 this: DHLike,
204 key: ArrayLike,
205 inEnc?: string,
206 outEnc?: string
207): Buffer | string {
208 key = getArrayBufferOrView(key, 'key', inEnc);
209 const ret = this[kHandle].computeSecret(key);
210 if (typeof ret === 'string') throw new ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY();
211 return encode(ret, outEnc);
212}
213 
214function dhGetPrime(this: DHLike, encoding?: string): Buffer | string {
215 const prime = this[kHandle].getPrime();
216 return encode(prime, encoding);
217}
218 
219function dhGetGenerator(this: DHLike, encoding?: string): Buffer | string {
220 const generator = this[kHandle].getGenerator();
221 return encode(generator, encoding);
222}
223 
224function dhGetPublicKey(this: DHLike, encoding?: string): Buffer | string {
225 const key = this[kHandle].getPublicKey();
226 return encode(key, encoding);
227}
228 
229function dhGetPrivateKey(this: DHLike, encoding?: string): Buffer | string {
230 const key = this[kHandle].getPrivateKey();
231 return encode(key, encoding);
232}
233 
234function dhSetPublicKey(
235 this: DiffieHellman,
236 key: ArrayLike,
237 encoding?: string
238): DiffieHellman {
239 key = getArrayBufferOrView(key, 'key', encoding);
240 this[kHandle].setPublicKey(key);
241 return this;
242}
243 
244function dhSetPrivateKey(
245 this: DiffieHellman,
246 key: ArrayLike,
247 encoding?: string
248): DiffieHellman {
249 key = getArrayBufferOrView(key, 'key', encoding);
250 this[kHandle].setPrivateKey(key);
251 return this;
252}
253 
254function encode(buffer: ArrayBuffer, encoding?: string): Buffer | string {
255 if (encoding && encoding !== 'buffer')
256 return Buffer.from(buffer).toString(encoding);
257 return Buffer.from(buffer);
258}
259 
260export function createDiffieHellman(
261 sizeOrKey: number | ArrayLike,
262 keyEncoding?: number | string,
263 generator?: number | ArrayLike,
264 genEncoding?: string
265): DiffieHellman {
266 return new DiffieHellman(sizeOrKey, keyEncoding, generator, genEncoding);
267}
268 
269export function createDiffieHellmanGroup(name: string): DiffieHellmanGroup {
270 return new DiffieHellmanGroup(name);
271}
272 
273export function getDiffieHellman(name: string): DiffieHellmanGroup {
274 return createDiffieHellmanGroup(name);
275}
276 
277export interface DiffieHellmanKeyPair {
278 publicKey: PublicKeyObject;
279 privateKey: PrivateKeyObject;
280}
281 
282export function diffieHellman(options: DiffieHellmanKeyPair): Buffer {
283 validateObject(options, 'options');
284 const { publicKey, privateKey } = options;
285 if (!isKeyObject(publicKey)) {
286 throw new ERR_INVALID_ARG_TYPE('options.publicKey', 'KeyObject', publicKey);
287 }
288 if (!isKeyObject(privateKey)) {
289 throw new ERR_INVALID_ARG_TYPE(
290 'options.privateKey',
291 'KeyObject',
292 privateKey
293 );
294 }
295 if (publicKey.type !== 'public') {
296 throw new ERR_INVALID_ARG_TYPE(
297 'options.publicKey',
298 'public key',
299 publicKey
300 );
301 }
302 if (privateKey.type !== 'private') {
303 throw new ERR_INVALID_ARG_TYPE(
304 'options.privateKey',
305 'private key',
306 privateKey
307 );
308 }
309 if (publicKey.asymmetricKeyType !== privateKey.asymmetricKeyType) {
310 throw new ERR_INVALID_ARG_VALUE(
311 'options.publicKey.asymmetricKeyType',
312 publicKey.asymmetricKeyType,
313 'must equal privateKey.asymmetricKeyType'
314 );
315 }
316 const res = cryptoImpl.statelessDH(
317 getKeyObjectHandle(privateKey),
318 getKeyObjectHandle(publicKey)
319 );
320 return Buffer.from(res);
321}
322 
323// =============================================================================
324 
325export interface ECDH {
326 [kHandle]: cryptoImpl.ECDHHandle;
327 computeSecret(
328 otherPublicKey: string | ArrayBufferView | ArrayBuffer,
329 inputEncoding?: string,
330 outputEncoding?: string
331 ): Buffer | string;
332 generateKeys(encoding?: string, format?: string): Buffer | string;
333 getPrivateKey(encoding?: string): Buffer | string;
334 getPublicKey(encoding?: string, format?: string): Buffer | string;
335 setPrivateKey(
336 key: string | ArrayBufferView | ArrayBuffer,
337 encoding?: string
338 ): void;
339}
340 
341export const ECDH = function (this: ECDH, curveName: string) {
342 if (!(this instanceof ECDH)) {
343 return new ECDH(curveName);
344 }
345 validateString(curveName, 'curveName');
346 this[kHandle] = new cryptoImpl.ECDHHandle(curveName);
347 return this;
348} as unknown as {
349 new (curveName: string): ECDH;
350};
351 
352// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
353ECDH.prototype.computeSecret = function (
354 this: ECDH,
355 otherPublicKey: string | ArrayBufferView | ArrayBuffer,
356 inputEncoding?: string,
357 outputEncoding?: string
358): Buffer | string {
359 if (typeof otherPublicKey === 'string') {
360 otherPublicKey = Buffer.from(otherPublicKey, inputEncoding);
361 }
362 if (!isArrayBufferView(otherPublicKey)) {
363 throw new ERR_INVALID_ARG_TYPE(
364 'otherPublicKey',
365 ['string', 'Buffer', 'TypedArray', 'DataView'],
366 otherPublicKey
367 );
368 }
369 if (inputEncoding != null) {
370 validateString(inputEncoding, 'inputEncoding');
371 }
372 if (outputEncoding != null) {
373 validateString(outputEncoding, 'outputEncoding');
374 }
375 const ret = this[kHandle].computeSecret(otherPublicKey);
376 if (typeof outputEncoding === 'string') {
377 return Buffer.from(ret).toString(outputEncoding);
378 }
379 return Buffer.from(ret);
380};
381 
382// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
383ECDH.prototype.generateKeys = function (
384 this: ECDH,
385 encoding?: string,
386 format?: string
387): Buffer | string {
388 if (encoding != null) {
389 validateString(encoding, 'encoding');
390 }
391 if (format != null) {
392 validateOneOf(format, 'format', ['compressed', 'uncompressed', 'hybrid']);
393 }
394 this[kHandle].generateKeys();
395 return this.getPublicKey(encoding, format);
396};
397 
398// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
399ECDH.prototype.getPrivateKey = function (
400 this: ECDH,
401 encoding?: string
402): Buffer | string {
403 if (encoding != null) {
404 validateString(encoding, 'encoding');
405 }
406 const pvt = this[kHandle].getPrivateKey();
407 if (typeof encoding === 'string') {
408 return Buffer.from(pvt).toString(encoding);
409 }
410 return Buffer.from(pvt);
411};
412 
413// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
414ECDH.prototype.getPublicKey = function (
415 this: ECDH,
416 encoding?: string,
417 format: ECDHFormat = 'uncompressed'
418): Buffer | string {
419 if (encoding != null) {
420 validateString(encoding, 'encoding');
421 }
422 validateOneOf(format, 'format', ['compressed', 'uncompressed', 'hybrid']);
423 const pub = this[kHandle].getPublicKey(format);
424 if (typeof encoding === 'string') {
425 return Buffer.from(pub).toString(encoding);
426 }
427 return Buffer.from(pub);
428};
429 
430// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
431ECDH.prototype.setPrivateKey = function (
432 this: ECDH,
433 key: string | ArrayBufferView | ArrayBuffer,
434 encoding?: string
435): void {
436 if (encoding != null) {
437 validateString(encoding, 'encoding');
438 }
439 if (typeof key === 'string') {
440 key = Buffer.from(key, encoding);
441 }
442 if (!isArrayBufferView(key)) {
443 throw new ERR_INVALID_ARG_TYPE(
444 'key',
445 ['string', 'Buffer', 'TypedArray', 'DataView'],
446 key
447 );
448 }
449 this[kHandle].setPrivateKey(key);
450};
451 
452// eslint-disable-next-line @typescript-eslint/no-explicit-any,@typescript-eslint/no-unsafe-member-access
453(ECDH as any).convertKey = function (
454 key: string | ArrayBufferView | ArrayBuffer,
455 curve: string,
456 inputEncoding?: string,
457 outputEncoding?: string,
458 format: ECDHFormat = 'uncompressed'
459): Buffer | string {
460 if (typeof key === 'string') {
461 key = Buffer.from(key, inputEncoding);
462 }
463 if (!isArrayBufferView(key)) {
464 throw new ERR_INVALID_ARG_TYPE(
465 'key',
466 ['string', 'Buffer', 'TypedArray', 'DataView'],
467 key
468 );
469 }
470 validateString(curve, 'curve');
471 if (inputEncoding != null) {
472 validateString(inputEncoding, 'inputEncoding');
473 }
474 if (outputEncoding != null) {
475 validateString(outputEncoding, 'outputEncoding');
476 }
477 validateOneOf(format, 'format', ['compressed', 'uncompressed', 'hybrid']);
478 const ret = cryptoImpl.ECDHHandle.convertKey(key, curve, format);
479 if (typeof outputEncoding === 'string') {
480 return Buffer.from(ret).toString(outputEncoding);
481 }
482 return Buffer.from(ret);
483};
484 
485export function createECDH(curveName: string): ECDH {
486 return new ECDH(curveName);
487}