chore: provide webcrypto from react-native-quick-crypto instead of webcrypto-liner #58

This commit is contained in:
Linus Rath
2026-10-31 16:33:18 +01:00
parent 051f6bf2d5
commit 4c818d64ea
9 changed files with 433 additions and 216 deletions
+4
View File
@@ -83,6 +83,10 @@ module.exports = {
},
plugins: [
'expo-secure-store',
// WebCrypto for PGP and S/MIME (#58). On iOS it raises the deployment
// target (app and pods) to 16.4, the minimum Nitro modules build for;
// Android has nothing to set.
'react-native-quick-crypto',
'@react-native-community/datetimepicker',
'expo-localization',
[
+3
View File
@@ -1,6 +1,9 @@
// First: wraps react-native's Text for the reading font before any module
// copies it (src/theme/reading-font-text).
import './src/theme/reading-font-install';
// WebCrypto for PGP and S/MIME (#58), before openpgp or pkijs can load.
// Loads nothing native until first used.
import './src/lib/crypto/install-webcrypto';
import { createElement } from 'react';
import { AppRegistry } from 'react-native';
import { registerRootComponent } from 'expo';
+14
View File
@@ -43,4 +43,18 @@ config.transformer.getTransformOptions = async () => ({
},
});
// openpgp's package exports send Metro to its Node build, which imports
// node:crypto. The browser build (same API, WebCrypto from
// src/lib/crypto/install-webcrypto.ts) is what runs on Hermes. Only openpgp
// gets it: adding the "browser" condition globally would change what every
// other package resolves to.
const OPENPGP_BROWSER_BUILD = path.join(path.dirname(require.resolve('openpgp')), '..', 'openpgp.min.mjs');
const baseResolveRequest = config.resolver.resolveRequest;
config.resolver.resolveRequest = (context, moduleName, platform) => {
if (moduleName === 'openpgp') return { type: 'sourceFile', filePath: OPENPGP_BROWSER_BUILD };
return baseResolveRequest
? baseResolveRequest(context, moduleName, platform)
: context.resolveRequest(context, moduleName, platform);
};
module.exports = config;
+163 -195
View File
@@ -44,12 +44,14 @@
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-android-widget": "0.22.1",
"react-native-nitro-modules": "^0.37.1",
"react-native-quick-base64": "^3.0.1",
"react-native-quick-crypto": "^1.1.9",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-sse": "^1.2.1",
"react-native-svg": "15.12.1",
"react-native-webview": "13.15.0",
"webcrypto-liner": "^1.4.3",
"zustand": "^5.0.16"
},
"devDependencies": {
@@ -1560,6 +1562,30 @@
"node": ">=6.9.0"
}
},
"node_modules/@craftzdog/react-native-buffer": {
"version": "6.1.2",
"resolved": "https://registry.npmjs.org/@craftzdog/react-native-buffer/-/react-native-buffer-6.1.2.tgz",
"integrity": "sha512-KV1HitN05FHLLDG7Zb/yftDsa+mKBYBzFMQ0PMldvUicq6vWOtAvz9mDavt7Fzozh+WNqORE+yFDkkdWysZ/SA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"ieee754": "^1.2.1",
"react-native-quick-base64": "^3.0.0"
}
},
"node_modules/@expo/code-signing-certificates": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz",
@@ -2655,41 +2681,6 @@
"url": "https://github.com/sponsors/oxc-project"
}
},
"node_modules/@peculiar/asn1-schema": {
"version": "2.11.1",
"resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.11.1.tgz",
"integrity": "sha512-jaU9OaCcbZUdyXx9hMETZB9xoNmxTGjOGFqd3cOwKFkpKnyKd9WxF3uqj4PvX4cyOMP9bSdHg5YyML2qtx/d/g==",
"license": "MIT",
"dependencies": {
"@peculiar/utils": "^2.0.2",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/@peculiar/json-schema": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz",
"integrity": "sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/@peculiar/utils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/@peculiar/utils/-/utils-2.0.3.tgz",
"integrity": "sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.8.1"
}
},
"node_modules/@react-native-async-storage/async-storage": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@react-native-async-storage/async-storage/-/async-storage-2.2.0.tgz",
@@ -3384,44 +3375,6 @@
"@sinonjs/commons": "^3.0.0"
}
},
"node_modules/@stablelib/binary": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/binary/-/binary-1.0.1.tgz",
"integrity": "sha512-ClJWvmL6UBM/wjkvv/7m5VP3GMr9t0osr4yVgLZsLCOz4hGN9gIAFEqnJ0TsSMAN+n840nf2cHZnA5/KFqHC7Q==",
"license": "MIT",
"dependencies": {
"@stablelib/int": "^1.0.1"
}
},
"node_modules/@stablelib/hash": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/hash/-/hash-1.0.1.tgz",
"integrity": "sha512-eTPJc/stDkdtOcrNMZ6mcMK1e6yBbqRBaNW55XA1jU8w/7QdnCF0CmMmOD1m7VSkBR44PWrMHU2l6r8YEQHMgg==",
"license": "MIT"
},
"node_modules/@stablelib/int": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/int/-/int-1.0.1.tgz",
"integrity": "sha512-byr69X/sDtDiIjIV6m4roLVWnNNlRGzsvxw+agj8CIEazqWGOQp2dTYgQhtyVXV9wpO6WyXRQUzLV/JRNumT2w==",
"license": "MIT"
},
"node_modules/@stablelib/sha3": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/sha3/-/sha3-1.0.1.tgz",
"integrity": "sha512-82OHZcxWsJAS34L64VItIbqZdcdYgBJmeToYaou9lUA+iMjajdfOVZDDrditfV8C8yXUDrlS3BuMRWmKf9NQhQ==",
"license": "MIT",
"dependencies": {
"@stablelib/binary": "^1.0.1",
"@stablelib/hash": "^1.0.1",
"@stablelib/wipe": "^1.0.1"
}
},
"node_modules/@stablelib/wipe": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@stablelib/wipe/-/wipe-1.0.1.tgz",
"integrity": "sha512-WfqfX/eXGiAd3RJe4VU2snh/ZPwtSjLG4ynQ/vYzvghTh7dHFcI1wl+nrkWG6lGhukOxOsUHfv8dUXr58D0ayg==",
"license": "MIT"
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -3893,12 +3846,6 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"license": "MIT"
},
"node_modules/asmcrypto.js": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/asmcrypto.js/-/asmcrypto.js-2.3.2.tgz",
"integrity": "sha512-3FgFARf7RupsZETQ1nHnhLUUvpcttcCq1iZCaVAbJZbCZ5VNRrNyvpDyHTOb0KC3llFcsyOT/a99NZcCbeiEsA==",
"license": "MIT"
},
"node_modules/asn1js": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/asn1js/-/asn1js-3.1.0.tgz",
@@ -4284,12 +4231,6 @@
"node": ">=0.6"
}
},
"node_modules/bn.js": {
"version": "4.12.5",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.5.tgz",
"integrity": "sha512-3aRg6/JxfffFD+OlOjOFR3Vo79l39ooBTFucxx+MT3dhCtzn3EmiUPQo+6/OZuI2jbXi3YKgmiTFBgChQMwIRQ==",
"license": "MIT"
},
"node_modules/boolbase": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz",
@@ -4341,12 +4282,6 @@
"node": ">=8"
}
},
"node_modules/brorand": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz",
"integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==",
"license": "MIT"
},
"node_modules/browserslist": {
"version": "4.29.5",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.5.tgz",
@@ -4814,20 +4749,6 @@
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"license": "MIT"
},
"node_modules/core-js": {
"version": "3.51.0",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.51.0.tgz",
"integrity": "sha512-7bDKx/5jm5MxzsXxc93g6Z2+4ZsuT/MddrfRmTDJNAkWsYWHU8EVNEsBiIEWnMAMCHMmRfUEjkuo3jeNBOIHRw==",
"hasInstallScript": true,
"license": "MIT",
"engines": {
"node": "*"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/core-js-compat": {
"version": "3.51.0",
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.51.0.tgz",
@@ -5030,16 +4951,6 @@
"node": ">= 0.8"
}
},
"node_modules/des.js": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz",
"integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
}
},
"node_modules/destroy": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz",
@@ -5167,20 +5078,6 @@
"integrity": "sha512-s4+nfT29b0FKpVKoaIxl2pWsoNcn7A/nMPSrxayw4T3BsmPeV2lF9Y06BsAQeMGhCTQ4nYJ5XST7jkqe4ZEqFw==",
"license": "ISC"
},
"node_modules/elliptic": {
"version": "6.5.0",
"resolved": "git+ssh://git@github.com/mahrud/elliptic.git#75637c76678e83c31682fd967c2fa9ff4761b3fc",
"license": "MIT",
"dependencies": {
"bn.js": "^4.4.0",
"brorand": "^1.0.1",
"hash.js": "^1.0.0",
"hmac-drbg": "^1.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.0"
}
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
@@ -5331,6 +5228,15 @@
"node": ">=6"
}
},
"node_modules/events": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
"integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
"license": "MIT",
"engines": {
"node": ">=0.8.x"
}
},
"node_modules/expect-type": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz",
@@ -6497,16 +6403,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hash.js": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz",
"integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==",
"license": "MIT",
"dependencies": {
"inherits": "^2.0.3",
"minimalistic-assert": "^1.0.1"
}
},
"node_modules/hasown": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
@@ -6534,17 +6430,6 @@
"hermes-estree": "0.32.0"
}
},
"node_modules/hmac-drbg": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz",
"integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==",
"license": "MIT",
"dependencies": {
"hash.js": "^1.0.3",
"minimalistic-assert": "^1.0.0",
"minimalistic-crypto-utils": "^1.0.1"
}
},
"node_modules/hosted-git-info": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz",
@@ -8213,18 +8098,6 @@
"node": ">=4"
}
},
"node_modules/minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
"integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==",
"license": "ISC"
},
"node_modules/minimalistic-crypto-utils": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz",
"integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==",
"license": "MIT"
},
"node_modules/minimatch": {
"version": "10.2.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
@@ -9026,6 +8899,15 @@
"node": "^14.17.0 || ^16.13.0 || >=18.0.0"
}
},
"node_modules/process": {
"version": "0.11.10",
"resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
"integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
@@ -9251,6 +9133,124 @@
"react-native": "*"
}
},
"node_modules/react-native-nitro-modules": {
"version": "0.37.1",
"resolved": "https://registry.npmjs.org/react-native-nitro-modules/-/react-native-nitro-modules-0.37.1.tgz",
"integrity": "sha512-KpW6EQVQ/bfegpCGxN9Be+ndvsfj56t4IESEvCASu9gWpJa/NDfHpIeIwCOvZQ3eXmLEj6YD4wCLqcC1FJPr2w==",
"license": "MIT",
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-quick-base64": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/react-native-quick-base64/-/react-native-quick-base64-3.0.1.tgz",
"integrity": "sha512-EjUP2U7WqKmlMmoY7XGyHomy8bM0q4+yCDCRg4ZezQ6zedYRwc7yVk4V2O/iSftKaLEzhuW98lpyPMdk1iPHXQ==",
"license": "MIT",
"workspaces": [
"example"
],
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-quick-crypto": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/react-native-quick-crypto/-/react-native-quick-crypto-1.1.9.tgz",
"integrity": "sha512-u60Kr4EdAYI1ksGq7uwJfOXNJR/iWkCQ1AxfoVbjM/pEpiydddzsFXFYqwyv7EeB7eTDgg5/LiCGwNxA7W7NKA==",
"license": "MIT",
"dependencies": {
"@craftzdog/react-native-buffer": "^6.1.2",
"events": "3.3.0",
"readable-stream": "4.7.0",
"safe-buffer": "^5.2.1",
"string_decoder": "^1.3.0",
"util": "0.12.5"
},
"peerDependencies": {
"expo": ">=48.0.0",
"react": "*",
"react-native": "*",
"react-native-nitro-modules": ">=0.31.2",
"react-native-quick-base64": ">=3.0.0"
},
"peerDependenciesMeta": {
"expo": {
"optional": true
}
}
},
"node_modules/react-native-quick-crypto/node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
"integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT",
"dependencies": {
"base64-js": "^1.3.1",
"ieee754": "^1.2.1"
}
},
"node_modules/react-native-quick-crypto/node_modules/readable-stream": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.7.0.tgz",
"integrity": "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==",
"license": "MIT",
"dependencies": {
"abort-controller": "^3.0.0",
"buffer": "^6.0.3",
"events": "^3.3.0",
"process": "^0.11.10",
"string_decoder": "^1.3.0"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
}
},
"node_modules/react-native-quick-crypto/node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/react-native-quick-crypto/node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
"integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.2.0"
}
},
"node_modules/react-native-safe-area-context": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
@@ -10947,38 +10947,6 @@
"defaults": "^1.0.3"
}
},
"node_modules/webcrypto-core": {
"version": "1.9.2",
"resolved": "https://registry.npmjs.org/webcrypto-core/-/webcrypto-core-1.9.2.tgz",
"integrity": "sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.7.0",
"@peculiar/json-schema": "^1.1.12",
"@peculiar/utils": "^2.0.2",
"asn1js": "^3.0.10",
"tslib": "^2.8.1"
}
},
"node_modules/webcrypto-liner": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/webcrypto-liner/-/webcrypto-liner-1.4.3.tgz",
"integrity": "sha512-gzlk7ciS5zqc8QZMwpzpRxxwkcQKDJDndhr/hHWQe18Rzafhji3a7CaSxIeA2jcL0bLcAK+P77K3lWS1QXMMYA==",
"license": "MIT",
"dependencies": {
"@peculiar/asn1-schema": "^2.3.8",
"@peculiar/json-schema": "^1.1.12",
"@stablelib/sha3": "^1.0.1",
"asmcrypto.js": "^2.3.2",
"asn1js": "^3.0.5",
"core-js": "^3.35.1",
"des.js": "^1.1.0",
"elliptic": "git+https://github.com/mahrud/elliptic.git",
"pvtsutils": "^1.3.5",
"tslib": "^2.6.2",
"webcrypto-core": "^1.7.8"
}
},
"node_modules/webidl-conversions": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz",
+3 -1
View File
@@ -53,12 +53,14 @@
"react": "19.1.0",
"react-native": "0.81.5",
"react-native-android-widget": "0.22.1",
"react-native-nitro-modules": "^0.37.1",
"react-native-quick-base64": "^3.0.1",
"react-native-quick-crypto": "^1.1.9",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-sse": "^1.2.1",
"react-native-svg": "15.12.1",
"react-native-webview": "13.15.0",
"webcrypto-liner": "^1.4.3",
"zustand": "^5.0.16"
},
"devDependencies": {
+78 -14
View File
@@ -1,17 +1,20 @@
// The PGP and S/MIME libraries (#58), installed ahead of the features: each
// one loads and does one basic operation. The same libraries as the webmail's
// plugins (repos/plugins/bulwark-gpg, repos/plugins/smime). This runs under
// Node, which has WebCrypto; on Hermes openpgp and pkijs need a WebCrypto
// subtle installed first (docs in the S/MIME and PGP feature work).
import { describe, it, expect } from 'vitest';
// Node, which has WebCrypto; on Hermes the app installs one from
// react-native-quick-crypto (src/lib/crypto/install-webcrypto.ts).
import { describe, it, expect, vi, afterEach } from 'vitest';
import * as nodeCrypto from 'node:crypto';
import { webcrypto } from 'node:crypto';
import { createRequire } from 'node:module';
import path from 'node:path';
import { pathToFileURL } from 'node:url';
import * as openpgp from 'openpgp';
import * as pkijs from 'pkijs';
import * as asn1js from 'asn1js';
import { Convert } from 'pvtsutils';
// @ts-expect-error The ES build has no typings; the webmail's smime plugin
// imports it the same way, since the "browser" build has no named exports.
import * as liner from 'webcrypto-liner/build/index.es.js';
import { cbcDecrypt, setNodeStyleCrypto, type NodeStyleCrypto } from '../crypto/provider';
import { installWebCrypto } from '../crypto/install-webcrypto';
const subtle = webcrypto.subtle as unknown as SubtleCrypto;
@@ -76,15 +79,76 @@ describe('crypto libraries', () => {
await expect(parsed.verify(undefined, engine)).resolves.toBe(true);
});
it('webcrypto-liner adds 3DES, which legacy S/MIME mail still uses', async () => {
liner.setCrypto(webcrypto);
const crypto = new liner.Crypto() as Crypto;
const key = await crypto.subtle.generateKey({ name: 'DES-EDE3-CBC', length: 192 } as unknown as AesKeyGenParams, true, ['encrypt', 'decrypt']);
const iv = crypto.getRandomValues(new Uint8Array(8));
const alg = { name: 'DES-EDE3-CBC', iv } as unknown as AesCbcParams;
const sealed = await crypto.subtle.encrypt(alg, key as CryptoKey, new TextEncoder().encode('legacy'));
it('3DES, which legacy S/MIME mail still uses, decrypts through the native crypto module', () => {
setNodeStyleCrypto(() => nodeCrypto as unknown as NodeStyleCrypto);
const key = nodeCrypto.randomBytes(24);
const iv = nodeCrypto.randomBytes(8);
const c = nodeCrypto.createCipheriv('des-ede3-cbc', key, iv);
const sealed = Buffer.concat([c.update('legacy'), c.final()]);
expect(sealed.byteLength).toBe(8);
const opened = await crypto.subtle.decrypt(alg, key as CryptoKey, sealed);
const opened = cbcDecrypt('des-ede3-cbc', new Uint8Array(key), new Uint8Array(iv), new Uint8Array(sealed));
expect(new TextDecoder().decode(opened)).toBe('legacy');
});
});
describe('openpgp on a Hermes-like runtime', () => {
afterEach(() => {
vi.unstubAllGlobals();
});
// Metro bundles openpgp's browser build (metro.config.js). Hermes has
// WebCrypto only through the installed module, and no web streams or
// WebAssembly: the build must work with plain strings and bytes.
it('the browser build Metro bundles encrypts, signs and decrypts without streams or WebAssembly', async () => {
vi.stubGlobal('ReadableStream', undefined);
vi.stubGlobal('TransformStream', undefined);
vi.stubGlobal('WritableStream', undefined);
vi.stubGlobal('WebAssembly', undefined);
const req = createRequire(import.meta.url);
const browserBuild = path.join(path.dirname(req.resolve('openpgp')), '..', 'openpgp.min.mjs');
const pgp = (await import(/* @vite-ignore */ pathToFileURL(browserBuild).href)) as typeof openpgp;
for (const keyOptions of [{ type: 'ecc', curve: 'curve25519Legacy' }, { type: 'rsa', rsaBits: 2048 }] as const) {
const { privateKey, publicKey } = await pgp.generateKey({
...keyOptions,
userIDs: [{ email: 'hermes@example.org' }],
passphrase: 'pw',
format: 'armored',
});
const priv = await pgp.decryptKey({ privateKey: await pgp.readPrivateKey({ armoredKey: privateKey }), passphrase: 'pw' });
const pub = await pgp.readKey({ armoredKey: publicKey });
const armored = await pgp.encrypt({
message: await pgp.createMessage({ text: 'ohne Streams' }),
encryptionKeys: pub,
signingKeys: priv,
});
const { data, signatures } = await pgp.decrypt({
message: await pgp.readMessage({ armoredMessage: armored as string }),
decryptionKeys: priv,
verificationKeys: pub,
});
expect(data).toBe('ohne Streams');
await expect(signatures[0].verified).resolves.toBe(true);
}
}, 60_000);
});
describe('installWebCrypto', () => {
it('puts a lazy subtle and getRandomValues on a runtime without crypto', () => {
const target: Record<string, unknown> = {};
installWebCrypto(target);
const crypto = target.crypto as Record<string, unknown>;
expect(typeof crypto.getRandomValues).toBe('function');
// The native module loads on first use of subtle, not at install.
expect(Object.getOwnPropertyDescriptor(crypto, 'subtle')?.get).toBeTypeOf('function');
});
it('keeps a crypto that is already there', () => {
const subtle = {};
const getRandomValues = () => undefined;
const target: Record<string, unknown> = { crypto: { subtle, getRandomValues } };
installWebCrypto(target);
expect((target.crypto as { subtle: unknown }).subtle).toBe(subtle);
expect((target.crypto as { getRandomValues: unknown }).getRandomValues).toBe(getRandomValues);
});
});
+54
View File
@@ -0,0 +1,54 @@
// Installs WebCrypto for the PGP and S/MIME code (#58). Hermes has no
// `crypto` global; openpgp throws "The WebCrypto API is not available" at
// its first operation without one, and pkijs only picks an engine when it
// finds one. react-native-quick-crypto (OpenSSL, native) provides it.
//
// Imported first thing in index.ts. Nothing native loads here: `subtle` and
// the Node-style module are required on first use, so an app start or a
// headless push task pays nothing until a key or an encrypted message is
// touched. getRandomValues comes from expo-crypto, which the app already
// loads for src/lib/random.ts.
import { setNodeStyleCrypto, type NodeStyleCrypto } from './provider';
type QuickCrypto = NodeStyleCrypto & { subtle: SubtleCrypto; getRandomValues<T extends ArrayBufferView>(a: T): T; randomUUID(): string };
let quick: QuickCrypto | null = null;
function quickCrypto(): QuickCrypto {
if (!quick) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const mod = require('react-native-quick-crypto') as { default?: QuickCrypto } & QuickCrypto;
quick = (mod.default ?? mod) as QuickCrypto;
}
return quick;
}
function randomValues<T extends ArrayBufferView>(array: T): T {
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const expo = require('expo-crypto') as { getRandomValues?: <V extends ArrayBufferView>(a: V) => V };
if (expo.getRandomValues) return expo.getRandomValues(array);
} catch {
// fall through to OpenSSL
}
return quickCrypto().getRandomValues(array);
}
/** Put `crypto.subtle` / `crypto.getRandomValues` on the global, loading the native module lazily. */
export function installWebCrypto(target: Record<string, unknown> = globalThis as unknown as Record<string, unknown>): void {
const existing = target.crypto as Record<string, unknown> | undefined;
const crypto: Record<string, unknown> = existing && Object.isExtensible(existing) ? existing : { ...(existing ?? {}) };
if (typeof crypto.getRandomValues !== 'function') crypto.getRandomValues = randomValues;
if (typeof crypto.randomUUID !== 'function') crypto.randomUUID = () => quickCrypto().randomUUID();
if (!crypto.subtle) {
Object.defineProperty(crypto, 'subtle', {
configurable: true,
enumerable: true,
get: () => quickCrypto().subtle,
});
}
if (crypto !== existing) target.crypto = crypto;
setNodeStyleCrypto(quickCrypto);
}
installWebCrypto();
+96
View File
@@ -0,0 +1,96 @@
// The crypto the PGP and S/MIME code (#58) runs on, beyond WebCrypto.
//
// openpgp and pkijs only need `globalThis.crypto` (getRandomValues + subtle).
// S/MIME mail still uses a few things WebCrypto has no names for: RSAES-
// PKCS1-v1_5 key transport, 3DES content encryption and ChaCha20-Poly1305
// (Stalwart's encryption at rest). Those go through a Node-style crypto
// module: react-native-quick-crypto in the app (src/lib/crypto/install-
// webcrypto.ts registers it), node:crypto in the tests. Both are OpenSSL.
/** The part of Node's crypto module this code uses. */
export interface NodeStyleCrypto {
privateDecrypt(
key: { key: unknown; format?: string; type?: string; padding?: number; oaepHash?: string },
buffer: Uint8Array,
): Uint8Array;
createDecipheriv(
algorithm: string,
key: Uint8Array,
iv: Uint8Array,
options?: { authTagLength?: number },
): {
update(data: Uint8Array): Uint8Array;
final(): Uint8Array;
setAuthTag?(tag: Uint8Array): unknown;
setAAD?(aad: Uint8Array, options?: { plaintextLength: number }): unknown;
setAutoPadding?(on: boolean): unknown;
};
constants: { RSA_PKCS1_PADDING: number };
}
let load: (() => NodeStyleCrypto) | null = null;
let cached: NodeStyleCrypto | null = null;
/** Register where the Node-style crypto comes from (loaded on first use). */
export function setNodeStyleCrypto(loader: () => NodeStyleCrypto): void {
load = loader;
cached = null;
}
/** The registered Node-style crypto, or an error naming what is missing. */
export function nodeStyleCrypto(): NodeStyleCrypto {
if (cached) return cached;
if (!load) throw new Error('No native crypto module is available');
cached = load();
return cached;
}
/** The WebCrypto subtle the app installed, or an error the UI can show. */
export function webCrypto(): Crypto {
const c = (globalThis as { crypto?: Crypto }).crypto;
if (!c?.subtle || !c.getRandomValues) throw new Error('The WebCrypto API is not available');
return c;
}
/** Copy a view into a standalone ArrayBuffer (WebCrypto and pkijs want one). */
export function toArrayBuffer(view: ArrayBuffer | ArrayBufferView): ArrayBuffer {
if (view instanceof ArrayBuffer) return view;
return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength) as ArrayBuffer;
}
/** Concatenate byte arrays. */
export function concatBytes(...parts: Uint8Array[]): Uint8Array {
const out = new Uint8Array(parts.reduce((n, p) => n + p.byteLength, 0));
let offset = 0;
for (const p of parts) {
out.set(p, offset);
offset += p.byteLength;
}
return out;
}
/** Decrypt RSAES-PKCS1-v1_5 (WebCrypto has no name for it) with a PKCS#8 key. */
export function rsaPkcs1Decrypt(pkcs8: Uint8Array, ciphertext: Uint8Array): Uint8Array {
const nc = nodeStyleCrypto();
const out = nc.privateDecrypt(
{ key: pkcs8, format: 'der', type: 'pkcs8', padding: nc.constants.RSA_PKCS1_PADDING },
ciphertext,
);
return new Uint8Array(out);
}
/** CBC decryption for ciphers WebCrypto lacks (3DES). PKCS#7 padding is removed. */
export function cbcDecrypt(algorithm: 'des-ede3-cbc', key: Uint8Array, iv: Uint8Array, data: Uint8Array): Uint8Array {
const d = nodeStyleCrypto().createDecipheriv(algorithm, key, iv);
return concatBytes(new Uint8Array(d.update(data)), new Uint8Array(d.final()));
}
/** ChaCha20-Poly1305 (RFC 8103) decryption; the tag is the last 16 bytes of `sealed`. */
export function chacha20Poly1305Decrypt(key: Uint8Array, nonce: Uint8Array, sealed: Uint8Array, aad?: Uint8Array): Uint8Array {
if (sealed.byteLength < 16) throw new Error('ChaCha20-Poly1305 ciphertext is too short');
const d = nodeStyleCrypto().createDecipheriv('chacha20-poly1305', key, nonce, { authTagLength: 16 });
const body = sealed.subarray(0, sealed.byteLength - 16);
d.setAuthTag?.(sealed.subarray(sealed.byteLength - 16));
if (aad) d.setAAD?.(aad, { plaintextLength: body.byteLength });
return concatBytes(new Uint8Array(d.update(body)), new Uint8Array(d.final()));
}
+18 -6
View File
@@ -208,6 +208,24 @@
"license": "MIT",
"url": "https://github.com/sAleksovski/react-native-android-widget"
},
{
"name": "react-native-nitro-modules",
"version": "0.37.1",
"license": "MIT",
"url": "https://nitro.margelo.com"
},
{
"name": "react-native-quick-base64",
"version": "3.0.1",
"license": "MIT",
"url": "https://github.com/craftzdog/react-native-quick-base64"
},
{
"name": "react-native-quick-crypto",
"version": "1.1.9",
"license": "MIT",
"url": "https://github.com/margelo/react-native-quick-crypto"
},
{
"name": "react-native-safe-area-context",
"version": "5.6.2",
@@ -238,12 +256,6 @@
"license": "MIT",
"url": "https://github.com/react-native-webview/react-native-webview"
},
{
"name": "webcrypto-liner",
"version": "1.4.3",
"license": "MIT",
"url": "https://github.com/PeculiarVentures/webcrypto-liner"
},
{
"name": "zustand",
"version": "5.0.16",