Skip to content

Commit 8709f84

Browse files
feat: ✨ add support for subtle.importKey() for HMAC (margelo#641)
1 parent 1578d19 commit 8709f84

File tree

5 files changed

+320
-125
lines changed

5 files changed

+320
-125
lines changed

docs/implementation-coverage.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ This document attempts to describe the implementation status of Crypto APIs/Inte
321321
| `Ed25519` |||||
322322
| `Ed448` |||||
323323
| `HDKF` | | | | |
324-
| `HMAC` | | | | |
324+
| `HMAC` | | | | |
325325
| `PBKDF2` | | | ||
326326
| `RSA-OAEP` |||| |
327327
| `RSA-PSS` |||| |

packages/example/src/testing/tests/webcryptoTests/import_export.ts

Lines changed: 217 additions & 115 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { expect } from 'chai';
1+
import { assert, expect } from 'chai';
22
import { Buffer } from '@craftzdog/react-native-buffer';
33
import {
44
fromByteArray,
@@ -108,83 +108,6 @@ describe('subtle - importKey / exportKey', () => {
108108
]),
109109
'Invalid argument type for "key". Need ArrayBuffer, TypedArray, KeyObject, CryptoKey, string',
110110
);
111-
await assertThrowsAsync(
112-
async () =>
113-
await subtle.importKey(
114-
'raw',
115-
keyData,
116-
{
117-
name: 'HMAC',
118-
},
119-
false,
120-
['sign', 'verify'],
121-
),
122-
'"subtle.importKey()" is not implemented for HMAC',
123-
// TODO: will be ERR_MISSING_OPTION or similar
124-
);
125-
await assertThrowsAsync(
126-
async () =>
127-
await subtle.importKey(
128-
'raw',
129-
keyData,
130-
{
131-
name: 'HMAC',
132-
hash: 'SHA-256',
133-
},
134-
false,
135-
['deriveBits'],
136-
),
137-
'"subtle.importKey()" is not implemented for HMAC',
138-
// TODO: will be 'Unsupported key usage for an HMAC key'
139-
);
140-
await assertThrowsAsync(
141-
async () =>
142-
await subtle.importKey(
143-
'raw',
144-
keyData,
145-
{
146-
name: 'HMAC',
147-
hash: 'SHA-256',
148-
length: 0,
149-
},
150-
false,
151-
['sign', 'verify'],
152-
),
153-
'"subtle.importKey()" is not implemented for HMAC',
154-
// TODO: will be 'Zero-length key is not supported'
155-
);
156-
await assertThrowsAsync(
157-
async () =>
158-
await subtle.importKey(
159-
'raw',
160-
keyData,
161-
{
162-
name: 'HMAC',
163-
hash: 'SHA-256',
164-
length: 1,
165-
},
166-
false,
167-
['sign', 'verify'],
168-
),
169-
'"subtle.importKey()" is not implemented for HMAC',
170-
// TODO: will be 'Invalid key length'
171-
);
172-
await assertThrowsAsync(
173-
async () =>
174-
await subtle.importKey(
175-
'jwk',
176-
// @ts-expect-error bad key data
177-
null,
178-
{
179-
name: 'HMAC',
180-
hash: 'SHA-256',
181-
},
182-
false,
183-
['sign', 'verify'],
184-
),
185-
'"subtle.importKey()" is not implemented for HMAC',
186-
// TODO: will be 'Invalid keyData'
187-
);
188111
});
189112

190113
it('Good Input - Uint8Array', async () => {
@@ -862,44 +785,223 @@ describe('subtle - importKey / exportKey', () => {
862785
}
863786
}
864787

865-
// // Import/Export HMAC Secret Key
866-
// // TODO: enable this after implementing HMAC import/export
867-
// // from Node.js https://github.com/nodejs/node/blob/main/test/parallel/test-webcrypto-export-import.js#L73-L113
868-
// const keyData = globalThis.crypto.getRandomValues(new Uint8Array(32));
869-
// const key = await subtle.importKey(
870-
// 'raw',
871-
// keyData, {
872-
// name: 'HMAC',
873-
// hash: 'SHA-256'
874-
// }, true, ['sign', 'verify']);
788+
// Import/Export HMAC Secret Key
789+
it('HMAC should import raw HMAC key', async () => {
790+
const keyData = crypto.getRandomValues(new Uint8Array(32));
791+
const key = await subtle.importKey(
792+
'raw',
793+
keyData,
794+
{
795+
name: 'HMAC',
796+
hash: 'SHA-256',
797+
},
798+
true,
799+
['sign', 'verify'],
800+
);
801+
802+
assert.strictEqual(key.algorithm, key.algorithm);
803+
assert.strictEqual(key.usages, key.usages);
804+
805+
const raw = await subtle.exportKey('raw', key);
806+
807+
assert.instanceOf(raw, ArrayBuffer);
808+
809+
assert.deepStrictEqual(
810+
Buffer.from(keyData).toString('hex'),
811+
Buffer.from(raw).toString('hex'),
812+
);
813+
814+
const jwk = (await subtle.exportKey('jwk', key)) as JWK;
815+
816+
assert.property(jwk, 'key_ops');
817+
assert.property(jwk, 'kty');
818+
819+
assert.deepStrictEqual(jwk.key_ops, ['sign', 'verify']);
820+
assert(jwk.ext);
821+
822+
assert.strictEqual(jwk.kty, 'oct');
823+
824+
assert.isDefined(jwk.k);
825+
826+
assert.deepStrictEqual(
827+
Buffer.from(jwk.k, 'base64').toString('hex'),
828+
Buffer.from(raw).toString('hex'),
829+
);
830+
831+
await assertThrowsAsync(
832+
async () =>
833+
await subtle.importKey(
834+
'raw',
835+
keyData,
836+
{
837+
name: 'HMAC',
838+
hash: 'SHA-256',
839+
},
840+
true,
841+
[
842+
/* empty usages */
843+
],
844+
),
845+
'Usages cannot be empty when importing a secret key.',
846+
);
847+
});
848+
849+
it('HMAC should import JWK HMAC key', async () => {
850+
const jwk: JWK = {
851+
kty: 'oct',
852+
k: 'Y0zt37HgOx-BY7SQjYVmrqhPkO44Ii2Jcb9yydUDPfE',
853+
alg: 'HS256',
854+
ext: true,
855+
key_ops: ['sign', 'verify'],
856+
};
857+
858+
const key = await subtle.importKey(
859+
'jwk',
860+
jwk,
861+
{
862+
name: 'HMAC',
863+
hash: 'SHA-256',
864+
},
865+
true,
866+
['sign', 'verify'],
867+
);
868+
869+
expect(key.type).to.equal('secret');
870+
expect(key.extractable).to.equal(true);
871+
expect(key.algorithm.name).to.equal('HMAC');
872+
expect(key.usages).to.have.members(['sign', 'verify']);
873+
});
874+
875+
it('HMAC should reject invalid key usages', async () => {
876+
const keyData = crypto.getRandomValues(new Uint8Array(32));
877+
878+
await assertThrowsAsync(
879+
async () =>
880+
await subtle.importKey(
881+
'raw',
882+
keyData,
883+
{
884+
name: 'HMAC',
885+
hash: 'SHA-256',
886+
},
887+
true,
888+
['encrypt'], // invalid usage for HMAC
889+
),
890+
'Unsupported key usage for an HMAC key',
891+
);
892+
});
893+
894+
it('HMAC should reject invalid JWK format', async () => {
895+
const invalidJwk: JWK = {
896+
kty: 'RSA', // wrong key type
897+
k: 'Y0zt37HgOx-BY7SQjYVmrqhPkO44Ii2Jcb9yydUDPfE',
898+
};
899+
900+
await assertThrowsAsync(
901+
async () =>
902+
await subtle.importKey(
903+
'jwk',
904+
invalidJwk,
905+
{
906+
name: 'HMAC',
907+
hash: 'SHA-256',
908+
},
909+
true,
910+
['sign', 'verify'],
911+
),
912+
'Invalid JWK format for HMAC key',
913+
);
914+
});
915+
916+
it('HMAC should reject invalid key length', async () => {
917+
const jwk: JWK = {
918+
kty: 'oct',
919+
k: 'Y0zt37HgOx-BY7SQjYVmrqhPkO44Ii2Jcb9yydUDPfE',
920+
alg: 'HS256',
921+
ext: true,
922+
};
923+
924+
await assertThrowsAsync(
925+
async () =>
926+
await subtle.importKey(
927+
'jwk',
928+
jwk,
929+
{
930+
name: 'HMAC',
931+
hash: 'SHA-256',
932+
length: 128, // Doesn't match the actual key length
933+
},
934+
true,
935+
['sign', 'verify'],
936+
),
937+
'Invalid key length',
938+
);
939+
});
940+
941+
it('HMAC should reject invalid zero key length', async () => {
942+
const jwk: JWK = {
943+
kty: 'oct',
944+
k: 'Y0zt37HgOx-BY7SQjYVmrqhPkO44Ii2Jcb9yydUDPfE',
945+
alg: 'HS256',
946+
ext: true,
947+
};
875948

876-
// const raw = await subtle.exportKey('raw', key);
877-
878-
// expect(
879-
// Buffer.from(keyData).toString('hex')).to.equal(
880-
// Buffer.from(raw).toString('hex'));
881-
882-
// const jwk = await subtle.exportKey('jwk', key);
883-
// expect(jwk.key_ops).to.have.all.members(['sign', 'verify']);
884-
// assert(jwk.ext);
885-
// expect(jwk.kty, 'oct');
886-
887-
// expect(
888-
// TODO: gonna be ab2str(base64toArrayBuffer(jwk.k)) like above ^^^^
889-
// Buffer.from(jwk.k, 'base64').toString('hex')).to.equal(
890-
// Buffer.from(raw).toString('hex'));
891-
892-
// await assert.rejects(
893-
// subtle.importKey(
894-
// 'raw',
895-
// keyData,
896-
// {
897-
// name: 'HMAC',
898-
// hash: 'SHA-256'
899-
// },
900-
// true,
901-
// [// empty usages ]),
902-
// { name: 'SyntaxError', message: 'Usages cannot be empty when importing a secret key.' });
949+
await assertThrowsAsync(
950+
async () =>
951+
await subtle.importKey(
952+
'jwk',
953+
jwk,
954+
{
955+
name: 'HMAC',
956+
hash: 'SHA-256',
957+
length: 0, // Doesn't match the actual key length
958+
},
959+
true,
960+
['sign', 'verify'],
961+
),
962+
'Zero-length key is not supported',
963+
);
964+
});
965+
966+
it('HMAC should reject invalid keyData', async () => {
967+
await assertThrowsAsync(
968+
async () =>
969+
await subtle.importKey(
970+
'jwk',
971+
/**
972+
* Force JWT ts validation, it just ensure that if someone use an invalide type then
973+
* we throw an error even if they don't use typescript
974+
*/
975+
null as unknown as JWK,
976+
{
977+
name: 'HMAC',
978+
hash: 'SHA-256',
979+
},
980+
true,
981+
['sign', 'verify'],
982+
),
983+
'Invalid keyData',
984+
);
985+
});
986+
987+
it('HMAC should reject unsupported import format', async () => {
988+
const keyData = crypto.getRandomValues(new Uint8Array(32));
989+
990+
await assertThrowsAsync(
991+
async () =>
992+
await subtle.importKey(
993+
'spki', // unsupported format for HMAC
994+
keyData,
995+
{
996+
name: 'HMAC',
997+
hash: 'SHA-256',
998+
},
999+
true,
1000+
['sign', 'verify'],
1001+
),
1002+
'Unable to import HMAC key with format spki',
1003+
);
1004+
});
9031005

9041006
// Import/Export RSA Key Pairs
9051007
// from Node.js https://github.com/nodejs/node/blob/main/test/parallel/test-webcrypto-export-import.js#L157-L215

0 commit comments

Comments
 (0)