GeezerVison is showing github.com — crypto: add subtle.getPublicKey() utility function in Web Cryptography · nodejs/node@6fcce90
File treeExpand file treeCollapse file tree Open diff view settings Expand file treeCollapse file tree Open diff view settings Collapse file Copy file name to clipboardExpand all lines: doc/api/webcrypto.md +47-31Lines changed: 47 additions & 31 deletions - Display the source diff
- Display the rich diff
| Original file line number | Diff line number | Diff line change |
|---|
@@ -114,6 +114,7 @@ Key Formats: | 114 | 114 |
| 115 | 115 | Methods: | 116 | 116 |
| | 117 | +* [`subtle.getPublicKey()`][] | 117 | 118 | * [`SubtleCrypto.supports()`][] | 118 | 119 |
| 119 | 120 | ## Secure Curves in the Web Cryptography API | @@ -477,36 +478,36 @@ const decrypted = new TextDecoder().decode(await crypto.subtle.decrypt( | 477 | 478 | The table details the algorithms supported by the Node.js Web Crypto API | 478 | 479 | implementation and the APIs supported for each: | 479 | 480 |
| 480 | | -| Algorithm | `generateKey` | `exportKey` | `importKey` | `encrypt` | `decrypt` | `wrapKey` | `unwrapKey` | `deriveBits` | `deriveKey` | `sign` | `verify` | `digest` | | 481 | | -| ---------------------------- | ------------- | ----------- | ----------- | --------- | --------- | --------- | ----------- | ------------ | ----------- | ------ | -------- | -------- | | 482 | | -| `'AES-CBC'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | | 483 | | -| `'AES-CTR'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | | 484 | | -| `'AES-GCM'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | | 485 | | -| `'AES-KW'` | ✔ | ✔ | ✔ | | | ✔ | ✔ | | | | | | | 486 | | -| `'cSHAKE128'`[^modern-algos] | | | | | | | | | | | | ✔ | | 487 | | -| `'cSHAKE256'`[^modern-algos] | | | | | | | | | | | | ✔ | | 488 | | -| `'ECDH'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | | 489 | | -| `'ECDSA'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | 490 | | -| `'Ed25519'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | 491 | | -| `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | 492 | | -| `'HKDF'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | | | 493 | | -| `'HMAC'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | 494 | | -| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | 495 | | -| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | 496 | | -| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | 497 | | -| `'PBKDF2'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | | | 498 | | -| `'RSA-OAEP'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | | 499 | | -| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | 500 | | -| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | 501 | | -| `'SHA-1'` | | | | | | | | | | | | ✔ | | 502 | | -| `'SHA-256'` | | | | | | | | | | | | ✔ | | 503 | | -| `'SHA-384'` | | | | | | | | | | | | ✔ | | 504 | | -| `'SHA-512'` | | | | | | | | | | | | ✔ | | 505 | | -| `'SHA3-256'`[^modern-algos] | | | | | | | | | | | | ✔ | | 506 | | -| `'SHA3-384'`[^modern-algos] | | | | | | | | | | | | ✔ | | 507 | | -| `'SHA3-512'`[^modern-algos] | | | | | | | | | | | | ✔ | | 508 | | -| `'X25519'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | | 509 | | -| `'X448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | | | 481 | +| Algorithm | `generateKey` | `exportKey` | `importKey` | `encrypt` | `decrypt` | `wrapKey` | `unwrapKey` | `deriveBits` | `deriveKey` | `sign` | `verify` | `digest` | `getPublicKey` | | | 482 | +| ---------------------------- | ------------- | ----------- | ----------- | --------- | --------- | --------- | ----------- | ------------ | ----------- | ------ | -------- | -------- | -------------- | | | 483 | +| `'AES-CBC'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | | | | 484 | +| `'AES-CTR'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | | | | 485 | +| `'AES-GCM'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | | | | 486 | +| `'AES-KW'` | ✔ | ✔ | ✔ | | | ✔ | ✔ | | | | | | | | | 487 | +| `'cSHAKE128'`[^modern-algos] | | | | | | | | | | | | ✔ | | | | 488 | +| `'cSHAKE256'`[^modern-algos] | | | | | | | | | | | | ✔ | | | | 489 | +| `'ECDH'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | ✔ | | | 490 | +| `'ECDSA'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | | | 491 | +| `'Ed25519'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | | | 492 | +| `'Ed448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | | | 493 | +| `'HKDF'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | | | | | 494 | +| `'HMAC'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | | | | 495 | +| `'ML-DSA-44'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | | | 496 | +| `'ML-DSA-65'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | | | 497 | +| `'ML-DSA-87'`[^modern-algos] | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | | | 498 | +| `'PBKDF2'` | | ✔ | ✔ | | | | | ✔ | ✔ | | | | | | | 499 | +| `'RSA-OAEP'` | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | | | | | | ✔ | | | 500 | +| `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | | | 501 | +| `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | | | | ✔ | ✔ | | ✔ | | | 502 | +| `'SHA-1'` | | | | | | | | | | | | ✔ | | | | 503 | +| `'SHA-256'` | | | | | | | | | | | | ✔ | | | | 504 | +| `'SHA-384'` | | | | | | | | | | | | ✔ | | | | 505 | +| `'SHA-512'` | | | | | | | | | | | | ✔ | | | | 506 | +| `'SHA3-256'`[^modern-algos] | | | | | | | | | | | | ✔ | | | | 507 | +| `'SHA3-384'`[^modern-algos] | | | | | | | | | | | | ✔ | | | | 508 | +| `'SHA3-512'`[^modern-algos] | | | | | | | | | | | | ✔ | | | | 509 | +| `'X25519'` | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | ✔ | | | 510 | +| `'X448'`[^secure-curves] | ✔ | ✔ | ✔ | | | | | ✔ | ✔ | | | | ✔ | | 510 | 511 |
| 511 | 512 | ## Class: `Crypto` | 512 | 513 |
| @@ -691,7 +692,7 @@ added: REPLACEME | 691 | 692 |
| 692 | 693 | <!--lint disable maximum-line-length remark-lint--> | 693 | 694 |
| 694 | | -* `operation` {string} "encrypt", "decrypt", "sign", "verify", "digest", "generateKey", "deriveKey", "deriveBits", "importKey", "exportKey", "wrapKey", or "unwrapKey" | | 695 | +* `operation` {string} "encrypt", "decrypt", "sign", "verify", "digest", "generateKey", "deriveKey", "deriveBits", "importKey", "exportKey", "getPublicKey", "wrapKey", or "unwrapKey" | 695 | 696 | * `algorithm` {string|Algorithm} | 696 | 697 | * `lengthOrAdditionalAlgorithm` {null|number|string|Algorithm|undefined} Depending on the operation this is either ignored, the value of the length argument when operation is "deriveBits", the algorithm of key to be derived when operation is "deriveKey", the algorithm of key to be exported before wrapping when operation is "wrapKey", or the algorithm of key to be imported after unwrapping when operation is "unwrapKey". **Default:** `null` when operation is "deriveBits", `undefined` otherwise. | 697 | 698 | * Returns: {boolean} Indicating whether the implementation supports the given operation | @@ -925,6 +926,20 @@ specification. | 925 | 926 | | `'RSA-PSS'` | ✔ | ✔ | ✔ | | | | | | 926 | 927 | | `'RSASSA-PKCS1-v1_5'` | ✔ | ✔ | ✔ | | | | | | 927 | 928 |
| | 929 | +### `subtle.getPublicKey(key, keyUsages)` | | 930 | +
| | 931 | +<!-- YAML | | 932 | +added: REPLACEME | | 933 | +--> | | 934 | +
| | 935 | +> Stability: 1.1 - Active development | | 936 | +
| | 937 | +* `key` {CryptoKey} A private key from which to derive the corresponding public key. | | 938 | +* `keyUsages` {string\[]} See [Key usages][]. | | 939 | +* Returns: {Promise} Fulfills with a {CryptoKey} upon success. | | 940 | +
| | 941 | +Derives the public key from a given private key. | | 942 | +
| 928 | 943 | ### `subtle.generateKey(algorithm, extractable, keyUsages)` | 929 | 944 |
| 930 | 945 | <!-- YAML | @@ -2142,3 +2157,4 @@ The length (in bytes) of the random salt to use. | 2142 | 2157 | [Secure Curves in the Web Cryptography API]: #secure-curves-in-the-web-cryptography-api | 2143 | 2158 | [Web Crypto API]: https://www.w3.org/TR/WebCryptoAPI/ | 2144 | 2159 | [`SubtleCrypto.supports()`]: #static-method-subtlecryptosupportsoperation-algorithm-lengthoradditionalalgorithm | | 2160 | +[`subtle.getPublicKey()`]: #subtlegetpublickeykey-keyusages |
Collapse file Copy file name to clipboardExpand all lines: lib/internal/crypto/webcrypto.js +55Lines changed: 55 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| 8 | 8 | ReflectApply, | 9 | 9 | ReflectConstruct, | 10 | 10 | StringPrototypeRepeat, | | 11 | + StringPrototypeSlice, | 11 | 12 | SymbolToStringTag, | 12 | 13 | } = primordials; | 13 | 14 |
| @@ -29,6 +30,7 @@ const { | 29 | 30 | } = require('internal/errors'); | 30 | 31 |
| 31 | 32 | const { | | 33 | + createPublicKey, | 32 | 34 | CryptoKey, | 33 | 35 | importGenericSecretKey, | 34 | 36 | } = require('internal/crypto/keys'); | @@ -1028,6 +1030,31 @@ async function decrypt(algorithm, key, data) { | 1028 | 1030 | return cipherOrWrap(kWebCryptoCipherDecrypt, algorithm, key, data, 'decrypt'); | 1029 | 1031 | } | 1030 | 1032 |
| | 1033 | +// Implements https://wicg.github.io/webcrypto-modern-algos/#SubtleCrypto-method-getPublicKey | | 1034 | +async function getPublicKey(key, keyUsages) { | | 1035 | + emitExperimentalWarning('The getPublicKey Web Crypto API method'); | | 1036 | + if (this !== subtle) throw new ERR_INVALID_THIS('SubtleCrypto'); | | 1037 | + | | 1038 | + webidl ??= require('internal/crypto/webidl'); | | 1039 | + const prefix = "Failed to execute 'getPublicKey' on 'SubtleCrypto'"; | | 1040 | + webidl.requiredArguments(arguments.length, 2, { prefix }); | | 1041 | + key = webidl.converters.CryptoKey(key, { | | 1042 | + prefix, | | 1043 | + context: '1st argument', | | 1044 | + }); | | 1045 | + keyUsages = webidl.converters['sequence<KeyUsage>'](keyUsages, { | | 1046 | + prefix, | | 1047 | + context: '2nd argument', | | 1048 | + }); | | 1049 | + | | 1050 | + if (key.type !== 'private') | | 1051 | + throw lazyDOMException('key must be a private key', 'InvalidAccessError'); | | 1052 | + | | 1053 | + const keyObject = createPublicKey(key[kKeyObject]); | | 1054 | + | | 1055 | + return keyObject.toCryptoKey(key.algorithm, true, keyUsages); | | 1056 | +} | | 1057 | + | 1031 | 1058 | // The SubtleCrypto and Crypto classes are defined as part of the | 1032 | 1059 | // Web Crypto API standard: https://www.w3.org/TR/WebCryptoAPI/ | 1033 | 1060 |
| @@ -1066,6 +1093,7 @@ class SubtleCrypto { | 1066 | 1093 | case 'exportKey': | 1067 | 1094 | case 'wrapKey': | 1068 | 1095 | case 'unwrapKey': | | 1096 | + case 'getPublicKey': | 1069 | 1097 | break; | 1070 | 1098 | default: | 1071 | 1099 | return false; | @@ -1116,6 +1144,26 @@ class SubtleCrypto { | 1116 | 1144 | context: '3rd argument', | 1117 | 1145 | }); | 1118 | 1146 | } | | 1147 | + } else if (operation === 'getPublicKey') { | | 1148 | + let normalizedAlgorithm; | | 1149 | + try { | | 1150 | + normalizedAlgorithm = normalizeAlgorithm(algorithm, 'exportKey'); | | 1151 | + } catch { | | 1152 | + return false; | | 1153 | + } | | 1154 | + | | 1155 | + switch (StringPrototypeSlice(normalizedAlgorithm.name, 0, 2)) { | | 1156 | + case 'ML': // ML-DSA-*, ML-KEM-* | | 1157 | + case 'SL': // SLH-DSA-* | | 1158 | + case 'RS': // RSA-OAEP, RSA-PSS, RSASSA-PKCS1-v1_5 | | 1159 | + case 'EC': // ECDSA, ECDH | | 1160 | + case 'Ed': // Ed* | | 1161 | + case 'X2': // X25519 | | 1162 | + case 'X4': // X448 | | 1163 | + return true; | | 1164 | + default: | | 1165 | + return false; | | 1166 | + } | 1119 | 1167 | } | 1120 | 1168 |
| 1121 | 1169 | return check(operation, algorithm, length); | @@ -1319,6 +1367,13 @@ ObjectDefineProperties( | 1319 | 1367 | writable: true, | 1320 | 1368 | value: unwrapKey, | 1321 | 1369 | }, | | 1370 | + getPublicKey: { | | 1371 | + __proto__: null, | | 1372 | + enumerable: true, | | 1373 | + configurable: true, | | 1374 | + writable: true, | | 1375 | + value: getPublicKey, | | 1376 | + }, | 1322 | 1377 | }); | 1323 | 1378 |
| 1324 | 1379 | module.exports = { | |
Collapse file Copy file name to clipboardExpand all lines: test/fixtures/webcrypto/supports-modern-algorithms.mjs +16Lines changed: 16 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -23,4 +23,20 @@ export const vectors = { | 23 | 23 | [pqc, 'ML-DSA-65'], | 24 | 24 | [pqc, 'ML-DSA-87'], | 25 | 25 | ], | | 26 | + 'getPublicKey': [ | | 27 | + [true, 'RSA-OAEP'], | | 28 | + [true, 'RSA-PSS'], | | 29 | + [true, 'RSASSA-PKCS1-v1_5'], | | 30 | + [true, 'X25519'], | | 31 | + [true, 'X448'], | | 32 | + [true, 'Ed25519'], | | 33 | + [true, 'Ed448'], | | 34 | + [true, 'ECDH'], | | 35 | + [true, 'ECDSA'], | | 36 | + [pqc, 'ML-DSA-44'], | | 37 | + [false, 'AES-CTR'], | | 38 | + [false, 'AES-CBC'], | | 39 | + [false, 'AES-GCM'], | | 40 | + [false, 'AES-KW'], | | 41 | + ], | 26 | 42 | }; |
Collapse file Copy file name to clipboardExpand all lines: test/parallel/test-webcrypto-constructors.js +7Lines changed: 7 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
@@ -144,6 +144,13 @@ const notSubtle = Reflect.construct(function() {}, [], SubtleCrypto); | 144 | 144 | }); | 145 | 145 | } | 146 | 146 |
| | 147 | +// Test SubtleCrypto.prototype.getPublicKey | | 148 | +{ | | 149 | + assert.rejects(() => notSubtle.getPublicKey(), { | | 150 | + name: 'TypeError', code: 'ERR_INVALID_THIS', | | 151 | + }).then(common.mustCall()); | | 152 | +} | | 153 | + | 147 | 154 | { | 148 | 155 | subtle.importKey( | 149 | 156 | 'raw', | |
Collapse file Copy file name to clipboard +51Lines changed: 51 additions & 0 deletions | Original file line number | Diff line number | Diff line change |
|---|
| | 1 | +import * as common from '../common/index.mjs'; | | 2 | + | | 3 | +if (!common.hasCrypto) common.skip('missing crypto'); | | 4 | + | | 5 | +import * as assert from 'node:assert'; | | 6 | +const { subtle } = globalThis.crypto; | | 7 | + | | 8 | +const RSA_KEY_GEN = { | | 9 | + modulusLength: 2048, | | 10 | + publicExponent: new Uint8Array([1, 0, 1]), | | 11 | + hash: 'SHA-256', | | 12 | +}; | | 13 | + | | 14 | +const publicUsages = { | | 15 | + 'ECDH': [], | | 16 | + 'ECDSA': ['verify'], | | 17 | + 'Ed25519': ['verify'], | | 18 | + 'RSA-OAEP': ['encrypt', 'wrapKey'], | | 19 | + 'RSA-PSS': ['verify'], | | 20 | + 'RSASSA-PKCS1-v1_5': ['verify'], | | 21 | + 'X25519': [], | | 22 | +}; | | 23 | + | | 24 | +for await (const { privateKey } of [ | | 25 | + subtle.generateKey({ name: 'ECDH', namedCurve: 'P-256' }, false, ['deriveBits']), | | 26 | + subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, false, ['sign']), | | 27 | + subtle.generateKey('Ed25519', false, ['sign']), | | 28 | + subtle.generateKey({ name: 'RSA-OAEP', ...RSA_KEY_GEN }, false, ['decrypt', 'unwrapKey']), | | 29 | + subtle.generateKey({ name: 'RSA-PSS', ...RSA_KEY_GEN }, false, ['sign']), | | 30 | + subtle.generateKey({ name: 'RSASSA-PKCS1-v1_5', ...RSA_KEY_GEN }, false, ['sign']), | | 31 | + subtle.generateKey('X25519', false, ['deriveBits']), | | 32 | +]) { | | 33 | + const { name } = privateKey.algorithm; | | 34 | + const usages = publicUsages[name]; | | 35 | + const publicKey = await subtle.getPublicKey(privateKey, usages); | | 36 | + assert.deepStrictEqual(publicKey.algorithm, privateKey.algorithm); | | 37 | + assert.strictEqual(publicKey.type, 'public'); | | 38 | + assert.strictEqual(publicKey.extractable, true); | | 39 | + | | 40 | + await assert.rejects(() => subtle.getPublicKey(privateKey, ['deriveBits']), { | | 41 | + name: 'SyntaxError', | | 42 | + message: /Unsupported key usage/ | | 43 | + }); | | 44 | +} | | 45 | + | | 46 | +const secretKey = await subtle.generateKey( | | 47 | + { name: 'AES-CBC', length: 128 }, true, ['encrypt', 'decrypt']); | | 48 | +await assert.rejects(() => subtle.getPublicKey(secretKey, ['encrypt', 'decrypt']), { | | 49 | + name: 'InvalidAccessError', | | 50 | + message: 'key must be a private key' | | 51 | +}); |
|