diff --git a/.changeset/proud-humans-flash.md b/.changeset/proud-humans-flash.md new file mode 100644 index 000000000..184adcec6 --- /dev/null +++ b/.changeset/proud-humans-flash.md @@ -0,0 +1,6 @@ +--- +"jazz-react-native-core": patch +"cojson": patch +--- + +Enable react-native-quick-crypto xsalsa20 accelerated algorithm for encrypt/decrypt functions diff --git a/.gitignore b/.gitignore index f8ab61507..dcffe8f89 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ __screenshots__ # Playwright test-results +# Java +.java-version + .husky .vscode/* diff --git a/packages/cojson/src/crypto/PureJSCrypto.ts b/packages/cojson/src/crypto/PureJSCrypto.ts index dd03e57a5..4d121284f 100644 --- a/packages/cojson/src/crypto/PureJSCrypto.ts +++ b/packages/cojson/src/crypto/PureJSCrypto.ts @@ -67,7 +67,7 @@ export class PureJSCrypto extends CryptoProvider { return this.blake3HashOnce(input).slice(0, 24); } - private generateJsonNonce(material: JsonValue): Uint8Array { + protected generateJsonNonce(material: JsonValue): Uint8Array { return this.generateNonce(textEncoder.encode(stableStringify(material))); } diff --git a/packages/cojson/src/exports.ts b/packages/cojson/src/exports.ts index 5918da948..cda7ae7a8 100644 --- a/packages/cojson/src/exports.ts +++ b/packages/cojson/src/exports.ts @@ -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 = import("./jsonValue.js").CoJsonValue; export type DoneMessage = import("./sync.js").DoneMessage; + export type Encrypted = import("./crypto/crypto.js").Encrypted; + 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 diff --git a/packages/jazz-react-native-core/package.json b/packages/jazz-react-native-core/package.json index 919c38f55..c917088cd 100644 --- a/packages/jazz-react-native-core/package.json +++ b/packages/jazz-react-native-core/package.json @@ -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" }, diff --git a/packages/jazz-react-native-core/src/crypto/RNQuickCrypto.ts b/packages/jazz-react-native-core/src/crypto/RNQuickCrypto.ts index 2990ca2bb..7023770b2 100644 --- a/packages/jazz-react-native-core/src/crypto/RNQuickCrypto.ts +++ b/packages/jazz-react-native-core/src/crypto/RNQuickCrypto.ts @@ -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( + value: T, + keySecret: CojsonInternalTypes.KeySecret, + nOnceMaterial: N, + ): CojsonInternalTypes.Encrypted { + 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( + encrypted: CojsonInternalTypes.Encrypted, + keySecret: CojsonInternalTypes.KeySecret, + nOnceMaterial: N, + ): Stringified { + 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; + } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 46f917ad4..75470a138 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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