feat: enable RNQC for encrypt/decrypt

This commit is contained in:
Brad Anderson
2025-06-02 12:16:31 -04:00
parent 4e32cae8a7
commit 0d5ee3ed07
7 changed files with 89 additions and 12 deletions

View File

@@ -0,0 +1,6 @@
---
"jazz-react-native-core": patch
"cojson": patch
---
Enable react-native-quick-crypto xsalsa20 accelerated algorithm for encrypt/decrypt functions

3
.gitignore vendored
View File

@@ -20,6 +20,9 @@ __screenshots__
# Playwright
test-results
# Java
.java-version
.husky
.vscode/*

View File

@@ -67,7 +67,7 @@ export class PureJSCrypto extends CryptoProvider<Blake3State> {
return this.blake3HashOnce(input).slice(0, 24);
}
private generateJsonNonce(material: JsonValue): Uint8Array {
protected generateJsonNonce(material: JsonValue): Uint8Array {
return this.generateNonce(textEncoder.encode(stableStringify(material)));
}

View File

@@ -58,7 +58,7 @@ import type {
BinaryStreamInfo,
} from "./coValues/coStream.js";
import type { InviteSecret } from "./coValues/group.js";
import type { AgentSecret } from "./crypto/crypto.js";
import { AgentSecret, textDecoder, textEncoder } from "./crypto/crypto.js";
import type { AgentID, RawCoID, SessionID } from "./ids.js";
import type { JsonObject, JsonValue } from "./jsonValue.js";
import type * as Media from "./media.js";
@@ -108,6 +108,8 @@ export const cojsonInternals = {
setCoValueLoadingRetryDelay(delay: number) {
CO_VALUE_LOADING_CONFIG.RETRY_DELAY = delay;
},
textEncoder,
textDecoder,
};
export {
@@ -169,18 +171,19 @@ export type {
AccountRole,
};
// biome-ignore format: off
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace CojsonInternalTypes {
export type CoValueKnownState = import("./sync.js").CoValueKnownState;
export type CoJsonValue<T> = import("./jsonValue.js").CoJsonValue<T>;
export type DoneMessage = import("./sync.js").DoneMessage;
export type Encrypted<T extends JsonValue, N extends JsonValue> = import("./crypto/crypto.js").Encrypted<T, N>;
export type KeySecret = import("./crypto/crypto.js").KeySecret;
export type KnownStateMessage = import("./sync.js").KnownStateMessage;
export type LoadMessage = import("./sync.js").LoadMessage;
export type NewContentMessage = import("./sync.js").NewContentMessage;
export type SessionNewContent = import("./sync.js").SessionNewContent;
// biome-ignore format: inserts spurious trialing comma that breaks some parsers
export type CoValueHeader = import("./coValueCore/verifiedState.js").CoValueHeader;
// biome-ignore format: inserts spurious trialing comma that breaks some parsers
export type Transaction = import("./coValueCore/verifiedState.js").Transaction;
export type TransactionID = import("./ids.js").TransactionID;
export type Signature = import("./crypto/crypto.js").Signature;
@@ -191,3 +194,4 @@ export namespace CojsonInternalTypes {
export type SignerSecret = import("./crypto/crypto.js").SignerSecret;
export type JsonObject = import("./jsonValue.js").JsonObject;
}
// biome-ignore format: on

View File

@@ -40,6 +40,7 @@
"cojson-transport-ws": "workspace:*",
"jazz-react-core": "workspace:*",
"jazz-tools": "workspace:*",
"react-native-fast-encoder": "^0.2.0",
"react-native-nitro-modules": "0.25.2",
"react-native-quick-crypto": "1.0.0-beta.16"
},

View File

@@ -1,12 +1,15 @@
import { base58 } from "@scure/base";
import { JsonValue } from "cojson";
import {
JsonValue,
Stringified,
base64URLtoBytes,
bytesToBase64url,
} from "cojson";
import { CojsonInternalTypes, cojsonInternals } from "cojson";
import { PureJSCrypto } from "cojson/dist/crypto/PureJSCrypto"; // Importing from dist to not rely on the exports field
import { Ed } from "react-native-quick-crypto";
import { Ed, xsalsa20 } from "react-native-quick-crypto";
const { stableStringify } = cojsonInternals;
const textEncoder = new TextEncoder();
export class RNQuickCrypto extends PureJSCrypto {
ed: Ed;
@@ -30,7 +33,7 @@ export class RNQuickCrypto extends PureJSCrypto {
): CojsonInternalTypes.Signature {
const signature = new Uint8Array(
this.ed.signSync(
textEncoder.encode(stableStringify(message)),
cojsonInternals.textEncoder.encode(stableStringify(message)),
base58.decode(secret.substring("signerSecret_z".length)),
),
);
@@ -44,8 +47,46 @@ export class RNQuickCrypto extends PureJSCrypto {
): boolean {
return this.ed.verifySync(
base58.decode(signature.substring("signature_z".length)),
textEncoder.encode(stableStringify(message)),
cojsonInternals.textEncoder.encode(stableStringify(message)),
base58.decode(id.substring("signer_z".length)),
);
}
encrypt<T extends JsonValue, N extends JsonValue>(
value: T,
keySecret: CojsonInternalTypes.KeySecret,
nOnceMaterial: N,
): CojsonInternalTypes.Encrypted<T, N> {
const keySecretBytes = base58.decode(
keySecret.substring("keySecret_z".length),
);
const nOnce = this.generateJsonNonce(nOnceMaterial);
const plaintext = cojsonInternals.textEncoder.encode(
stableStringify(value),
);
const ciphertext = xsalsa20(keySecretBytes, nOnce, plaintext);
return `encrypted_U${bytesToBase64url(ciphertext)}` as CojsonInternalTypes.Encrypted<
T,
N
>;
}
decryptRaw<T extends JsonValue, N extends JsonValue>(
encrypted: CojsonInternalTypes.Encrypted<T, N>,
keySecret: CojsonInternalTypes.KeySecret,
nOnceMaterial: N,
): Stringified<T> {
const keySecretBytes = base58.decode(
keySecret.substring("keySecret_z".length),
);
const nOnce = this.generateJsonNonce(nOnceMaterial);
const ciphertext = base64URLtoBytes(
encrypted.substring("encrypted_U".length),
);
const plaintext = xsalsa20(keySecretBytes, nOnce, ciphertext);
return cojsonInternals.textDecoder.decode(plaintext) as Stringified<T>;
}
}

26
pnpm-lock.yaml generated
View File

@@ -2616,6 +2616,9 @@ importers:
jazz-tools:
specifier: workspace:*
version: link:../jazz-tools
react-native-fast-encoder:
specifier: ^0.2.0
version: 0.2.0(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@15.0.1(typescript@5.6.2))(@types/react@19.0.0)(react@19.0.0))(react@19.0.0)
react-native-nitro-modules:
specifier: 0.25.2
version: 0.25.2(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@15.0.1(typescript@5.6.2))(@types/react@19.0.0)(react@19.0.0))(react@19.0.0)
@@ -9641,6 +9644,9 @@ packages:
resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==}
engines: {node: '>=16'}
flatbuffers@2.0.6:
resolution: {integrity: sha512-QTTZTXTbVfuOVQu2X6eLOw4vefUxnFJZxAKeN3rEPhjEzBtIbehimJLfVGHPM8iX0Na+9i76SBEg0skf0c0sCA==}
flatted@3.3.2:
resolution: {integrity: sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==}
@@ -12527,6 +12533,13 @@ packages:
react: 19.0.0
react-native: '*'
react-native-fast-encoder@0.2.0:
resolution: {integrity: sha512-E4mx81fRMVs0qq8is3cZTrbuEJdsDo8Nfe7qTxKZwsCianpYpA2QfyH6cEYumSOEht6l+KeRJ4RqcyfxMDyesg==}
engines: {node: '>= 18.0.0'}
peerDependencies:
react: 19.0.0
react-native: '*'
react-native-fetch-api@3.0.0:
resolution: {integrity: sha512-g2rtqPjdroaboDKTsJCTlcmtw54E25OjyaunUP0anOZn4Fuo2IKs8BVfe02zVggA/UysbmfSnRJIqtNkAgggNA==}
@@ -19144,7 +19157,7 @@ snapshots:
eslint: 8.57.1
eslint-config-prettier: 8.10.0(eslint@8.57.1)
eslint-plugin-eslint-comments: 3.2.0(eslint@8.57.1)
eslint-plugin-ft-flow: 2.0.3(@babel/eslint-parser@7.27.0(@babel/core@7.26.0)(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-ft-flow: 2.0.3(@babel/eslint-parser@7.27.0(@babel/core@7.27.1)(eslint@8.57.1))(eslint@8.57.1)
eslint-plugin-jest: 27.9.0(@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(jest@29.7.0(@types/node@22.15.18)(babel-plugin-macros@3.1.0)(ts-node@10.9.2(@swc/core@1.10.1(@swc/helpers@0.5.15))(@types/node@22.15.18)(typescript@5.6.2)))(typescript@5.6.2)
eslint-plugin-react: 7.37.4(eslint@8.57.1)
eslint-plugin-react-hooks: 4.6.2(eslint@8.57.1)
@@ -22600,7 +22613,7 @@ snapshots:
eslint: 8.57.1
ignore: 5.3.2
eslint-plugin-ft-flow@2.0.3(@babel/eslint-parser@7.27.0(@babel/core@7.26.0)(eslint@8.57.1))(eslint@8.57.1):
eslint-plugin-ft-flow@2.0.3(@babel/eslint-parser@7.27.0(@babel/core@7.27.1)(eslint@8.57.1))(eslint@8.57.1):
dependencies:
'@babel/eslint-parser': 7.27.0(@babel/core@7.27.1)(eslint@8.57.1)
eslint: 8.57.1
@@ -23509,6 +23522,8 @@ snapshots:
flatted: 3.3.2
keyv: 4.5.4
flatbuffers@2.0.6: {}
flatted@3.3.2: {}
flatted@3.3.3: {}
@@ -26870,6 +26885,13 @@ snapshots:
react: 19.0.0
react-native: 0.79.2(@babel/core@7.27.1)(@react-native-community/cli@15.0.1(typescript@5.6.2))(@types/react@19.0.0)(react@19.0.0)
react-native-fast-encoder@0.2.0(react-native@0.79.2(@babel/core@7.27.1)(@react-native-community/cli@15.0.1(typescript@5.6.2))(@types/react@19.0.0)(react@19.0.0))(react@19.0.0):
dependencies:
big-integer: 1.6.52
flatbuffers: 2.0.6
react: 19.0.0
react-native: 0.79.2(@babel/core@7.27.1)(@react-native-community/cli@15.0.1(typescript@5.6.2))(@types/react@19.0.0)(react@19.0.0)
react-native-fetch-api@3.0.0:
dependencies:
p-defer: 3.0.0