Skip to content

Commit 44d65b4

Browse files
authored
feat: expand support with modern curves, AEAD, key wrapping, derivation & HMAC (#840)
1 parent 225380e commit 44d65b4

File tree

18 files changed

+1453
-46
lines changed

18 files changed

+1453
-46
lines changed

.github/actions/post-maestro-screenshot/action.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ inputs:
1414
default: 'unknown'
1515
imgbb-api-key:
1616
description: 'ImgBB API key for image hosting'
17-
required: true
17+
required: false
1818

1919
runs:
2020
using: 'composite'
@@ -69,7 +69,7 @@ runs:
6969
fi
7070
7171
- name: Upload screenshot to ImgBB
72-
if: steps.check-screenshot.outputs.exists == 'true' && github.event_name == 'pull_request'
72+
if: steps.check-screenshot.outputs.exists == 'true' && github.event_name == 'pull_request' && inputs.imgbb-api-key != ''
7373
id: upload-screenshot
7474
uses: McCzarny/[email protected]
7575
with:
@@ -82,13 +82,15 @@ runs:
8282
uses: peter-evans/find-comment@v3
8383
id: find-comment
8484
if: github.event_name == 'pull_request'
85+
continue-on-error: true
8586
with:
8687
issue-number: ${{ github.event.pull_request.number }}
8788
comment-author: 'github-actions[bot]'
8889
body-includes: End-to-End Test Results - ${{ inputs.platform == 'ios' && 'iOS' || 'Android' }}
8990

9091
- name: Create or update PR comment (with screenshot)
9192
if: github.event_name == 'pull_request' && steps.check-screenshot.outputs.exists == 'true' && steps.upload-screenshot.outputs.url
93+
continue-on-error: true
9294
uses: peter-evans/create-or-update-comment@v4
9395
with:
9496
token: ${{ inputs.github-token }}
@@ -113,6 +115,7 @@ runs:
113115

114116
- name: Create or update PR comment (no screenshot)
115117
if: github.event_name == 'pull_request' && steps.check-screenshot.outputs.exists != 'true'
118+
continue-on-error: true
116119
uses: peter-evans/create-or-update-comment@v4
117120
with:
118121
token: ${{ inputs.github-token }}
@@ -135,6 +138,7 @@ runs:
135138

136139
- name: Create or update PR comment (upload failed)
137140
if: github.event_name == 'pull_request' && steps.check-screenshot.outputs.exists == 'true' && !steps.upload-screenshot.outputs.url
141+
continue-on-error: true
138142
uses: peter-evans/create-or-update-comment@v4
139143
with:
140144
token: ${{ inputs.github-token }}

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,6 @@ tsconfig.tsbuildinfo
187187
# development stuffs
188188
*scratch*
189189

190+
# agents
190191
.claude/settings.local.json
192+
.agent/

.husky/pre-commit

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
1+
# Add bun to PATH
2+
export PATH="$HOME/.bun/bin:$PATH"
3+
14
# Run linting and formatting on staged files
25
bun lint-staged
36

bun.lock

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
},
3131
"example": {
3232
"name": "react-native-quick-crypto-example",
33-
"version": "1.0.0",
33+
"version": "1.0.1",
3434
"dependencies": {
3535
"@craftzdog/react-native-buffer": "6.1.0",
3636
"@noble/ciphers": "^2.0.1",
@@ -47,7 +47,7 @@
4747
"react": "19.1.0",
4848
"react-native": "0.81.1",
4949
"react-native-bouncy-checkbox": "4.1.2",
50-
"react-native-fast-encoder": "^0.3.1",
50+
"react-native-fast-encoder": "0.3.1",
5151
"react-native-nitro-modules": "0.29.1",
5252
"react-native-quick-base64": "2.2.2",
5353
"react-native-quick-crypto": "workspace:*",
@@ -87,7 +87,7 @@
8787
},
8888
"packages/react-native-quick-crypto": {
8989
"name": "react-native-quick-crypto",
90-
"version": "1.0.0",
90+
"version": "1.0.1",
9191
"dependencies": {
9292
"@craftzdog/react-native-buffer": "6.1.0",
9393
"events": "3.3.0",
@@ -1308,7 +1308,7 @@
13081308

13091309
"expo-asset": ["[email protected]", "", { "dependencies": { "@expo/image-utils": "^0.8.7", "expo-constants": "~18.0.10" }, "peerDependencies": { "expo": "*", "react": "*", "react-native": "*" } }, "sha512-pZyeJkoDsALh4gpCQDzTA/UCLaPH/1rjQNGubmLn/uDM27S4iYJb/YWw4+CNZOtd5bCUOhDPg5DtGQnydNFSXg=="],
13101310

1311-
"expo-build-properties": ["[email protected].9", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-2icttCy3OPTk/GWIFt+vwA+0hup53jnmYb7JKRbvNvrrOrz+WblzpeoiaOleI2dYG/vjwpNO8to8qVyKhYJtrQ=="],
1311+
"expo-build-properties": ["[email protected].10", "", { "dependencies": { "ajv": "^8.11.0", "semver": "^7.6.0" }, "peerDependencies": { "expo": "*" } }, "sha512-mFCZbrbrv0AP5RB151tAoRzwRJelqM7bCJzCkxpu+owOyH+p/rFC/q7H5q8B9EpVWj8etaIuszR+gKwohpmu1Q=="],
13121312

13131313
"expo-constants": ["[email protected]", "", { "dependencies": { "@expo/config": "~12.0.10", "@expo/env": "~2.0.7" }, "peerDependencies": { "expo": "*", "react-native": "*" } }, "sha512-Rhtv+X974k0Cahmvx6p7ER5+pNhBC0XbP1lRviL2J1Xl4sT2FBaIuIxF/0I0CbhOsySf0ksqc5caFweAy9Ewiw=="],
13141314

@@ -1340,7 +1340,7 @@
13401340

13411341
"fast-levenshtein": ["[email protected]", "", {}, ""],
13421342

1343-
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
1343+
"fast-uri": ["fast-uri@3.0.6", "", {}, "sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw=="],
13441344

13451345
"fast-xml-parser": ["[email protected]", "", { "dependencies": { "strnum": "^1.0.5" }, "bin": { "fxparser": "src/cli/cli.js" } }, ""],
13461346

docs/implementation-coverage.md

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
# Implementation Coverage - NodeJS
22
This document attempts to describe the implementation status of Crypto APIs/Interfaces from Node.js in the `react-native-quick-crypto` library.
33

4+
> Note: This is the status for version 1.x and higher. For version `0.x` see [this document](https://github.com/margelo/react-native-quick-crypto/blob/0.x/docs/implementation-coverage.md) and the [0.x branch](https://github.com/margelo/react-native-quick-crypto/tree/0.x).
5+
46
* ` ` - not implemented in Node
57
* ❌ - implemented in Node, not RNQC
68
* ✅ - implemented in Node and RNQC
@@ -13,6 +15,7 @@ This document attempts to describe the implementation status of Crypto APIs/Inte
1315

1416
These algorithms provide quantum-resistant cryptography.
1517

18+
1619
# `Crypto`
1720

1821
* ❌ Class: `Certificate`
@@ -260,9 +263,9 @@ These algorithms provide quantum-resistant cryptography.
260263
*`subtle.getPublicKey(key, keyUsages)`
261264
* 🚧 `subtle.importKey(format, keyData, algorithm, extractable, keyUsages)`
262265
*`subtle.sign(algorithm, key, data)`
263-
* `subtle.unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgo, unwrappedKeyAlgo, extractable, keyUsages)`
266+
* `subtle.unwrapKey(format, wrappedKey, unwrappingKey, unwrapAlgo, unwrappedKeyAlgo, extractable, keyUsages)`
264267
*`subtle.verify(algorithm, key, signature, data)`
265-
* `subtle.wrapKey(format, key, wrappingKey, wrapAlgo)`
268+
* `subtle.wrapKey(format, key, wrappingKey, wrapAlgo)`
266269

267270
## `subtle.decrypt`
268271
| Algorithm | Status |
@@ -271,13 +274,14 @@ These algorithms provide quantum-resistant cryptography.
271274
| `AES-CTR` ||
272275
| `AES-CBC` ||
273276
| `AES-GCM` ||
277+
| `ChaCha20-Poly1305` ||
274278

275279
## `subtle.deriveBits`
276280
| Algorithm | Status |
277281
| --------- | :----: |
278282
| `ECDH` ||
279-
| `X25519` | |
280-
| `X448` | |
283+
| `X25519` | |
284+
| `X448` | |
281285
| `HKDF` ||
282286
| `PBKDF2` ||
283287

@@ -286,9 +290,9 @@ These algorithms provide quantum-resistant cryptography.
286290
| --------- | :----: |
287291
| `ECDH` ||
288292
| `HKDF` ||
289-
| `PBKDF2` | |
290-
| `X25519` | |
291-
| `X448` | |
293+
| `PBKDF2` | |
294+
| `X25519` | |
295+
| `X448` | |
292296

293297
## `subtle.digest`
294298
| Algorithm | Status |
@@ -310,7 +314,7 @@ These algorithms provide quantum-resistant cryptography.
310314
| `AES-CBC` ||
311315
| `AES-GCM` ||
312316
| `AES-OCB` ||
313-
| `ChaCha20-Poly1305` | |
317+
| `ChaCha20-Poly1305` | |
314318
| `RSA-OAEP` ||
315319

316320
## `subtle.exportKey`
@@ -398,8 +402,8 @@ These algorithms provide quantum-resistant cryptography.
398402
| `RSA-OAEP` |||| | | | |
399403
| `RSA-PSS` |||| | | | |
400404
| `RSASSA-PKCS1-v1_5` |||| | | | |
401-
| `X25519` | | | | | | | |
402-
| `X448` | | | | | | | |
405+
| `X25519` | | | | | | | |
406+
| `X448` | | | | | | | |
403407

404408
## `subtle.sign`
405409
| Algorithm | Status |
@@ -421,10 +425,10 @@ These algorithms provide quantum-resistant cryptography.
421425
| ------------------- | :----: |
422426
| `AES-CBC` ||
423427
| `AES-CTR` ||
424-
| `AES-GCM` | |
425-
| `AES-KW` | |
428+
| `AES-GCM` | |
429+
| `AES-KW` | |
426430
| `AES-OCB` ||
427-
| `ChaCha20-Poly1305` | |
431+
| `ChaCha20-Poly1305` | |
428432
| `RSA-OAEP` ||
429433

430434
### unwrapped key algorithms
@@ -473,8 +477,8 @@ These algorithms provide quantum-resistant cryptography.
473477
| ------------------- | :----: |
474478
| `AES-CBC` ||
475479
| `AES-CTR` ||
476-
| `AES-GCM` | |
477-
| `AES-KW` | |
480+
| `AES-GCM` | |
481+
| `AES-KW` | |
478482
| `AES-OCB` ||
479-
| `ChaCha20-Poly1305` | |
483+
| `ChaCha20-Poly1305` | |
480484
| `RSA-OAEP` ||

example/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,10 @@
3636
"react": "19.1.0",
3737
"react-native": "0.81.1",
3838
"react-native-bouncy-checkbox": "4.1.2",
39-
"react-native-fast-encoder": "^0.3.1",
39+
"react-native-fast-encoder": "0.3.1",
4040
"react-native-nitro-modules": "0.29.1",
4141
"react-native-quick-base64": "2.2.2",
42-
"react-native-quick-crypto": "1.0.1",
42+
"react-native-quick-crypto": "workspace:*",
4343
"react-native-safe-area-context": "^5.2.2",
4444
"react-native-screens": "4.18.0",
4545
"react-native-vector-icons": "^10.3.0",

example/src/hooks/useTestsList.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,16 @@ import '../tests/keys/public_cipher';
1919
import '../tests/keys/sign_verify_streaming';
2020
import '../tests/pbkdf2/pbkdf2_tests';
2121
import '../tests/random/random_tests';
22+
import '../tests/subtle/x25519_x448';
2223
import '../tests/subtle/deriveBits';
24+
import '../tests/subtle/derive_key';
2325
import '../tests/subtle/digest';
2426
import '../tests/subtle/encrypt_decrypt';
2527
import '../tests/subtle/generateKey';
2628
import '../tests/subtle/import_export';
2729
import '../tests/subtle/jwk_rfc7517_tests';
2830
import '../tests/subtle/sign_verify';
31+
import '../tests/subtle/wrap_unwrap';
2932

3033
export const useTestsList = (): [
3134
TestSuites,
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
import { test } from '../util';
2+
import { expect } from 'chai';
3+
import { subtle, getRandomValues } from 'react-native-quick-crypto';
4+
import { CryptoKey } from 'react-native-quick-crypto';
5+
import type { CryptoKeyPair } from 'react-native-quick-crypto';
6+
7+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
8+
const subtleAny = subtle as any;
9+
10+
const SUITE = 'subtle.deriveKey';
11+
12+
// Test 1: PBKDF2 deriveKey
13+
test(SUITE, 'PBKDF2 deriveKey to AES-GCM', async () => {
14+
const password = new TextEncoder().encode('my-password');
15+
const salt = getRandomValues(new Uint8Array(16));
16+
17+
const baseKey = await subtle.importKey(
18+
'raw',
19+
password,
20+
{ name: 'PBKDF2' },
21+
false,
22+
['deriveKey'],
23+
);
24+
25+
const derivedKey = await subtleAny.deriveKey(
26+
{
27+
name: 'PBKDF2',
28+
salt,
29+
iterations: 100000,
30+
hash: 'SHA-256',
31+
},
32+
baseKey as CryptoKey,
33+
{ name: 'AES-GCM', length: 256 },
34+
true,
35+
['encrypt', 'decrypt'],
36+
);
37+
38+
// Verify key can encrypt/decrypt
39+
const plaintext = new Uint8Array([1, 2, 3, 4]);
40+
const iv = getRandomValues(new Uint8Array(12));
41+
42+
const ciphertext = await subtle.encrypt(
43+
{ name: 'AES-GCM', iv },
44+
derivedKey as CryptoKey,
45+
plaintext,
46+
);
47+
48+
const decrypted = await subtle.decrypt(
49+
{ name: 'AES-GCM', iv },
50+
derivedKey as CryptoKey,
51+
ciphertext,
52+
);
53+
54+
expect(Buffer.from(decrypted).toString('hex')).to.equal(
55+
Buffer.from(plaintext).toString('hex'),
56+
);
57+
});
58+
59+
// Test 2: X25519 deriveKey
60+
test(SUITE, 'X25519 deriveKey to AES-GCM', async () => {
61+
const aliceKeyPair = await subtle.generateKey({ name: 'X25519' }, false, [
62+
'deriveKey',
63+
'deriveBits',
64+
]);
65+
66+
const bobKeyPair = await subtle.generateKey({ name: 'X25519' }, false, [
67+
'deriveKey',
68+
'deriveBits',
69+
]);
70+
71+
const aliceDerivedKey = await subtleAny.deriveKey(
72+
{
73+
name: 'X25519',
74+
public: (bobKeyPair as CryptoKeyPair).publicKey,
75+
},
76+
(aliceKeyPair as CryptoKeyPair).privateKey,
77+
{ name: 'AES-GCM', length: 256 },
78+
true,
79+
['encrypt', 'decrypt'],
80+
);
81+
82+
const bobDerivedKey = await subtleAny.deriveKey(
83+
{
84+
name: 'X25519',
85+
public: (aliceKeyPair as CryptoKeyPair).publicKey,
86+
},
87+
(bobKeyPair as CryptoKeyPair).privateKey,
88+
{ name: 'AES-GCM', length: 256 },
89+
true,
90+
['encrypt', 'decrypt'],
91+
);
92+
93+
// Both should derive the same key
94+
const aliceRaw = await subtle.exportKey('raw', aliceDerivedKey as CryptoKey);
95+
const bobRaw = await subtle.exportKey('raw', bobDerivedKey as CryptoKey);
96+
97+
expect(Buffer.from(aliceRaw as ArrayBuffer).toString('hex')).to.equal(
98+
Buffer.from(bobRaw as ArrayBuffer).toString('hex'),
99+
);
100+
});

0 commit comments

Comments
 (0)