Skip to content

Commit 27e530b

Browse files
authored
fix: HashAlgorithm shape (#571)
1 parent 5a9943a commit 27e530b

File tree

11 files changed

+333
-274
lines changed

11 files changed

+333
-274
lines changed

.github/dependabot.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ version: 2
22

33
updates:
44
- package-ecosystem: "npm"
5+
target-branch: "0.x"
56
directory: "/"
67
schedule:
7-
interval: "daily"
8+
interval: "weekly"

.github/workflows/validate-js.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,9 @@ jobs:
6565
env:
6666
REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6767

68+
- name: Test JS
69+
run: bun test
70+
6871
lint_js:
6972
name: JS Lint (eslint, prettier)
7073
runs-on: ubuntu-latest

example/src/testing/tests/webcryptoTests/sign_verify.ts

Lines changed: 28 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { describe, it } from '../../MochaRNAdapter';
44
import { expect } from 'chai';
55

66
const { subtle } = crypto;
7+
const encoder = new TextEncoder();
78

89
describe('subtle - sign / verify', () => {
910
// // Test Sign/Verify RSASSA-PKCS1-v1_5
@@ -57,35 +58,25 @@ describe('subtle - sign / verify', () => {
5758
// Test Sign/Verify ECDSA
5859
{
5960
async function test(data: string) {
60-
const ec = new TextEncoder();
6161
const pair = await subtle.generateKey(
62-
{
63-
name: 'ECDSA',
64-
namedCurve: 'P-384',
65-
},
62+
{ name: 'ECDSA', namedCurve: 'P-384' },
6663
true,
6764
['sign', 'verify'],
6865
);
6966
const { publicKey, privateKey } = pair as CryptoKeyPair;
7067

7168
const signature = await subtle.sign(
72-
{
73-
name: 'ECDSA',
74-
hash: 'SHA-384',
75-
},
69+
{ name: 'ECDSA', hash: 'SHA-384' },
7670
privateKey as CryptoKey,
77-
ec.encode(data),
71+
encoder.encode(data),
7872
);
7973

8074
expect(
8175
await subtle.verify(
82-
{
83-
name: 'ECDSA',
84-
hash: 'SHA-384',
85-
},
76+
{ name: 'ECDSA', hash: 'SHA-384' },
8677
publicKey as CryptoKey,
8778
signature,
88-
ec.encode(data),
79+
encoder.encode(data),
8980
),
9081
).to.equal(true);
9182
}
@@ -95,6 +86,28 @@ describe('subtle - sign / verify', () => {
9586
});
9687
}
9788

89+
it('ECDSA with HashAlgorithmIdentifier', async () => {
90+
const pair = await subtle.generateKey(
91+
{ name: 'ECDSA', namedCurve: 'P-256' },
92+
true,
93+
['sign', 'verify'],
94+
);
95+
const { publicKey, privateKey } = pair as CryptoKeyPair;
96+
const signature = await subtle.sign(
97+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
98+
privateKey as CryptoKey,
99+
encoder.encode('hello world'),
100+
);
101+
expect(
102+
await subtle.verify(
103+
{ name: 'ECDSA', hash: { name: 'SHA-256' } },
104+
publicKey as CryptoKey,
105+
signature,
106+
encoder.encode('hello world'),
107+
),
108+
).to.equal(true);
109+
});
110+
98111
// // Test Sign/Verify HMAC
99112
// {
100113
// async function test(data) {

src/Algorithms.ts

Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import type {
2+
AnyAlgorithm,
3+
DeriveBitsAlgorithm,
4+
DigestAlgorithm,
5+
EncryptDecryptAlgorithm,
6+
EncryptDecryptParams,
7+
KeyPairAlgorithm,
8+
SecretKeyAlgorithm,
9+
SignVerifyAlgorithm,
10+
SubtleAlgorithm,
11+
} from './keys';
12+
13+
type SupportedAlgorithm<Type extends string> = {
14+
[key in Type]: string | null;
15+
};
16+
17+
type SupportedAlgorithms = {
18+
digest: SupportedAlgorithm<DigestAlgorithm>;
19+
generateKey: SupportedAlgorithm<KeyPairAlgorithm | SecretKeyAlgorithm>;
20+
sign: SupportedAlgorithm<SignVerifyAlgorithm>;
21+
verify: SupportedAlgorithm<SignVerifyAlgorithm>;
22+
importKey: SupportedAlgorithm<
23+
KeyPairAlgorithm | 'PBKDF2' | SecretKeyAlgorithm | 'HKDF'
24+
>;
25+
deriveBits: SupportedAlgorithm<DeriveBitsAlgorithm>;
26+
encrypt: SupportedAlgorithm<EncryptDecryptAlgorithm>;
27+
decrypt: SupportedAlgorithm<EncryptDecryptAlgorithm>;
28+
'get key length': SupportedAlgorithm<SecretKeyAlgorithm | 'PBKDF2' | 'HKDF'>;
29+
wrapKey: SupportedAlgorithm<'AES-KW'>;
30+
unwrapKey: SupportedAlgorithm<'AES-KW'>;
31+
};
32+
33+
export type Operation =
34+
| 'digest'
35+
| 'generateKey'
36+
| 'sign'
37+
| 'verify'
38+
| 'importKey'
39+
| 'deriveBits'
40+
| 'encrypt'
41+
| 'decrypt'
42+
| 'get key length'
43+
| 'wrapKey'
44+
| 'unwrapKey';
45+
46+
const kSupportedAlgorithms: SupportedAlgorithms = {
47+
digest: {
48+
'SHA-1': null,
49+
'SHA-256': null,
50+
'SHA-384': null,
51+
'SHA-512': null,
52+
},
53+
generateKey: {
54+
'RSASSA-PKCS1-v1_5': 'RsaHashedKeyGenParams',
55+
'RSA-PSS': 'RsaHashedKeyGenParams',
56+
'RSA-OAEP': 'RsaHashedKeyGenParams',
57+
ECDSA: 'EcKeyGenParams',
58+
ECDH: 'EcKeyGenParams',
59+
'AES-CTR': 'AesKeyGenParams',
60+
'AES-CBC': 'AesKeyGenParams',
61+
'AES-GCM': 'AesKeyGenParams',
62+
'AES-KW': 'AesKeyGenParams',
63+
HMAC: 'HmacKeyGenParams',
64+
X25519: null,
65+
Ed25519: null,
66+
X448: null,
67+
Ed448: null,
68+
},
69+
sign: {
70+
'RSASSA-PKCS1-v1_5': null,
71+
'RSA-PSS': 'RsaPssParams',
72+
ECDSA: 'EcdsaParams',
73+
HMAC: null,
74+
Ed25519: null,
75+
Ed448: 'Ed448Params',
76+
},
77+
verify: {
78+
'RSASSA-PKCS1-v1_5': null,
79+
'RSA-PSS': 'RsaPssParams',
80+
ECDSA: 'EcdsaParams',
81+
HMAC: null,
82+
Ed25519: null,
83+
Ed448: 'Ed448Params',
84+
},
85+
importKey: {
86+
'RSASSA-PKCS1-v1_5': 'RsaHashedImportParams',
87+
'RSA-PSS': 'RsaHashedImportParams',
88+
'RSA-OAEP': 'RsaHashedImportParams',
89+
ECDSA: 'EcKeyImportParams',
90+
ECDH: 'EcKeyImportParams',
91+
HMAC: 'HmacImportParams',
92+
HKDF: null,
93+
PBKDF2: null,
94+
'AES-CTR': null,
95+
'AES-CBC': null,
96+
'AES-GCM': null,
97+
'AES-KW': null,
98+
Ed25519: null,
99+
X25519: null,
100+
Ed448: null,
101+
X448: null,
102+
},
103+
deriveBits: {
104+
HKDF: 'HkdfParams',
105+
PBKDF2: 'Pbkdf2Params',
106+
ECDH: 'EcdhKeyDeriveParams',
107+
X25519: 'EcdhKeyDeriveParams',
108+
X448: 'EcdhKeyDeriveParams',
109+
},
110+
encrypt: {
111+
'RSA-OAEP': 'RsaOaepParams',
112+
'AES-CBC': 'AesCbcParams',
113+
'AES-GCM': 'AesGcmParams',
114+
'AES-CTR': 'AesCtrParams',
115+
},
116+
decrypt: {
117+
'RSA-OAEP': 'RsaOaepParams',
118+
'AES-CBC': 'AesCbcParams',
119+
'AES-GCM': 'AesGcmParams',
120+
'AES-CTR': 'AesCtrParams',
121+
},
122+
'get key length': {
123+
'AES-CBC': 'AesDerivedKeyParams',
124+
'AES-CTR': 'AesDerivedKeyParams',
125+
'AES-GCM': 'AesDerivedKeyParams',
126+
'AES-KW': 'AesDerivedKeyParams',
127+
HMAC: 'HmacImportParams',
128+
HKDF: null,
129+
PBKDF2: null,
130+
},
131+
wrapKey: {
132+
'AES-KW': null,
133+
},
134+
unwrapKey: {
135+
'AES-KW': null,
136+
},
137+
};
138+
139+
type AlgorithmDictionaries = {
140+
[key in string]: object;
141+
};
142+
143+
const simpleAlgorithmDictionaries: AlgorithmDictionaries = {
144+
AesGcmParams: { iv: 'BufferSource', additionalData: 'BufferSource' },
145+
RsaHashedKeyGenParams: { hash: 'HashAlgorithmIdentifier' },
146+
EcKeyGenParams: {},
147+
HmacKeyGenParams: { hash: 'HashAlgorithmIdentifier' },
148+
RsaPssParams: {},
149+
EcdsaParams: { hash: 'HashAlgorithmIdentifier' },
150+
HmacImportParams: { hash: 'HashAlgorithmIdentifier' },
151+
HkdfParams: {
152+
hash: 'HashAlgorithmIdentifier',
153+
salt: 'BufferSource',
154+
info: 'BufferSource',
155+
},
156+
Ed448Params: { context: 'BufferSource' },
157+
Pbkdf2Params: { hash: 'HashAlgorithmIdentifier', salt: 'BufferSource' },
158+
RsaOaepParams: { label: 'BufferSource' },
159+
RsaHashedImportParams: { hash: 'HashAlgorithmIdentifier' },
160+
EcKeyImportParams: {},
161+
};
162+
163+
// https://w3c.github.io/webcrypto/#algorithm-normalization-normalize-an-algorithm
164+
// adapted for Node.js from Deno's implementation
165+
// https://github.com/denoland/deno/blob/v1.29.1/ext/crypto/00_crypto.js#L195
166+
export const normalizeAlgorithm = (
167+
algorithm: SubtleAlgorithm | EncryptDecryptParams | AnyAlgorithm,
168+
op: Operation,
169+
): SubtleAlgorithm | EncryptDecryptParams => {
170+
if (typeof algorithm === 'string') {
171+
return normalizeAlgorithm({ name: algorithm }, op);
172+
}
173+
174+
// 1.
175+
const registeredAlgorithms = kSupportedAlgorithms[op];
176+
// 2. 3.
177+
// commented, because typescript takes care of this for us 🤞👀
178+
// const initialAlg = webidl.converters.Algorithm(algorithm, {
179+
// prefix: 'Failed to normalize algorithm',
180+
// context: 'passed algorithm',
181+
// });
182+
183+
// 4.
184+
let algName = algorithm.name;
185+
if (algName === undefined) return { name: 'unknown' };
186+
187+
// 5.
188+
let desiredType: string | null | undefined;
189+
for (const key in registeredAlgorithms) {
190+
if (!Object.prototype.hasOwnProperty.call(registeredAlgorithms, key)) {
191+
continue;
192+
}
193+
if (key.toUpperCase() === algName.toUpperCase()) {
194+
algName = key as AnyAlgorithm;
195+
desiredType = (
196+
registeredAlgorithms as Record<string, typeof desiredType>
197+
)[algName];
198+
}
199+
}
200+
if (desiredType === undefined)
201+
throw new Error(`Unrecognized algorithm name: ${algName}`);
202+
203+
// Fast path everything below if the registered dictionary is null
204+
if (desiredType === null) return { name: algName };
205+
206+
// 6.
207+
const normalizedAlgorithm = algorithm;
208+
// TODO: implement this? Maybe via typescript?
209+
// webidl.converters[desiredType](algorithm, {
210+
// prefix: 'Failed to normalize algorithm',
211+
// context: 'passed algorithm',
212+
// });
213+
// 7.
214+
normalizedAlgorithm.name = algName;
215+
216+
// 9.
217+
const dict = simpleAlgorithmDictionaries[desiredType];
218+
// 10.
219+
const dictKeys = dict ? Object.keys(dict) : [];
220+
for (let i = 0; i < dictKeys.length; i++) {
221+
const member = dictKeys[i] || '';
222+
if (!Object.prototype.hasOwnProperty.call(dict, member)) continue;
223+
// TODO: implement this? Maybe via typescript?
224+
// const idlType = dict[member];
225+
// const idlValue = normalizedAlgorithm[member];
226+
// 3.
227+
// if (idlType === 'BufferSource' && idlValue) {
228+
// const isView = ArrayBufferIsView(idlValue);
229+
// normalizedAlgorithm[member] = TypedArrayPrototypeSlice(
230+
// new Uint8Array(
231+
// isView ? getDataViewOrTypedArrayBuffer(idlValue) : idlValue,
232+
// isView ? getDataViewOrTypedArrayByteOffset(idlValue) : 0,
233+
// isView
234+
// ? getDataViewOrTypedArrayByteLength(idlValue)
235+
// : ArrayBufferPrototypeGetByteLength(idlValue)
236+
// )
237+
// );
238+
// } else if (idlType === 'HashAlgorithmIdentifier') {
239+
// normalizedAlgorithm[member] = normalizeAlgorithm(idlValue, 'digest');
240+
// } else if (idlType === 'AlgorithmIdentifier') {
241+
// // This extension point is not used by any supported algorithm (yet?)
242+
// throw lazyDOMException('Not implemented.', 'NotSupportedError');
243+
// }
244+
}
245+
246+
return normalizedAlgorithm;
247+
};

src/Hashnames.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { HashAlgorithm, SubtleAlgorithm } from './keys';
1+
import type { HashAlgorithmIdentifier, HashAlgorithm } from './keys';
22

33
export enum HashContext {
44
Node,
@@ -79,7 +79,7 @@ const kHashNames: HashNames = {
7979
}
8080

8181
export function normalizeHashName(
82-
algo: string | HashAlgorithm | SubtleAlgorithm | undefined,
82+
algo: string | HashAlgorithm | HashAlgorithmIdentifier | undefined,
8383
context: HashContext = HashContext.Node,
8484
): HashAlgorithm {
8585
if (typeof algo !== 'undefined') {
@@ -91,8 +91,8 @@ export function normalizeHashName(
9191
const alias = kHashNames[normAlgo]![context] as HashAlgorithm;
9292
if (alias) return alias;
9393
// eslint-disable-next-line @typescript-eslint/no-unused-vars
94-
} catch (_e) {
95-
// ignore
94+
} catch (_e: unknown) {
95+
/* empty */
9696
}
9797
}
9898
throw new Error(`Invalid Hash Algorithm: ${algo}`);

0 commit comments

Comments
 (0)