From 71f0204c6f2cc652fa99d6954a9e8ad4fe3d581f Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 7 Jul 2024 19:41:30 +0800 Subject: [PATCH 01/60] Add basic framework for native encryption --- .eslintignore | 2 + .gitignore | 2 + packages/app-mobile/package.json | 1 + .../e2ee/NativeEncryption.react-native.ts | 51 +++++++++++++++++++ packages/app-mobile/utils/shim-init-react.js | 4 ++ .../services/e2ee/NativeEncryption.node.ts | 35 +++++++++++++ packages/lib/services/e2ee/types.ts | 11 ++++ packages/lib/shim-init-node.ts | 2 + packages/lib/shim.ts | 3 ++ packages/tools/cspell/dictionary4.txt | 3 +- yarn.lock | 51 ++++++++++++++++++- 11 files changed, 163 insertions(+), 2 deletions(-) create mode 100644 packages/app-mobile/services/e2ee/NativeEncryption.react-native.ts create mode 100644 packages/lib/services/e2ee/NativeEncryption.node.ts diff --git a/.eslintignore b/.eslintignore index 956c7ea05c3..7b787bb45be 100644 --- a/.eslintignore +++ b/.eslintignore @@ -684,6 +684,7 @@ packages/app-mobile/gulpfile.js packages/app-mobile/root.js packages/app-mobile/services/AlarmServiceDriver.android.js packages/app-mobile/services/AlarmServiceDriver.ios.js +packages/app-mobile/services/e2ee/NativeEncryption.react-native.js packages/app-mobile/services/e2ee/RSA.react-native.js packages/app-mobile/services/plugins/PlatformImplementation.js packages/app-mobile/services/profiles/index.js @@ -1019,6 +1020,7 @@ packages/lib/services/database/types.js packages/lib/services/debug/populateDatabase.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js +packages/lib/services/e2ee/NativeEncryption.node.js packages/lib/services/e2ee/RSA.node.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js diff --git a/.gitignore b/.gitignore index 38f234baa2e..04969ee027a 100644 --- a/.gitignore +++ b/.gitignore @@ -663,6 +663,7 @@ packages/app-mobile/gulpfile.js packages/app-mobile/root.js packages/app-mobile/services/AlarmServiceDriver.android.js packages/app-mobile/services/AlarmServiceDriver.ios.js +packages/app-mobile/services/e2ee/NativeEncryption.react-native.js packages/app-mobile/services/e2ee/RSA.react-native.js packages/app-mobile/services/plugins/PlatformImplementation.js packages/app-mobile/services/profiles/index.js @@ -998,6 +999,7 @@ packages/lib/services/database/types.js packages/lib/services/debug/populateDatabase.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js +packages/lib/services/e2ee/NativeEncryption.node.js packages/lib/services/e2ee/RSA.node.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js diff --git a/packages/app-mobile/package.json b/packages/app-mobile/package.json index f117ca297f6..6f4fc9d5db8 100644 --- a/packages/app-mobile/package.json +++ b/packages/app-mobile/package.json @@ -60,6 +60,7 @@ "react-native-paper": "5.12.3", "react-native-popup-menu": "0.16.1", "react-native-quick-actions": "0.3.13", + "react-native-quick-crypto": "0.7.1", "react-native-rsa-native": "2.0.5", "react-native-safe-area-context": "4.10.1", "react-native-securerandom": "1.0.1", diff --git a/packages/app-mobile/services/e2ee/NativeEncryption.react-native.ts b/packages/app-mobile/services/e2ee/NativeEncryption.react-native.ts new file mode 100644 index 00000000000..90013e71677 --- /dev/null +++ b/packages/app-mobile/services/e2ee/NativeEncryption.react-native.ts @@ -0,0 +1,51 @@ +import { NativeEncryptionInterface } from '@joplin/lib/services/e2ee/types'; +import crypto from 'react-native-quick-crypto'; +import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; + +class NativeEncryption implements NativeEncryptionInterface { + public getCiphers(): string[] { + return crypto.getCiphers(); + } + + public getHashes(): string[] { + return crypto.getHashes(); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: remove the type "any" here + public async randomBytes(size: number): Promise { + return new Promise((resolve, reject) => { + crypto.randomBytes(size, (error, result) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }); + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: remove the type "any" here + public async pbkdf2Raw(password: string, salt: Buffer, iterations: number, keylen: number, digest: string): Promise { + const digestMap: { [key: string]: HashAlgorithm } = { + 'sha1': 'SHA-1', + 'sha224': 'SHA-224', + 'sha256': 'SHA-256', + 'sha384': 'SHA-384', + 'sha512': 'SHA-512', + 'ripemd160': 'RIPEMD-160', + }; + const digestAlgorithm: string = digestMap[digest.toLowerCase()] || digest; + return new Promise((resolve, reject) => { + crypto.pbkdf2(password, salt, iterations, keylen, digestAlgorithm as HashAlgorithm, (error, result) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }); + }); + } + +} + +export default NativeEncryption; diff --git a/packages/app-mobile/utils/shim-init-react.js b/packages/app-mobile/utils/shim-init-react.js index 16bd2b0536d..07d881e2a53 100644 --- a/packages/app-mobile/utils/shim-init-react.js +++ b/packages/app-mobile/utils/shim-init-react.js @@ -26,6 +26,10 @@ function shimInit() { shim.Geolocation = GeolocationReact; shim.sjclModule = require('@joplin/lib/vendor/sjcl-rn.js'); + // copied from generated shim-init-node.js. Strange but works + const NativeEncryption = require('../services/e2ee/NativeEncryption.react-native'); + shim.NativeEncryption = new NativeEncryption.default; + shim.fsDriver = () => { if (!shim.fsDriver_) { shim.fsDriver_ = new FsDriverRN(); diff --git a/packages/lib/services/e2ee/NativeEncryption.node.ts b/packages/lib/services/e2ee/NativeEncryption.node.ts new file mode 100644 index 00000000000..cc8f31bbfec --- /dev/null +++ b/packages/lib/services/e2ee/NativeEncryption.node.ts @@ -0,0 +1,35 @@ +import { NativeEncryptionInterface } from './types'; +import { promisify } from 'util'; +import crypto = require('crypto'); + +class NativeEncryption implements NativeEncryptionInterface { + public getCiphers(): string[] { + return crypto.getCiphers(); + } + + public getHashes(): string[] { + return crypto.getHashes(); + } + + public async randomBytes(size: number): Promise { + const randomBytesAsync = promisify(crypto.randomBytes); + return randomBytesAsync(size); + } + + public async pbkdf2Raw(password: string, salt: Buffer, iterations: number, keylen: number, digest: string): Promise { + const digestMap: { [key: string]: string } = { + 'sha-1': 'sha1', + 'sha-224': 'sha224', + 'sha-256': 'sha256', + 'sha-384': 'sha384', + 'sha-512': 'sha512', + 'ripemd-160': 'ripemd160', + }; + const digestAlgorithm: string = digestMap[digest.toLowerCase()] || digest; + + const pbkdf2Async = promisify(crypto.pbkdf2); + return pbkdf2Async(password, salt, iterations, keylen, digestAlgorithm); + } +} + +export default NativeEncryption; diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index be7235e9d65..0e77e3e7cf4 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -25,3 +25,14 @@ export interface RSA { publicKey(rsaKeyPair: RSAKeyPair): string; privateKey(rsaKeyPair: RSAKeyPair): string; } + +export interface NativeEncryptionInterface { + getCiphers(): string[]; + getHashes(): string[]; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: remove the type "any" here + randomBytes(size: number): Promise; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: remove the type "any" here + pbkdf2Raw(password: string, salt: Buffer, iterations: number, keylen: number, digest: string): Promise; +} diff --git a/packages/lib/shim-init-node.ts b/packages/lib/shim-init-node.ts index 1ed4872362f..fa186ada40d 100644 --- a/packages/lib/shim-init-node.ts +++ b/packages/lib/shim-init-node.ts @@ -12,6 +12,7 @@ import { ResourceEntity } from './services/database/types'; import { TextItem } from 'pdfjs-dist/types/src/display/api'; import replaceUnsupportedCharacters from './utils/replaceUnsupportedCharacters'; import { FetchBlobOptions } from './types'; +import NativeEncryption from './services/e2ee/NativeEncryption.node'; import FileApiDriverLocal from './file-api-driver-local'; import * as mimeUtils from './mime-utils'; @@ -135,6 +136,7 @@ function shimInit(options: ShimInitOptions = null) { shim.Geolocation = GeolocationNode; shim.FormData = require('form-data'); shim.sjclModule = require('./vendor/sjcl.js'); + shim.NativeEncryption = new NativeEncryption; shim.electronBridge_ = options.electronBridge; shim.fsDriver = () => { diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts index 18d6313b5b8..d50e516fcae 100644 --- a/packages/lib/shim.ts +++ b/packages/lib/shim.ts @@ -2,6 +2,7 @@ import * as React from 'react'; import { NoteEntity, ResourceEntity } from './services/database/types'; import type FsDriverBase from './fs-driver-base'; import type FileApiDriverLocal from './file-api-driver-local'; +import { NativeEncryptionInterface } from './services/e2ee/types'; export interface CreateResourceFromPathOptions { resizeLargeImages?: 'always' | 'never' | 'ask'; @@ -276,6 +277,8 @@ const shim = { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied sjclModule: null as any, + NativeEncryption: null as NativeEncryptionInterface, + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied randomBytes: async (_count: number): Promise => { throw new Error('Not implemented: randomBytes'); diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index f7a65787daa..6fba0022891 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -110,4 +110,5 @@ ENOTFOUND Scaleway Inkscape Ionicon -Stormlikes \ No newline at end of file +Stormlikes +ripemd \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 65b559885de..f83f2b21842 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4356,6 +4356,16 @@ __metadata: languageName: node linkType: hard +"@craftzdog/react-native-buffer@npm:^6.0.5": + version: 6.0.5 + resolution: "@craftzdog/react-native-buffer@npm:6.0.5" + dependencies: + ieee754: ^1.2.1 + react-native-quick-base64: ^2.0.5 + checksum: 921b8bc7f84778e355e81e475792399276d611a346a7e51b6266a45cf4aa82194beb3a8106af796ed143d958c8476070c59e3720c0eec0a3c31e368fbb08b350 + languageName: node + linkType: hard + "@cronvel/get-pixels@npm:^3.4.0": version: 3.4.0 resolution: "@cronvel/get-pixels@npm:3.4.0" @@ -6707,6 +6717,7 @@ __metadata: react-native-paper: 5.12.3 react-native-popup-menu: 0.16.1 react-native-quick-actions: 0.3.13 + react-native-quick-crypto: 0.7.1 react-native-rsa-native: 2.0.5 react-native-safe-area-context: 4.10.1 react-native-securerandom: 1.0.1 @@ -35887,6 +35898,31 @@ __metadata: languageName: node linkType: hard +"react-native-quick-base64@npm:^2.0.5": + version: 2.1.2 + resolution: "react-native-quick-base64@npm:2.1.2" + dependencies: + base64-js: ^1.5.1 + peerDependencies: + react: "*" + react-native: "*" + checksum: 46f3b26f48b26978686b0c043336220d681e6a02af5abcf3eb4ab7b9216251d1eb2fac5c559e984d963e93f54bd9f323651daac09762196815558abbd551729b + languageName: node + linkType: hard + +"react-native-quick-crypto@npm:0.7.1": + version: 0.7.1 + resolution: "react-native-quick-crypto@npm:0.7.1" + dependencies: + "@craftzdog/react-native-buffer": ^6.0.5 + events: ^3.3.0 + readable-stream: ^4.5.2 + string_decoder: ^1.3.0 + util: ^0.12.5 + checksum: 9a7a1fb1456410db30f062078f570bc566cac36fbc165e7d8ee8677bec09fcc96923de3cf7a0464804142af242a822bf66ea22460951399b9247e4a03fcfe059 + languageName: node + linkType: hard + "react-native-rsa-native@npm:2.0.5": version: 2.0.5 resolution: "react-native-rsa-native@npm:2.0.5" @@ -36603,6 +36639,19 @@ __metadata: languageName: node linkType: hard +"readable-stream@npm:^4.5.2": + version: 4.5.2 + resolution: "readable-stream@npm:4.5.2" + dependencies: + abort-controller: ^3.0.0 + buffer: ^6.0.3 + events: ^3.3.0 + process: ^0.11.10 + string_decoder: ^1.3.0 + checksum: c4030ccff010b83e4f33289c535f7830190773e274b3fcb6e2541475070bdfd69c98001c3b0cb78763fc00c8b62f514d96c2b10a8bd35d5ce45203a25fa1d33a + languageName: node + linkType: hard + "readable-stream@npm:~2.0.0": version: 2.0.6 resolution: "readable-stream@npm:2.0.6" @@ -42975,7 +43024,7 @@ __metadata: languageName: node linkType: hard -"util@npm:^0.12.4": +"util@npm:^0.12.4, util@npm:^0.12.5": version: 0.12.5 resolution: "util@npm:0.12.5" dependencies: From 17aa0fdac7cc2a83a574d96612db0b953dae2c47 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Mon, 8 Jul 2024 19:43:40 +0800 Subject: [PATCH 02/60] Remove class --- .../e2ee/NativeEncryption.react-native.ts | 22 +++++++++---------- packages/app-mobile/utils/shim-init-react.js | 4 +--- .../services/e2ee/NativeEncryption.node.ts | 21 +++++++++--------- packages/lib/shim-init-node.ts | 2 +- 4 files changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/app-mobile/services/e2ee/NativeEncryption.react-native.ts b/packages/app-mobile/services/e2ee/NativeEncryption.react-native.ts index 90013e71677..f5277e5f329 100644 --- a/packages/app-mobile/services/e2ee/NativeEncryption.react-native.ts +++ b/packages/app-mobile/services/e2ee/NativeEncryption.react-native.ts @@ -2,17 +2,18 @@ import { NativeEncryptionInterface } from '@joplin/lib/services/e2ee/types'; import crypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; -class NativeEncryption implements NativeEncryptionInterface { - public getCiphers(): string[] { +const NativeEncryption: NativeEncryptionInterface = { + + getCiphers: (): string[] => { return crypto.getCiphers(); - } + }, - public getHashes(): string[] { + getHashes: (): string[] => { return crypto.getHashes(); - } + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: remove the type "any" here - public async randomBytes(size: number): Promise { + randomBytes: async (size: number): Promise => { return new Promise((resolve, reject) => { crypto.randomBytes(size, (error, result) => { if (error) { @@ -22,10 +23,10 @@ class NativeEncryption implements NativeEncryptionInterface { } }); }); - } + }, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: remove the type "any" here - public async pbkdf2Raw(password: string, salt: Buffer, iterations: number, keylen: number, digest: string): Promise { + pbkdf2Raw: async (password: string, salt: Buffer, iterations: number, keylen: number, digest: string): Promise => { const digestMap: { [key: string]: HashAlgorithm } = { 'sha1': 'SHA-1', 'sha224': 'SHA-224', @@ -44,8 +45,7 @@ class NativeEncryption implements NativeEncryptionInterface { } }); }); - } - -} + }, +}; export default NativeEncryption; diff --git a/packages/app-mobile/utils/shim-init-react.js b/packages/app-mobile/utils/shim-init-react.js index 07d881e2a53..df826e87354 100644 --- a/packages/app-mobile/utils/shim-init-react.js +++ b/packages/app-mobile/utils/shim-init-react.js @@ -26,9 +26,7 @@ function shimInit() { shim.Geolocation = GeolocationReact; shim.sjclModule = require('@joplin/lib/vendor/sjcl-rn.js'); - // copied from generated shim-init-node.js. Strange but works - const NativeEncryption = require('../services/e2ee/NativeEncryption.react-native'); - shim.NativeEncryption = new NativeEncryption.default; + shim.NativeEncryption = require('../services/e2ee/NativeEncryption.react-native').default; shim.fsDriver = () => { if (!shim.fsDriver_) { diff --git a/packages/lib/services/e2ee/NativeEncryption.node.ts b/packages/lib/services/e2ee/NativeEncryption.node.ts index cc8f31bbfec..0c744b82519 100644 --- a/packages/lib/services/e2ee/NativeEncryption.node.ts +++ b/packages/lib/services/e2ee/NativeEncryption.node.ts @@ -2,21 +2,22 @@ import { NativeEncryptionInterface } from './types'; import { promisify } from 'util'; import crypto = require('crypto'); -class NativeEncryption implements NativeEncryptionInterface { - public getCiphers(): string[] { +const NativeEncryption: NativeEncryptionInterface = { + + getCiphers: (): string[] => { return crypto.getCiphers(); - } + }, - public getHashes(): string[] { + getHashes: (): string[] => { return crypto.getHashes(); - } + }, - public async randomBytes(size: number): Promise { + randomBytes: async (size: number): Promise => { const randomBytesAsync = promisify(crypto.randomBytes); return randomBytesAsync(size); - } + }, - public async pbkdf2Raw(password: string, salt: Buffer, iterations: number, keylen: number, digest: string): Promise { + pbkdf2Raw: async (password: string, salt: Buffer, iterations: number, keylen: number, digest: string): Promise => { const digestMap: { [key: string]: string } = { 'sha-1': 'sha1', 'sha-224': 'sha224', @@ -29,7 +30,7 @@ class NativeEncryption implements NativeEncryptionInterface { const pbkdf2Async = promisify(crypto.pbkdf2); return pbkdf2Async(password, salt, iterations, keylen, digestAlgorithm); - } -} + }, +}; export default NativeEncryption; diff --git a/packages/lib/shim-init-node.ts b/packages/lib/shim-init-node.ts index fa186ada40d..b66e4c3b551 100644 --- a/packages/lib/shim-init-node.ts +++ b/packages/lib/shim-init-node.ts @@ -136,7 +136,7 @@ function shimInit(options: ShimInitOptions = null) { shim.Geolocation = GeolocationNode; shim.FormData = require('form-data'); shim.sjclModule = require('./vendor/sjcl.js'); - shim.NativeEncryption = new NativeEncryption; + shim.NativeEncryption = NativeEncryption; shim.electronBridge_ = options.electronBridge; shim.fsDriver = () => { From a2be9f1126501561cc104e02ae91f03ac4a7dc5c Mon Sep 17 00:00:00 2001 From: wh201906 Date: Mon, 8 Jul 2024 20:02:24 +0800 Subject: [PATCH 03/60] Rename to Crypto --- .eslintignore | 4 ++-- .gitignore | 4 ++-- ...iveEncryption.react-native.ts => Crypto.react-native.ts} | 6 +++--- packages/app-mobile/utils/shim-init-react.js | 2 +- .../e2ee/{NativeEncryption.node.ts => Crypto.node.ts} | 6 +++--- packages/lib/services/e2ee/types.ts | 2 +- packages/lib/shim-init-node.ts | 4 ++-- packages/lib/shim.ts | 4 ++-- 8 files changed, 16 insertions(+), 16 deletions(-) rename packages/app-mobile/services/e2ee/{NativeEncryption.react-native.ts => Crypto.react-native.ts} (89%) rename packages/lib/services/e2ee/{NativeEncryption.node.ts => Crypto.node.ts} (86%) diff --git a/.eslintignore b/.eslintignore index 7b787bb45be..a15dcb46226 100644 --- a/.eslintignore +++ b/.eslintignore @@ -684,7 +684,7 @@ packages/app-mobile/gulpfile.js packages/app-mobile/root.js packages/app-mobile/services/AlarmServiceDriver.android.js packages/app-mobile/services/AlarmServiceDriver.ios.js -packages/app-mobile/services/e2ee/NativeEncryption.react-native.js +packages/app-mobile/services/e2ee/Crypto.react-native.js packages/app-mobile/services/e2ee/RSA.react-native.js packages/app-mobile/services/plugins/PlatformImplementation.js packages/app-mobile/services/profiles/index.js @@ -1018,9 +1018,9 @@ packages/lib/services/database/migrations/index.js packages/lib/services/database/sqlStringToLines.js packages/lib/services/database/types.js packages/lib/services/debug/populateDatabase.js +packages/lib/services/e2ee/Crypto.node.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js -packages/lib/services/e2ee/NativeEncryption.node.js packages/lib/services/e2ee/RSA.node.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js diff --git a/.gitignore b/.gitignore index 04969ee027a..897c0149add 100644 --- a/.gitignore +++ b/.gitignore @@ -663,7 +663,7 @@ packages/app-mobile/gulpfile.js packages/app-mobile/root.js packages/app-mobile/services/AlarmServiceDriver.android.js packages/app-mobile/services/AlarmServiceDriver.ios.js -packages/app-mobile/services/e2ee/NativeEncryption.react-native.js +packages/app-mobile/services/e2ee/Crypto.react-native.js packages/app-mobile/services/e2ee/RSA.react-native.js packages/app-mobile/services/plugins/PlatformImplementation.js packages/app-mobile/services/profiles/index.js @@ -997,9 +997,9 @@ packages/lib/services/database/migrations/index.js packages/lib/services/database/sqlStringToLines.js packages/lib/services/database/types.js packages/lib/services/debug/populateDatabase.js +packages/lib/services/e2ee/Crypto.node.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js -packages/lib/services/e2ee/NativeEncryption.node.js packages/lib/services/e2ee/RSA.node.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js diff --git a/packages/app-mobile/services/e2ee/NativeEncryption.react-native.ts b/packages/app-mobile/services/e2ee/Crypto.react-native.ts similarity index 89% rename from packages/app-mobile/services/e2ee/NativeEncryption.react-native.ts rename to packages/app-mobile/services/e2ee/Crypto.react-native.ts index f5277e5f329..cc7876723c0 100644 --- a/packages/app-mobile/services/e2ee/NativeEncryption.react-native.ts +++ b/packages/app-mobile/services/e2ee/Crypto.react-native.ts @@ -1,8 +1,8 @@ -import { NativeEncryptionInterface } from '@joplin/lib/services/e2ee/types'; +import { CryptoInterface } from '@joplin/lib/services/e2ee/types'; import crypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; -const NativeEncryption: NativeEncryptionInterface = { +const Crypto: CryptoInterface = { getCiphers: (): string[] => { return crypto.getCiphers(); @@ -48,4 +48,4 @@ const NativeEncryption: NativeEncryptionInterface = { }, }; -export default NativeEncryption; +export default Crypto; diff --git a/packages/app-mobile/utils/shim-init-react.js b/packages/app-mobile/utils/shim-init-react.js index df826e87354..c0e09e57631 100644 --- a/packages/app-mobile/utils/shim-init-react.js +++ b/packages/app-mobile/utils/shim-init-react.js @@ -26,7 +26,7 @@ function shimInit() { shim.Geolocation = GeolocationReact; shim.sjclModule = require('@joplin/lib/vendor/sjcl-rn.js'); - shim.NativeEncryption = require('../services/e2ee/NativeEncryption.react-native').default; + shim.Crypto = require('../services/e2ee/Crypto.react-native').default; shim.fsDriver = () => { if (!shim.fsDriver_) { diff --git a/packages/lib/services/e2ee/NativeEncryption.node.ts b/packages/lib/services/e2ee/Crypto.node.ts similarity index 86% rename from packages/lib/services/e2ee/NativeEncryption.node.ts rename to packages/lib/services/e2ee/Crypto.node.ts index 0c744b82519..086189703f2 100644 --- a/packages/lib/services/e2ee/NativeEncryption.node.ts +++ b/packages/lib/services/e2ee/Crypto.node.ts @@ -1,8 +1,8 @@ -import { NativeEncryptionInterface } from './types'; +import { CryptoInterface } from './types'; import { promisify } from 'util'; import crypto = require('crypto'); -const NativeEncryption: NativeEncryptionInterface = { +const Crypto: CryptoInterface = { getCiphers: (): string[] => { return crypto.getCiphers(); @@ -33,4 +33,4 @@ const NativeEncryption: NativeEncryptionInterface = { }, }; -export default NativeEncryption; +export default Crypto; diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 0e77e3e7cf4..dc1c9bb08ed 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -26,7 +26,7 @@ export interface RSA { privateKey(rsaKeyPair: RSAKeyPair): string; } -export interface NativeEncryptionInterface { +export interface CryptoInterface { getCiphers(): string[]; getHashes(): string[]; diff --git a/packages/lib/shim-init-node.ts b/packages/lib/shim-init-node.ts index b66e4c3b551..d68c13ade7d 100644 --- a/packages/lib/shim-init-node.ts +++ b/packages/lib/shim-init-node.ts @@ -12,7 +12,7 @@ import { ResourceEntity } from './services/database/types'; import { TextItem } from 'pdfjs-dist/types/src/display/api'; import replaceUnsupportedCharacters from './utils/replaceUnsupportedCharacters'; import { FetchBlobOptions } from './types'; -import NativeEncryption from './services/e2ee/NativeEncryption.node'; +import Crypto from './services/e2ee/Crypto.node'; import FileApiDriverLocal from './file-api-driver-local'; import * as mimeUtils from './mime-utils'; @@ -136,7 +136,7 @@ function shimInit(options: ShimInitOptions = null) { shim.Geolocation = GeolocationNode; shim.FormData = require('form-data'); shim.sjclModule = require('./vendor/sjcl.js'); - shim.NativeEncryption = NativeEncryption; + shim.Crypto = Crypto; shim.electronBridge_ = options.electronBridge; shim.fsDriver = () => { diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts index d50e516fcae..de15bd7c13b 100644 --- a/packages/lib/shim.ts +++ b/packages/lib/shim.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { NoteEntity, ResourceEntity } from './services/database/types'; import type FsDriverBase from './fs-driver-base'; import type FileApiDriverLocal from './file-api-driver-local'; -import { NativeEncryptionInterface } from './services/e2ee/types'; +import { CryptoInterface } from './services/e2ee/types'; export interface CreateResourceFromPathOptions { resizeLargeImages?: 'always' | 'never' | 'ask'; @@ -277,7 +277,7 @@ const shim = { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied sjclModule: null as any, - NativeEncryption: null as NativeEncryptionInterface, + Crypto: null as CryptoInterface, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied randomBytes: async (_count: number): Promise => { From 17badd52e30f1dd9b8e793c832b22a38334c8808 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Thu, 11 Jul 2024 20:40:58 +0800 Subject: [PATCH 04/60] Rename to Krypto --- .eslintignore | 4 ++-- .gitignore | 4 ++-- .../e2ee/{Crypto.react-native.ts => Krypto.react-native.ts} | 6 +++--- packages/app-mobile/utils/shim-init-react.js | 2 +- .../lib/services/e2ee/{Crypto.node.ts => Krypto.node.ts} | 6 +++--- packages/lib/services/e2ee/types.ts | 2 +- packages/lib/shim-init-node.ts | 4 ++-- packages/lib/shim.ts | 4 ++-- packages/tools/cspell/dictionary4.txt | 3 ++- 9 files changed, 18 insertions(+), 17 deletions(-) rename packages/app-mobile/services/e2ee/{Crypto.react-native.ts => Krypto.react-native.ts} (91%) rename packages/lib/services/e2ee/{Crypto.node.ts => Krypto.node.ts} (89%) diff --git a/.eslintignore b/.eslintignore index a15dcb46226..567ea9379e5 100644 --- a/.eslintignore +++ b/.eslintignore @@ -684,7 +684,7 @@ packages/app-mobile/gulpfile.js packages/app-mobile/root.js packages/app-mobile/services/AlarmServiceDriver.android.js packages/app-mobile/services/AlarmServiceDriver.ios.js -packages/app-mobile/services/e2ee/Crypto.react-native.js +packages/app-mobile/services/e2ee/Krypto.react-native.js packages/app-mobile/services/e2ee/RSA.react-native.js packages/app-mobile/services/plugins/PlatformImplementation.js packages/app-mobile/services/profiles/index.js @@ -1018,9 +1018,9 @@ packages/lib/services/database/migrations/index.js packages/lib/services/database/sqlStringToLines.js packages/lib/services/database/types.js packages/lib/services/debug/populateDatabase.js -packages/lib/services/e2ee/Crypto.node.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js +packages/lib/services/e2ee/Krypto.node.js packages/lib/services/e2ee/RSA.node.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js diff --git a/.gitignore b/.gitignore index 897c0149add..98d7a01d3b1 100644 --- a/.gitignore +++ b/.gitignore @@ -663,7 +663,7 @@ packages/app-mobile/gulpfile.js packages/app-mobile/root.js packages/app-mobile/services/AlarmServiceDriver.android.js packages/app-mobile/services/AlarmServiceDriver.ios.js -packages/app-mobile/services/e2ee/Crypto.react-native.js +packages/app-mobile/services/e2ee/Krypto.react-native.js packages/app-mobile/services/e2ee/RSA.react-native.js packages/app-mobile/services/plugins/PlatformImplementation.js packages/app-mobile/services/profiles/index.js @@ -997,9 +997,9 @@ packages/lib/services/database/migrations/index.js packages/lib/services/database/sqlStringToLines.js packages/lib/services/database/types.js packages/lib/services/debug/populateDatabase.js -packages/lib/services/e2ee/Crypto.node.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js +packages/lib/services/e2ee/Krypto.node.js packages/lib/services/e2ee/RSA.node.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js diff --git a/packages/app-mobile/services/e2ee/Crypto.react-native.ts b/packages/app-mobile/services/e2ee/Krypto.react-native.ts similarity index 91% rename from packages/app-mobile/services/e2ee/Crypto.react-native.ts rename to packages/app-mobile/services/e2ee/Krypto.react-native.ts index cc7876723c0..ed9dcf24c4a 100644 --- a/packages/app-mobile/services/e2ee/Crypto.react-native.ts +++ b/packages/app-mobile/services/e2ee/Krypto.react-native.ts @@ -1,8 +1,8 @@ -import { CryptoInterface } from '@joplin/lib/services/e2ee/types'; +import { KryptoInterface } from '@joplin/lib/services/e2ee/types'; import crypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; -const Crypto: CryptoInterface = { +const Krypto: KryptoInterface = { getCiphers: (): string[] => { return crypto.getCiphers(); @@ -48,4 +48,4 @@ const Crypto: CryptoInterface = { }, }; -export default Crypto; +export default Krypto; diff --git a/packages/app-mobile/utils/shim-init-react.js b/packages/app-mobile/utils/shim-init-react.js index c0e09e57631..69fb013260b 100644 --- a/packages/app-mobile/utils/shim-init-react.js +++ b/packages/app-mobile/utils/shim-init-react.js @@ -26,7 +26,7 @@ function shimInit() { shim.Geolocation = GeolocationReact; shim.sjclModule = require('@joplin/lib/vendor/sjcl-rn.js'); - shim.Crypto = require('../services/e2ee/Crypto.react-native').default; + shim.Krypto = require('../services/e2ee/Krypto.react-native').default; shim.fsDriver = () => { if (!shim.fsDriver_) { diff --git a/packages/lib/services/e2ee/Crypto.node.ts b/packages/lib/services/e2ee/Krypto.node.ts similarity index 89% rename from packages/lib/services/e2ee/Crypto.node.ts rename to packages/lib/services/e2ee/Krypto.node.ts index 086189703f2..3a929f27df2 100644 --- a/packages/lib/services/e2ee/Crypto.node.ts +++ b/packages/lib/services/e2ee/Krypto.node.ts @@ -1,8 +1,8 @@ -import { CryptoInterface } from './types'; +import { KryptoInterface } from './types'; import { promisify } from 'util'; import crypto = require('crypto'); -const Crypto: CryptoInterface = { +const Krypto: KryptoInterface = { getCiphers: (): string[] => { return crypto.getCiphers(); @@ -33,4 +33,4 @@ const Crypto: CryptoInterface = { }, }; -export default Crypto; +export default Krypto; diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index dc1c9bb08ed..29736c766ca 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -26,7 +26,7 @@ export interface RSA { privateKey(rsaKeyPair: RSAKeyPair): string; } -export interface CryptoInterface { +export interface KryptoInterface { getCiphers(): string[]; getHashes(): string[]; diff --git a/packages/lib/shim-init-node.ts b/packages/lib/shim-init-node.ts index d68c13ade7d..fc605a68941 100644 --- a/packages/lib/shim-init-node.ts +++ b/packages/lib/shim-init-node.ts @@ -12,7 +12,7 @@ import { ResourceEntity } from './services/database/types'; import { TextItem } from 'pdfjs-dist/types/src/display/api'; import replaceUnsupportedCharacters from './utils/replaceUnsupportedCharacters'; import { FetchBlobOptions } from './types'; -import Crypto from './services/e2ee/Crypto.node'; +import Krypto from './services/e2ee/Krypto.node'; import FileApiDriverLocal from './file-api-driver-local'; import * as mimeUtils from './mime-utils'; @@ -136,7 +136,7 @@ function shimInit(options: ShimInitOptions = null) { shim.Geolocation = GeolocationNode; shim.FormData = require('form-data'); shim.sjclModule = require('./vendor/sjcl.js'); - shim.Crypto = Crypto; + shim.Krypto = Krypto; shim.electronBridge_ = options.electronBridge; shim.fsDriver = () => { diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts index de15bd7c13b..f830c9875eb 100644 --- a/packages/lib/shim.ts +++ b/packages/lib/shim.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { NoteEntity, ResourceEntity } from './services/database/types'; import type FsDriverBase from './fs-driver-base'; import type FileApiDriverLocal from './file-api-driver-local'; -import { CryptoInterface } from './services/e2ee/types'; +import { KryptoInterface } from './services/e2ee/types'; export interface CreateResourceFromPathOptions { resizeLargeImages?: 'always' | 'never' | 'ask'; @@ -277,7 +277,7 @@ const shim = { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied sjclModule: null as any, - Crypto: null as CryptoInterface, + Krypto: null as KryptoInterface, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied randomBytes: async (_count: number): Promise => { diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index 6fba0022891..63326f1e27c 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -111,4 +111,5 @@ Scaleway Inkscape Ionicon Stormlikes -ripemd \ No newline at end of file +ripemd +krypto \ No newline at end of file From 4c8c2f329ddacf3f79e877ff177cfa786aaf3a51 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Fri, 19 Jul 2024 23:53:39 +0800 Subject: [PATCH 05/60] Rename --- .eslintignore | 4 ++-- .gitignore | 4 ++-- .../services/e2ee/{Krypto.react-native.ts => crypto.ts} | 6 +++--- packages/app-mobile/utils/shim-init-react.ts | 4 ++-- packages/lib/services/e2ee/{Krypto.node.ts => crypto.ts} | 6 +++--- packages/lib/services/e2ee/types.ts | 2 +- packages/lib/shim-init-node.ts | 4 ++-- packages/lib/shim.ts | 4 ++-- packages/tools/cspell/dictionary4.txt | 3 +-- 9 files changed, 18 insertions(+), 19 deletions(-) rename packages/app-mobile/services/e2ee/{Krypto.react-native.ts => crypto.ts} (91%) rename packages/lib/services/e2ee/{Krypto.node.ts => crypto.ts} (89%) diff --git a/.eslintignore b/.eslintignore index da7cfc2fc75..dcf3130fe77 100644 --- a/.eslintignore +++ b/.eslintignore @@ -687,8 +687,8 @@ packages/app-mobile/gulpfile.js packages/app-mobile/root.js packages/app-mobile/services/AlarmServiceDriver.android.js packages/app-mobile/services/AlarmServiceDriver.ios.js -packages/app-mobile/services/e2ee/Krypto.react-native.js packages/app-mobile/services/e2ee/RSA.react-native.js +packages/app-mobile/services/e2ee/crypto.js packages/app-mobile/services/plugins/PlatformImplementation.js packages/app-mobile/services/profiles/index.js packages/app-mobile/services/voiceTyping/vosk.android.js @@ -1028,8 +1028,8 @@ packages/lib/services/database/types.js packages/lib/services/debug/populateDatabase.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js -packages/lib/services/e2ee/Krypto.node.js packages/lib/services/e2ee/RSA.node.js +packages/lib/services/e2ee/crypto.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js packages/lib/services/e2ee/ppkTestUtils.js diff --git a/.gitignore b/.gitignore index 958856e360d..1ee06be33c9 100644 --- a/.gitignore +++ b/.gitignore @@ -666,8 +666,8 @@ packages/app-mobile/gulpfile.js packages/app-mobile/root.js packages/app-mobile/services/AlarmServiceDriver.android.js packages/app-mobile/services/AlarmServiceDriver.ios.js -packages/app-mobile/services/e2ee/Krypto.react-native.js packages/app-mobile/services/e2ee/RSA.react-native.js +packages/app-mobile/services/e2ee/crypto.js packages/app-mobile/services/plugins/PlatformImplementation.js packages/app-mobile/services/profiles/index.js packages/app-mobile/services/voiceTyping/vosk.android.js @@ -1007,8 +1007,8 @@ packages/lib/services/database/types.js packages/lib/services/debug/populateDatabase.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js -packages/lib/services/e2ee/Krypto.node.js packages/lib/services/e2ee/RSA.node.js +packages/lib/services/e2ee/crypto.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js packages/lib/services/e2ee/ppkTestUtils.js diff --git a/packages/app-mobile/services/e2ee/Krypto.react-native.ts b/packages/app-mobile/services/e2ee/crypto.ts similarity index 91% rename from packages/app-mobile/services/e2ee/Krypto.react-native.ts rename to packages/app-mobile/services/e2ee/crypto.ts index ed9dcf24c4a..cba9ff138be 100644 --- a/packages/app-mobile/services/e2ee/Krypto.react-native.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,8 +1,8 @@ -import { KryptoInterface } from '@joplin/lib/services/e2ee/types'; +import { Crypto } from '@joplin/lib/services/e2ee/types'; import crypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; -const Krypto: KryptoInterface = { +const cryptoLib: Crypto = { getCiphers: (): string[] => { return crypto.getCiphers(); @@ -48,4 +48,4 @@ const Krypto: KryptoInterface = { }, }; -export default Krypto; +export default cryptoLib; diff --git a/packages/app-mobile/utils/shim-init-react.ts b/packages/app-mobile/utils/shim-init-react.ts index be09f8f7531..1b79c7d268a 100644 --- a/packages/app-mobile/utils/shim-init-react.ts +++ b/packages/app-mobile/utils/shim-init-react.ts @@ -15,12 +15,12 @@ import { getLocales } from 'react-native-localize'; import { setLocale, defaultLocale, closestSupportedLocale } from '@joplin/lib/locale'; import type SettingType from '@joplin/lib/models/Setting'; import injectedJs from './injectedJs'; +import cryptoLib from '../services/e2ee/crypto'; export default function shimInit() { shim.Geolocation = GeolocationReact; shim.sjclModule = require('@joplin/lib/vendor/sjcl-rn.js'); - - shim.Krypto = require('../services/e2ee/Krypto.react-native').default; + shim.cryptoLib = cryptoLib; shim.fsDriver = () => { if (!shim.fsDriver_) { diff --git a/packages/lib/services/e2ee/Krypto.node.ts b/packages/lib/services/e2ee/crypto.ts similarity index 89% rename from packages/lib/services/e2ee/Krypto.node.ts rename to packages/lib/services/e2ee/crypto.ts index 3a929f27df2..598dca29749 100644 --- a/packages/lib/services/e2ee/Krypto.node.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,8 +1,8 @@ -import { KryptoInterface } from './types'; +import { Crypto } from './types'; import { promisify } from 'util'; import crypto = require('crypto'); -const Krypto: KryptoInterface = { +const cryptoLib: Crypto = { getCiphers: (): string[] => { return crypto.getCiphers(); @@ -33,4 +33,4 @@ const Krypto: KryptoInterface = { }, }; -export default Krypto; +export default cryptoLib; diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 29736c766ca..1f7e138e7a5 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -26,7 +26,7 @@ export interface RSA { privateKey(rsaKeyPair: RSAKeyPair): string; } -export interface KryptoInterface { +export interface Crypto { getCiphers(): string[]; getHashes(): string[]; diff --git a/packages/lib/shim-init-node.ts b/packages/lib/shim-init-node.ts index fc605a68941..f51939bc4ee 100644 --- a/packages/lib/shim-init-node.ts +++ b/packages/lib/shim-init-node.ts @@ -12,7 +12,7 @@ import { ResourceEntity } from './services/database/types'; import { TextItem } from 'pdfjs-dist/types/src/display/api'; import replaceUnsupportedCharacters from './utils/replaceUnsupportedCharacters'; import { FetchBlobOptions } from './types'; -import Krypto from './services/e2ee/Krypto.node'; +import cryptoLib from './services/e2ee/crypto'; import FileApiDriverLocal from './file-api-driver-local'; import * as mimeUtils from './mime-utils'; @@ -136,7 +136,7 @@ function shimInit(options: ShimInitOptions = null) { shim.Geolocation = GeolocationNode; shim.FormData = require('form-data'); shim.sjclModule = require('./vendor/sjcl.js'); - shim.Krypto = Krypto; + shim.cryptoLib = cryptoLib; shim.electronBridge_ = options.electronBridge; shim.fsDriver = () => { diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts index 7706f6212fd..1b5037695a8 100644 --- a/packages/lib/shim.ts +++ b/packages/lib/shim.ts @@ -2,7 +2,7 @@ import * as React from 'react'; import { NoteEntity, ResourceEntity } from './services/database/types'; import type FsDriverBase from './fs-driver-base'; import type FileApiDriverLocal from './file-api-driver-local'; -import { KryptoInterface } from './services/e2ee/types'; +import { Crypto } from './services/e2ee/types'; export interface CreateResourceFromPathOptions { resizeLargeImages?: 'always' | 'never' | 'ask'; @@ -287,7 +287,7 @@ const shim = { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied sjclModule: null as any, - Krypto: null as KryptoInterface, + cryptoLib: null as Crypto, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied randomBytes: async (_count: number): Promise => { diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index 63326f1e27c..6fba0022891 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -111,5 +111,4 @@ Scaleway Inkscape Ionicon Stormlikes -ripemd -krypto \ No newline at end of file +ripemd \ No newline at end of file From 8387c96bed041723844d1f81f4d5db8d9ee29059 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Fri, 19 Jul 2024 23:55:43 +0800 Subject: [PATCH 06/60] Remove type any --- packages/app-mobile/services/e2ee/crypto.ts | 6 ++---- packages/lib/services/e2ee/types.ts | 8 ++------ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index cba9ff138be..c4d10b03ffe 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -12,8 +12,7 @@ const cryptoLib: Crypto = { return crypto.getHashes(); }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: remove the type "any" here - randomBytes: async (size: number): Promise => { + randomBytes: async (size: number): Promise => { return new Promise((resolve, reject) => { crypto.randomBytes(size, (error, result) => { if (error) { @@ -25,8 +24,7 @@ const cryptoLib: Crypto = { }); }, - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: remove the type "any" here - pbkdf2Raw: async (password: string, salt: Buffer, iterations: number, keylen: number, digest: string): Promise => { + pbkdf2Raw: async (password: string, salt: Uint8Array, iterations: number, keylen: number, digest: string): Promise => { const digestMap: { [key: string]: HashAlgorithm } = { 'sha1': 'SHA-1', 'sha224': 'SHA-224', diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 1f7e138e7a5..cbf6f3f72df 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -29,10 +29,6 @@ export interface RSA { export interface Crypto { getCiphers(): string[]; getHashes(): string[]; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: remove the type "any" here - randomBytes(size: number): Promise; - - // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: remove the type "any" here - pbkdf2Raw(password: string, salt: Buffer, iterations: number, keylen: number, digest: string): Promise; + randomBytes(size: number): Promise; + pbkdf2Raw(password: string, salt: Uint8Array, iterations: number, keylen: number, digest: string): Promise; } From 0276041851265a9ffae2e803adea6a31ec1612f6 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Fri, 19 Jul 2024 23:57:52 +0800 Subject: [PATCH 07/60] Add common interface for the Buffer in Node.js and react-native-buffer --- .eslintrc.js | 2 ++ packages/app-mobile/services/e2ee/crypto.ts | 6 +++--- packages/lib/services/e2ee/types.ts | 8 ++++++-- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index ce8065678a2..e5bfc4bbb96 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -44,6 +44,8 @@ module.exports = { 'tinymce': 'readonly', 'JSX': 'readonly', + + 'BufferEncoding': 'readonly', }, 'parserOptions': { 'ecmaVersion': 2018, diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index c4d10b03ffe..22d9cf4e757 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,4 +1,4 @@ -import { Crypto } from '@joplin/lib/services/e2ee/types'; +import { Crypto, CryptoBuffer } from '@joplin/lib/services/e2ee/types'; import crypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; @@ -12,7 +12,7 @@ const cryptoLib: Crypto = { return crypto.getHashes(); }, - randomBytes: async (size: number): Promise => { + randomBytes: async (size: number): Promise => { return new Promise((resolve, reject) => { crypto.randomBytes(size, (error, result) => { if (error) { @@ -24,7 +24,7 @@ const cryptoLib: Crypto = { }); }, - pbkdf2Raw: async (password: string, salt: Uint8Array, iterations: number, keylen: number, digest: string): Promise => { + pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: string): Promise => { const digestMap: { [key: string]: HashAlgorithm } = { 'sha1': 'SHA-1', 'sha224': 'SHA-224', diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index cbf6f3f72df..986254ccbaf 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -29,6 +29,10 @@ export interface RSA { export interface Crypto { getCiphers(): string[]; getHashes(): string[]; - randomBytes(size: number): Promise; - pbkdf2Raw(password: string, salt: Uint8Array, iterations: number, keylen: number, digest: string): Promise; + randomBytes(size: number): Promise; + pbkdf2Raw(password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: string): Promise; +} + +export interface CryptoBuffer extends Uint8Array { + toString(encoding?: BufferEncoding, start?: number, end?: number): string; } From 832e589f28547096f87ba8adecb6ecfedbfe0967 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sat, 20 Jul 2024 13:58:46 +0800 Subject: [PATCH 08/60] Clean up --- packages/app-mobile/services/e2ee/crypto.ts | 14 +++++++------- packages/app-mobile/utils/shim-init-react.ts | 4 ++-- packages/lib/services/e2ee/crypto.ts | 18 +++++++++++------- packages/lib/shim-init-node.ts | 4 ++-- packages/lib/shim.ts | 2 +- 5 files changed, 23 insertions(+), 19 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 22d9cf4e757..c186c8d83e5 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,20 +1,20 @@ import { Crypto, CryptoBuffer } from '@joplin/lib/services/e2ee/types'; -import crypto from 'react-native-quick-crypto'; +import QuickCrypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; -const cryptoLib: Crypto = { +const crypto: Crypto = { getCiphers: (): string[] => { - return crypto.getCiphers(); + return QuickCrypto.getCiphers(); }, getHashes: (): string[] => { - return crypto.getHashes(); + return QuickCrypto.getHashes(); }, randomBytes: async (size: number): Promise => { return new Promise((resolve, reject) => { - crypto.randomBytes(size, (error, result) => { + QuickCrypto.randomBytes(size, (error, result) => { if (error) { reject(error); } else { @@ -35,7 +35,7 @@ const cryptoLib: Crypto = { }; const digestAlgorithm: string = digestMap[digest.toLowerCase()] || digest; return new Promise((resolve, reject) => { - crypto.pbkdf2(password, salt, iterations, keylen, digestAlgorithm as HashAlgorithm, (error, result) => { + QuickCrypto.pbkdf2(password, salt, iterations, keylen, digestAlgorithm as HashAlgorithm, (error, result) => { if (error) { reject(error); } else { @@ -46,4 +46,4 @@ const cryptoLib: Crypto = { }, }; -export default cryptoLib; +export default crypto; diff --git a/packages/app-mobile/utils/shim-init-react.ts b/packages/app-mobile/utils/shim-init-react.ts index 1b79c7d268a..7a927302d89 100644 --- a/packages/app-mobile/utils/shim-init-react.ts +++ b/packages/app-mobile/utils/shim-init-react.ts @@ -15,12 +15,12 @@ import { getLocales } from 'react-native-localize'; import { setLocale, defaultLocale, closestSupportedLocale } from '@joplin/lib/locale'; import type SettingType from '@joplin/lib/models/Setting'; import injectedJs from './injectedJs'; -import cryptoLib from '../services/e2ee/crypto'; +import crypto from '../services/e2ee/crypto'; export default function shimInit() { shim.Geolocation = GeolocationReact; shim.sjclModule = require('@joplin/lib/vendor/sjcl-rn.js'); - shim.cryptoLib = cryptoLib; + shim.crypto = crypto; shim.fsDriver = () => { if (!shim.fsDriver_) { diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 598dca29749..58ad85ae817 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,19 +1,23 @@ import { Crypto } from './types'; import { promisify } from 'util'; -import crypto = require('crypto'); +import { + getCiphers as nodeGetCiphers, getHashes as nodeGetHashes, + randomBytes as nodeRandomBytes, + pbkdf2 as nodePbkdf2, +} from 'crypto'; -const cryptoLib: Crypto = { +const crypto: Crypto = { getCiphers: (): string[] => { - return crypto.getCiphers(); + return nodeGetCiphers(); }, getHashes: (): string[] => { - return crypto.getHashes(); + return nodeGetHashes(); }, randomBytes: async (size: number): Promise => { - const randomBytesAsync = promisify(crypto.randomBytes); + const randomBytesAsync = promisify(nodeRandomBytes); return randomBytesAsync(size); }, @@ -28,9 +32,9 @@ const cryptoLib: Crypto = { }; const digestAlgorithm: string = digestMap[digest.toLowerCase()] || digest; - const pbkdf2Async = promisify(crypto.pbkdf2); + const pbkdf2Async = promisify(nodePbkdf2); return pbkdf2Async(password, salt, iterations, keylen, digestAlgorithm); }, }; -export default cryptoLib; +export default crypto; diff --git a/packages/lib/shim-init-node.ts b/packages/lib/shim-init-node.ts index f51939bc4ee..5d076897484 100644 --- a/packages/lib/shim-init-node.ts +++ b/packages/lib/shim-init-node.ts @@ -12,7 +12,7 @@ import { ResourceEntity } from './services/database/types'; import { TextItem } from 'pdfjs-dist/types/src/display/api'; import replaceUnsupportedCharacters from './utils/replaceUnsupportedCharacters'; import { FetchBlobOptions } from './types'; -import cryptoLib from './services/e2ee/crypto'; +import crypto from './services/e2ee/crypto'; import FileApiDriverLocal from './file-api-driver-local'; import * as mimeUtils from './mime-utils'; @@ -136,7 +136,7 @@ function shimInit(options: ShimInitOptions = null) { shim.Geolocation = GeolocationNode; shim.FormData = require('form-data'); shim.sjclModule = require('./vendor/sjcl.js'); - shim.cryptoLib = cryptoLib; + shim.crypto = crypto; shim.electronBridge_ = options.electronBridge; shim.fsDriver = () => { diff --git a/packages/lib/shim.ts b/packages/lib/shim.ts index 1b5037695a8..f6155acf9d1 100644 --- a/packages/lib/shim.ts +++ b/packages/lib/shim.ts @@ -287,7 +287,7 @@ const shim = { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied sjclModule: null as any, - cryptoLib: null as Crypto, + crypto: null as Crypto, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied randomBytes: async (_count: number): Promise => { From 373ecac56cb3b598b3a5bc542acf8a913495de32 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 21 Jul 2024 19:12:14 +0800 Subject: [PATCH 09/60] Fix the type of digest --- packages/app-mobile/services/e2ee/crypto.ts | 16 +++------------ packages/lib/services/e2ee/crypto.ts | 22 +++++++++++---------- packages/lib/services/e2ee/types.ts | 11 ++++++++++- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index c186c8d83e5..4d0dcfd5bc2 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,6 +1,5 @@ -import { Crypto, CryptoBuffer } from '@joplin/lib/services/e2ee/types'; +import { Crypto, CryptoBuffer, HashAlgorithm } from '@joplin/lib/services/e2ee/types'; import QuickCrypto from 'react-native-quick-crypto'; -import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; const crypto: Crypto = { @@ -24,18 +23,9 @@ const crypto: Crypto = { }); }, - pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: string): Promise => { - const digestMap: { [key: string]: HashAlgorithm } = { - 'sha1': 'SHA-1', - 'sha224': 'SHA-224', - 'sha256': 'SHA-256', - 'sha384': 'SHA-384', - 'sha512': 'SHA-512', - 'ripemd160': 'RIPEMD-160', - }; - const digestAlgorithm: string = digestMap[digest.toLowerCase()] || digest; + pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: HashAlgorithm): Promise => { return new Promise((resolve, reject) => { - QuickCrypto.pbkdf2(password, salt, iterations, keylen, digestAlgorithm as HashAlgorithm, (error, result) => { + QuickCrypto.pbkdf2(password, salt, iterations, keylen, digest, (error, result) => { if (error) { reject(error); } else { diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 58ad85ae817..bdc628b5776 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,4 +1,4 @@ -import { Crypto } from './types'; +import { Crypto, HashAlgorithm } from './types'; import { promisify } from 'util'; import { getCiphers as nodeGetCiphers, getHashes as nodeGetHashes, @@ -6,6 +6,8 @@ import { pbkdf2 as nodePbkdf2, } from 'crypto'; +type NodeDigestNameMap = Record; + const crypto: Crypto = { getCiphers: (): string[] => { @@ -21,16 +23,16 @@ const crypto: Crypto = { return randomBytesAsync(size); }, - pbkdf2Raw: async (password: string, salt: Buffer, iterations: number, keylen: number, digest: string): Promise => { - const digestMap: { [key: string]: string } = { - 'sha-1': 'sha1', - 'sha-224': 'sha224', - 'sha-256': 'sha256', - 'sha-384': 'sha384', - 'sha-512': 'sha512', - 'ripemd-160': 'ripemd160', + pbkdf2Raw: async (password: string, salt: Buffer, iterations: number, keylen: number, digest: HashAlgorithm): Promise => { + const digestMap: NodeDigestNameMap = { + 'SHA-1': 'sha1', + 'SHA-224': 'sha224', + 'SHA-256': 'sha256', + 'SHA-384': 'sha384', + 'SHA-512': 'sha512', + 'RIPEMD-160': 'ripemd160', }; - const digestAlgorithm: string = digestMap[digest.toLowerCase()] || digest; + const digestAlgorithm = digestMap[digest]; const pbkdf2Async = promisify(nodePbkdf2); return pbkdf2Async(password, salt, iterations, keylen, digestAlgorithm); diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 986254ccbaf..085b8a47019 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -30,9 +30,18 @@ export interface Crypto { getCiphers(): string[]; getHashes(): string[]; randomBytes(size: number): Promise; - pbkdf2Raw(password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: string): Promise; + pbkdf2Raw(password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: HashAlgorithm): Promise; } export interface CryptoBuffer extends Uint8Array { toString(encoding?: BufferEncoding, start?: number, end?: number): string; } + +// From react-native-quick-crypto +export type HashAlgorithm = + | 'SHA-1' + | 'SHA-224' + | 'SHA-256' + | 'SHA-384' + | 'SHA-512' + | 'RIPEMD-160'; From d3bd657c3bcaaaedf4f2b718f7eb929b7e130e38 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 21 Jul 2024 19:49:40 +0800 Subject: [PATCH 10/60] Clean up Remove the return value type in the implementation Rename some variables --- packages/app-mobile/services/e2ee/crypto.ts | 8 ++++---- packages/lib/services/e2ee/crypto.ts | 14 +++++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 4d0dcfd5bc2..903d25848d8 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -3,15 +3,15 @@ import QuickCrypto from 'react-native-quick-crypto'; const crypto: Crypto = { - getCiphers: (): string[] => { + getCiphers: () => { return QuickCrypto.getCiphers(); }, - getHashes: (): string[] => { + getHashes: () => { return QuickCrypto.getHashes(); }, - randomBytes: async (size: number): Promise => { + randomBytes: async (size: number) => { return new Promise((resolve, reject) => { QuickCrypto.randomBytes(size, (error, result) => { if (error) { @@ -23,7 +23,7 @@ const crypto: Crypto = { }); }, - pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: HashAlgorithm): Promise => { + pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: HashAlgorithm) => { return new Promise((resolve, reject) => { QuickCrypto.pbkdf2(password, salt, iterations, keylen, digest, (error, result) => { if (error) { diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index bdc628b5776..66c31b98fce 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -10,21 +10,21 @@ type NodeDigestNameMap = Record; const crypto: Crypto = { - getCiphers: (): string[] => { + getCiphers: () => { return nodeGetCiphers(); }, - getHashes: (): string[] => { + getHashes: () => { return nodeGetHashes(); }, - randomBytes: async (size: number): Promise => { + randomBytes: async (size: number) => { const randomBytesAsync = promisify(nodeRandomBytes); return randomBytesAsync(size); }, - pbkdf2Raw: async (password: string, salt: Buffer, iterations: number, keylen: number, digest: HashAlgorithm): Promise => { - const digestMap: NodeDigestNameMap = { + pbkdf2Raw: async (password: string, salt: Buffer, iterations: number, keylen: number, digest: HashAlgorithm) => { + const nodeDigestNameMap: NodeDigestNameMap = { 'SHA-1': 'sha1', 'SHA-224': 'sha224', 'SHA-256': 'sha256', @@ -32,10 +32,10 @@ const crypto: Crypto = { 'SHA-512': 'sha512', 'RIPEMD-160': 'ripemd160', }; - const digestAlgorithm = digestMap[digest]; + const nodeDigestName = nodeDigestNameMap[digest]; const pbkdf2Async = promisify(nodePbkdf2); - return pbkdf2Async(password, salt, iterations, keylen, digestAlgorithm); + return pbkdf2Async(password, salt, iterations, keylen, nodeDigestName); }, }; From e4eb58c21df17d51000610cae52594835c9eafd6 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 23 Jul 2024 21:50:24 +0800 Subject: [PATCH 11/60] Rename --- packages/app-mobile/services/e2ee/crypto.ts | 4 ++-- packages/lib/services/e2ee/crypto.ts | 10 +++++----- packages/lib/services/e2ee/types.ts | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 903d25848d8..ca01303b9e5 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,4 +1,4 @@ -import { Crypto, CryptoBuffer, HashAlgorithm } from '@joplin/lib/services/e2ee/types'; +import { Crypto, CryptoBuffer, Digest } from '@joplin/lib/services/e2ee/types'; import QuickCrypto from 'react-native-quick-crypto'; const crypto: Crypto = { @@ -23,7 +23,7 @@ const crypto: Crypto = { }); }, - pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: HashAlgorithm) => { + pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest) => { return new Promise((resolve, reject) => { QuickCrypto.pbkdf2(password, salt, iterations, keylen, digest, (error, result) => { if (error) { diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 66c31b98fce..c0a5495af9f 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,4 +1,4 @@ -import { Crypto, HashAlgorithm } from './types'; +import { Crypto, Digest } from './types'; import { promisify } from 'util'; import { getCiphers as nodeGetCiphers, getHashes as nodeGetHashes, @@ -6,7 +6,7 @@ import { pbkdf2 as nodePbkdf2, } from 'crypto'; -type NodeDigestNameMap = Record; +type DigestNameMap = Record; const crypto: Crypto = { @@ -23,8 +23,8 @@ const crypto: Crypto = { return randomBytesAsync(size); }, - pbkdf2Raw: async (password: string, salt: Buffer, iterations: number, keylen: number, digest: HashAlgorithm) => { - const nodeDigestNameMap: NodeDigestNameMap = { + pbkdf2Raw: async (password: string, salt: Buffer, iterations: number, keylen: number, digest: Digest) => { + const digestNameMap: DigestNameMap = { 'SHA-1': 'sha1', 'SHA-224': 'sha224', 'SHA-256': 'sha256', @@ -32,7 +32,7 @@ const crypto: Crypto = { 'SHA-512': 'sha512', 'RIPEMD-160': 'ripemd160', }; - const nodeDigestName = nodeDigestNameMap[digest]; + const nodeDigestName = digestNameMap[digest]; const pbkdf2Async = promisify(nodePbkdf2); return pbkdf2Async(password, salt, iterations, keylen, nodeDigestName); diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 085b8a47019..1c8ce18766e 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -30,15 +30,15 @@ export interface Crypto { getCiphers(): string[]; getHashes(): string[]; randomBytes(size: number): Promise; - pbkdf2Raw(password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: HashAlgorithm): Promise; + pbkdf2Raw(password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest): Promise; } export interface CryptoBuffer extends Uint8Array { toString(encoding?: BufferEncoding, start?: number, end?: number): string; } -// From react-native-quick-crypto -export type HashAlgorithm = +// From react-native-quick-crypto.HashAlgorithm +export type Digest = | 'SHA-1' | 'SHA-224' | 'SHA-256' From f2ed47384a0b9c2a544a3367aacdb28dd6a1ec84 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 23 Jul 2024 22:13:00 +0800 Subject: [PATCH 12/60] Use enum for Digest --- .eslintrc.js | 2 +- packages/app-mobile/services/e2ee/crypto.ts | 15 ++++++++++++++- packages/lib/services/e2ee/crypto.ts | 14 +------------- packages/lib/services/e2ee/types.ts | 17 +++++++++-------- packages/tools/cspell/dictionary4.txt | 3 ++- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index e5bfc4bbb96..d5a66e8b7a2 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -272,7 +272,7 @@ module.exports = { selector: 'enumMember', format: null, 'filter': { - 'regex': '^(GET|POST|PUT|DELETE|PATCH|HEAD|SQLite|PostgreSQL|ASC|DESC|E2EE|OR|AND|UNION|INTERSECT|EXCLUSION|INCLUSION|EUR|GBP|USD|SJCL.*|iOS)$', + 'regex': '^(GET|POST|PUT|DELETE|PATCH|HEAD|SQLite|PostgreSQL|ASC|DESC|E2EE|OR|AND|UNION|INTERSECT|EXCLUSION|INCLUSION|EUR|GBP|USD|SJCL.*|iOS|sha1|sha224|sha256|sha384|sha512|ripemd160)$', 'match': true, }, }, diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index ca01303b9e5..1550426d3f5 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,5 +1,8 @@ import { Crypto, CryptoBuffer, Digest } from '@joplin/lib/services/e2ee/types'; import QuickCrypto from 'react-native-quick-crypto'; +import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; + +type DigestNameMap = Record; const crypto: Crypto = { @@ -24,8 +27,18 @@ const crypto: Crypto = { }, pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest) => { + const digestNameMap: DigestNameMap = { + sha1: 'SHA-1', + sha224: 'SHA-224', + sha256: 'SHA-256', + sha384: 'SHA-384', + sha512: 'SHA-512', + ripemd160: 'RIPEMD-160', + }; + const rnqcDigestName = digestNameMap[digest]; + return new Promise((resolve, reject) => { - QuickCrypto.pbkdf2(password, salt, iterations, keylen, digest, (error, result) => { + QuickCrypto.pbkdf2(password, salt, iterations, keylen, rnqcDigestName, (error, result) => { if (error) { reject(error); } else { diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index c0a5495af9f..72c7abff1d3 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -6,8 +6,6 @@ import { pbkdf2 as nodePbkdf2, } from 'crypto'; -type DigestNameMap = Record; - const crypto: Crypto = { getCiphers: () => { @@ -24,18 +22,8 @@ const crypto: Crypto = { }, pbkdf2Raw: async (password: string, salt: Buffer, iterations: number, keylen: number, digest: Digest) => { - const digestNameMap: DigestNameMap = { - 'SHA-1': 'sha1', - 'SHA-224': 'sha224', - 'SHA-256': 'sha256', - 'SHA-384': 'sha384', - 'SHA-512': 'sha512', - 'RIPEMD-160': 'ripemd160', - }; - const nodeDigestName = digestNameMap[digest]; - const pbkdf2Async = promisify(nodePbkdf2); - return pbkdf2Async(password, salt, iterations, keylen, nodeDigestName); + return pbkdf2Async(password, salt, iterations, keylen, digest); }, }; diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 1c8ce18766e..73c4188b156 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -37,11 +37,12 @@ export interface CryptoBuffer extends Uint8Array { toString(encoding?: BufferEncoding, start?: number, end?: number): string; } -// From react-native-quick-crypto.HashAlgorithm -export type Digest = - | 'SHA-1' - | 'SHA-224' - | 'SHA-256' - | 'SHA-384' - | 'SHA-512' - | 'RIPEMD-160'; +// From react-native-quick-crypto.HashAlgorithm, but use the hash name style in node:crypto +export enum Digest { + sha1 = 'sha1', + sha224 = 'sha224', + sha256 = 'sha256', + sha384 = 'sha384', + sha512 = 'sha512', + ripemd160 = 'ripemd160', +} diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index 6fba0022891..55cb3bd6aeb 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -111,4 +111,5 @@ Scaleway Inkscape Ionicon Stormlikes -ripemd \ No newline at end of file +ripemd +rnqc \ No newline at end of file From 48b54ecc54381eead9f5d588d6178f09896d9bd2 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 28 Jul 2024 23:00:13 +0800 Subject: [PATCH 13/60] Add a linter ignore rule for crypto terms --- .eslintrc.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index d5a66e8b7a2..223e7face3f 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -272,7 +272,15 @@ module.exports = { selector: 'enumMember', format: null, 'filter': { - 'regex': '^(GET|POST|PUT|DELETE|PATCH|HEAD|SQLite|PostgreSQL|ASC|DESC|E2EE|OR|AND|UNION|INTERSECT|EXCLUSION|INCLUSION|EUR|GBP|USD|SJCL.*|iOS|sha1|sha224|sha256|sha384|sha512|ripemd160)$', + 'regex': '^(GET|POST|PUT|DELETE|PATCH|HEAD|SQLite|PostgreSQL|ASC|DESC|E2EE|OR|AND|UNION|INTERSECT|EXCLUSION|INCLUSION|EUR|GBP|USD|SJCL.*|iOS)$', + 'match': true, + }, + }, + { + selector: 'enumMember', + format: null, + 'filter': { + 'regex': '^(sha1|sha224|sha256|sha384|sha512|ripemd160|AES_128_CCM|AES_192_CCM|AES_256_CCM|AES_128_GCM|AES_192_GCM|AES_256_GCM)$', 'match': true, }, }, From 70db3547d313a12cd1eadce97f838f33ee7de31e Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 28 Jul 2024 23:02:27 +0800 Subject: [PATCH 14/60] Add encryption core and string wrapper --- packages/app-mobile/services/e2ee/crypto.ts | 106 ++++++++++++++++++- packages/lib/services/e2ee/crypto.ts | 111 +++++++++++++++++++- packages/lib/services/e2ee/types.ts | 27 +++++ 3 files changed, 241 insertions(+), 3 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 1550426d3f5..4b04f3c26fa 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,6 +1,8 @@ -import { Crypto, CryptoBuffer, Digest } from '@joplin/lib/services/e2ee/types'; +import { _ } from '@joplin/lib/locale'; +import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult } from '@joplin/lib/services/e2ee/types'; import QuickCrypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; +import type { CipherCCMOptions, CipherCCM, DecipherCCM, CipherGCMOptions, CipherGCM, DecipherGCM } from 'crypto'; type DigestNameMap = Record; @@ -47,6 +49,108 @@ const crypto: Crypto = { }); }); }, + + encryptRaw: (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer | null, authTagLength: number, associatedData: CryptoBuffer | null) => { + if (associatedData === null) { + associatedData = Buffer.alloc(0); + } + + let cipher = null; + if (algorithm === CipherAlgorithm.AES_128_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_256_GCM) { + cipher = QuickCrypto.createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; + iv = iv || QuickCrypto.randomBytes(12); // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D + } else if (algorithm === CipherAlgorithm.AES_128_CCM || algorithm === CipherAlgorithm.AES_192_CCM || algorithm === CipherAlgorithm.AES_256_CCM) { + cipher = QuickCrypto.createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherCCMOptions) as CipherCCM; + iv = iv || QuickCrypto.randomBytes(13); // 13 is the maximum IV length for CCM mode - https://nodejs.org/docs/latest-v20.x/api/crypto.html#ccm-mode + } else { + throw new Error(_('Unknown cipher algorithm: %s', algorithm)); + } + + cipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); + + const encryptedData = Buffer.concat([cipher.update(data), cipher.final()]); + const authTag = cipher.getAuthTag(); + + return Buffer.concat([encryptedData, authTag]); + }, + + decryptRaw: (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { + if (associatedData === null) { + associatedData = Buffer.alloc(0); + } + + let decipher = null; + if (algorithm === CipherAlgorithm.AES_128_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_256_GCM) { + decipher = QuickCrypto.createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; + } else if (algorithm === CipherAlgorithm.AES_128_CCM || algorithm === CipherAlgorithm.AES_192_CCM || algorithm === CipherAlgorithm.AES_256_CCM) { + decipher = QuickCrypto.createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherCCMOptions) as DecipherCCM; + } else { + throw new Error(_('Unknown decipher algorithm: %s', algorithm)); + } + + const authTag = data.subarray(-authTagLength); + const encryptedData = data.subarray(0, data.byteLength - authTag.byteLength); + decipher.setAuthTag(authTag); + decipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); + + let decryptedData = null; + try { + decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]); + } catch (error) { + throw new Error(`Authentication failed! ${error}`); + } + + return decryptedData; + }, + + encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer | null, data: CryptoBuffer) => { + + // default encryption parameters + const cipherAlgorithm = CipherAlgorithm.AES_256_GCM; + const authTagLength = 16; + const digest = Digest.sha512; + const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes + + // default encryption parameters won't appear in result + const result: EncryptionResult = { + iter: iterationCount, + salt: '', + iv: '', + ct: '', // cipherText + }; + salt = salt || await crypto.randomBytes(32); // 256 bits + const iv = await crypto.randomBytes(12); // 96 bits + + const key = await crypto.pbkdf2Raw(password, salt, iterationCount, keySize, digest); + const encrypted = crypto.encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); + + result.salt = salt.toString('base64'); + result.iv = iv.toString('base64'); + result.ct = encrypted.toString('base64'); + + return result; + }, + + decrypt: async (password: string, data: EncryptionResult) => { + + // default encryption parameters + const cipherAlgorithm = data.algo || CipherAlgorithm.AES_256_GCM; + const authTagLength = data.ts || 16; + const digest = data.digest || Digest.sha512; + const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes + + const salt = Buffer.from(data.salt, 'base64'); + const iv = Buffer.from(data.iv, 'base64'); + + const key = await crypto.pbkdf2Raw(password, salt, data.iter, keySize, digest); + const decrypted = crypto.decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, null); + + return decrypted; + }, + + encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer | null, data: string, encoding: BufferEncoding) => { + return crypto.encrypt(password, iterationCount, salt, Buffer.from(data, encoding)); + }, }; export default crypto; diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 72c7abff1d3..cfa6d2714bb 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,9 +1,12 @@ -import { Crypto, Digest } from './types'; +import { _ } from '../../locale'; +import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult } from './types'; import { promisify } from 'util'; import { getCiphers as nodeGetCiphers, getHashes as nodeGetHashes, randomBytes as nodeRandomBytes, pbkdf2 as nodePbkdf2, + createCipheriv, createDecipheriv, + CipherCCMOptions, CipherCCM, DecipherCCM, CipherGCMOptions, CipherGCM, DecipherGCM, } from 'crypto'; const crypto: Crypto = { @@ -21,10 +24,114 @@ const crypto: Crypto = { return randomBytesAsync(size); }, - pbkdf2Raw: async (password: string, salt: Buffer, iterations: number, keylen: number, digest: Digest) => { + pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest) => { const pbkdf2Async = promisify(nodePbkdf2); return pbkdf2Async(password, salt, iterations, keylen, digest); }, + + + encryptRaw: (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer | null, authTagLength: number, associatedData: CryptoBuffer | null) => { + + if (associatedData === null) { + associatedData = Buffer.alloc(0); + } + + let cipher = null; + if (algorithm === CipherAlgorithm.AES_128_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_256_GCM) { + cipher = createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; + iv = iv || nodeRandomBytes(12); // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D + } else if (algorithm === CipherAlgorithm.AES_128_CCM || algorithm === CipherAlgorithm.AES_192_CCM || algorithm === CipherAlgorithm.AES_256_CCM) { + cipher = createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherCCMOptions) as CipherCCM; + iv = iv || nodeRandomBytes(13); // 13 is the maximum IV length for CCM mode - https://nodejs.org/docs/latest-v20.x/api/crypto.html#ccm-mode + } else { + throw new Error(_('Unknown cipher algorithm: %s', algorithm)); + } + + cipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); + + const encryptedData = Buffer.concat([cipher.update(data), cipher.final()]); + const authTag = cipher.getAuthTag(); + + return Buffer.concat([encryptedData, authTag]); + }, + + decryptRaw: (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { + if (associatedData === null) { + associatedData = Buffer.alloc(0); + } + + let decipher = null; + if (algorithm === CipherAlgorithm.AES_128_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_256_GCM) { + decipher = createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; + } else if (algorithm === CipherAlgorithm.AES_128_CCM || algorithm === CipherAlgorithm.AES_192_CCM || algorithm === CipherAlgorithm.AES_256_CCM) { + decipher = createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherCCMOptions) as DecipherCCM; + } else { + throw new Error(_('Unknown decipher algorithm: %s', algorithm)); + } + + const authTag = data.subarray(-authTagLength); + const encryptedData = data.subarray(0, data.byteLength - authTag.byteLength); + decipher.setAuthTag(authTag); + decipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); + + let decryptedData = null; + try { + decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]); + } catch (error) { + throw new Error(`Authentication failed! ${error}`); + } + + return decryptedData; + }, + + encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer | null, data: CryptoBuffer) => { + + // default encryption parameters + const cipherAlgorithm = CipherAlgorithm.AES_256_GCM; + const authTagLength = 16; + const digest = Digest.sha512; + const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes + + // default encryption parameters won't appear in result + const result: EncryptionResult = { + iter: iterationCount, + salt: '', + iv: '', + ct: '', // cipherText + }; + salt = salt || await crypto.randomBytes(32); // 256 bits + const iv = await crypto.randomBytes(12); // 96 bits + + const key = await crypto.pbkdf2Raw(password, salt, iterationCount, keySize, digest); + const encrypted = crypto.encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); + + result.salt = salt.toString('base64'); + result.iv = iv.toString('base64'); + result.ct = encrypted.toString('base64'); + + return result; + }, + + decrypt: async (password: string, data: EncryptionResult) => { + + // default encryption parameters + const cipherAlgorithm = data.algo || CipherAlgorithm.AES_256_GCM; + const authTagLength = data.ts || 16; + const digest = data.digest || Digest.sha512; + const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes + + const salt = Buffer.from(data.salt, 'base64'); + const iv = Buffer.from(data.iv, 'base64'); + + const key = await crypto.pbkdf2Raw(password, salt, data.iter, keySize, digest); + const decrypted = crypto.decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, null); + + return decrypted; + }, + + encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer | null, data: string, encoding: BufferEncoding) => { + return crypto.encrypt(password, iterationCount, salt, Buffer.from(data, encoding)); + }, }; export default crypto; diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 73c4188b156..a28be854879 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -27,10 +27,18 @@ export interface RSA { } export interface Crypto { + // low level functions getCiphers(): string[]; getHashes(): string[]; randomBytes(size: number): Promise; pbkdf2Raw(password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest): Promise; + encryptRaw(data: CryptoBuffer, algorithm: string, key: CryptoBuffer, iv: CryptoBuffer | null, authTagLength: number, associatedData: CryptoBuffer | null): Buffer; + decryptRaw(data: CryptoBuffer, algorithm: string, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null): Buffer; + + // convenient functions + encrypt(password: string, iterationCount: number, salt: CryptoBuffer | null, data: CryptoBuffer): Promise; + decrypt(password: string, data: EncryptionResult): Promise; + encryptString(password: string, iterationCount: number, salt: CryptoBuffer | null, data: string, encoding: BufferEncoding): Promise; } export interface CryptoBuffer extends Uint8Array { @@ -46,3 +54,22 @@ export enum Digest { sha512 = 'sha512', ripemd160 = 'ripemd160', } + +export enum CipherAlgorithm { + AES_128_CCM = 'aes-128-ccm', + AES_192_CCM = 'aes-192-ccm', + AES_256_CCM = 'aes-256-ccm', + AES_128_GCM = 'aes-128-gcm', + AES_192_GCM = 'aes-192-gcm', + AES_256_GCM = 'aes-256-gcm', +} + +export interface EncryptionResult { + algo?: CipherAlgorithm; // cipher algorithm, default: aes-256-gcm + ts?: number; // authTagLength in bytes, default: 16 + digest?: Digest; // digestAlgorithm, default: sha512 + iter: number; // iteration count + salt: string; // base64 encoded + iv: string; // base64 encoded + ct: string; // cipherText, base64 encoded +} From 52f82449698db9f6eabe6e149f547e7e0ccfcd11 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 4 Aug 2024 03:02:45 +0800 Subject: [PATCH 15/60] Clean up Add some comments Remove unused cipher algorithms (CCM mode) Move the most likely condition to the head of the chain --- .eslintrc.js | 2 +- packages/app-mobile/services/e2ee/crypto.ts | 15 +++++---------- packages/lib/services/e2ee/crypto.ts | 15 +++++---------- packages/lib/services/e2ee/types.ts | 3 --- 4 files changed, 11 insertions(+), 24 deletions(-) diff --git a/.eslintrc.js b/.eslintrc.js index 223e7face3f..f6f8f33a772 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -280,7 +280,7 @@ module.exports = { selector: 'enumMember', format: null, 'filter': { - 'regex': '^(sha1|sha224|sha256|sha384|sha512|ripemd160|AES_128_CCM|AES_192_CCM|AES_256_CCM|AES_128_GCM|AES_192_GCM|AES_256_GCM)$', + 'regex': '^(sha1|sha224|sha256|sha384|sha512|ripemd160|AES_128_GCM|AES_192_GCM|AES_256_GCM)$', 'match': true, }, }, diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 4b04f3c26fa..b0c02519ab4 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -2,7 +2,7 @@ import { _ } from '@joplin/lib/locale'; import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult } from '@joplin/lib/services/e2ee/types'; import QuickCrypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; -import type { CipherCCMOptions, CipherCCM, DecipherCCM, CipherGCMOptions, CipherGCM, DecipherGCM } from 'crypto'; +import type { CipherGCMOptions, CipherGCM, DecipherGCM } from 'crypto'; type DigestNameMap = Record; @@ -56,12 +56,9 @@ const crypto: Crypto = { } let cipher = null; - if (algorithm === CipherAlgorithm.AES_128_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_256_GCM) { + if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { cipher = QuickCrypto.createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; iv = iv || QuickCrypto.randomBytes(12); // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D - } else if (algorithm === CipherAlgorithm.AES_128_CCM || algorithm === CipherAlgorithm.AES_192_CCM || algorithm === CipherAlgorithm.AES_256_CCM) { - cipher = QuickCrypto.createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherCCMOptions) as CipherCCM; - iv = iv || QuickCrypto.randomBytes(13); // 13 is the maximum IV length for CCM mode - https://nodejs.org/docs/latest-v20.x/api/crypto.html#ccm-mode } else { throw new Error(_('Unknown cipher algorithm: %s', algorithm)); } @@ -80,10 +77,8 @@ const crypto: Crypto = { } let decipher = null; - if (algorithm === CipherAlgorithm.AES_128_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_256_GCM) { + if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { decipher = QuickCrypto.createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; - } else if (algorithm === CipherAlgorithm.AES_128_CCM || algorithm === CipherAlgorithm.AES_192_CCM || algorithm === CipherAlgorithm.AES_256_CCM) { - decipher = QuickCrypto.createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherCCMOptions) as DecipherCCM; } else { throw new Error(_('Unknown decipher algorithm: %s', algorithm)); } @@ -107,7 +102,7 @@ const crypto: Crypto = { // default encryption parameters const cipherAlgorithm = CipherAlgorithm.AES_256_GCM; - const authTagLength = 16; + const authTagLength = 16; // 128 bits const digest = Digest.sha512; const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes @@ -135,7 +130,7 @@ const crypto: Crypto = { // default encryption parameters const cipherAlgorithm = data.algo || CipherAlgorithm.AES_256_GCM; - const authTagLength = data.ts || 16; + const authTagLength = data.ts || 16; // 128 bits const digest = data.digest || Digest.sha512; const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index cfa6d2714bb..ba32e4a8dea 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -6,7 +6,7 @@ import { randomBytes as nodeRandomBytes, pbkdf2 as nodePbkdf2, createCipheriv, createDecipheriv, - CipherCCMOptions, CipherCCM, DecipherCCM, CipherGCMOptions, CipherGCM, DecipherGCM, + CipherGCMOptions, CipherGCM, DecipherGCM, } from 'crypto'; const crypto: Crypto = { @@ -37,12 +37,9 @@ const crypto: Crypto = { } let cipher = null; - if (algorithm === CipherAlgorithm.AES_128_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_256_GCM) { + if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { cipher = createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; iv = iv || nodeRandomBytes(12); // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D - } else if (algorithm === CipherAlgorithm.AES_128_CCM || algorithm === CipherAlgorithm.AES_192_CCM || algorithm === CipherAlgorithm.AES_256_CCM) { - cipher = createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherCCMOptions) as CipherCCM; - iv = iv || nodeRandomBytes(13); // 13 is the maximum IV length for CCM mode - https://nodejs.org/docs/latest-v20.x/api/crypto.html#ccm-mode } else { throw new Error(_('Unknown cipher algorithm: %s', algorithm)); } @@ -61,10 +58,8 @@ const crypto: Crypto = { } let decipher = null; - if (algorithm === CipherAlgorithm.AES_128_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_256_GCM) { + if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { decipher = createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; - } else if (algorithm === CipherAlgorithm.AES_128_CCM || algorithm === CipherAlgorithm.AES_192_CCM || algorithm === CipherAlgorithm.AES_256_CCM) { - decipher = createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherCCMOptions) as DecipherCCM; } else { throw new Error(_('Unknown decipher algorithm: %s', algorithm)); } @@ -88,7 +83,7 @@ const crypto: Crypto = { // default encryption parameters const cipherAlgorithm = CipherAlgorithm.AES_256_GCM; - const authTagLength = 16; + const authTagLength = 16; // 128 bits const digest = Digest.sha512; const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes @@ -116,7 +111,7 @@ const crypto: Crypto = { // default encryption parameters const cipherAlgorithm = data.algo || CipherAlgorithm.AES_256_GCM; - const authTagLength = data.ts || 16; + const authTagLength = data.ts || 16; // 128 bits const digest = data.digest || Digest.sha512; const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index a28be854879..ea9981aafa7 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -56,9 +56,6 @@ export enum Digest { } export enum CipherAlgorithm { - AES_128_CCM = 'aes-128-ccm', - AES_192_CCM = 'aes-192-ccm', - AES_256_CCM = 'aes-256-ccm', AES_128_GCM = 'aes-128-gcm', AES_192_GCM = 'aes-192-gcm', AES_256_GCM = 'aes-256-gcm', From 7b7595f88263b177b46b882c24078a64991aea1d Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 4 Aug 2024 03:05:26 +0800 Subject: [PATCH 16/60] Integrate into EncryptionService --- .../lib/services/e2ee/EncryptionService.ts | 68 +++++++++++++++---- packages/tools/cspell/dictionary4.txt | 3 +- 2 files changed, 56 insertions(+), 15 deletions(-) diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index cb73acffe57..7ae61bbeb94 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -42,6 +42,9 @@ export enum EncryptionMethod { SJCL1a = 5, Custom = 6, SJCL1b = 7, + KeyV1 = 8, + FileV1 = 9, + StringV1 = 10, } export interface EncryptOptions { @@ -75,6 +78,7 @@ export default class EncryptionService { private chunkSize_ = 5000; private decryptedMasterKeys_: Record = {}; public defaultEncryptionMethod_ = EncryptionMethod.SJCL1a; // public because used in tests + public defaultFileEncryptionMethod_ = EncryptionMethod.SJCL1a; // public because used in tests private defaultMasterKeyEncryptionMethod_ = EncryptionMethod.SJCL4; private headerTemplates_ = { @@ -107,6 +111,10 @@ export default class EncryptionService { return this.defaultEncryptionMethod_; } + public defaultFileEncryptionMethod() { + return this.defaultFileEncryptionMethod_; + } + public setActiveMasterKeyId(id: string) { setActiveMasterKeyId(id); } @@ -278,8 +286,9 @@ export default class EncryptionService { if (!key) throw new Error('Encryption key is required'); const sjcl = shim.sjclModule; + const crypto = shim.crypto; - const handlers: Record string> = { + const handlers: Record Promise> = { // 2020-01-23: Deprecated and no longer secure due to the use og OCB2 mode - do not use. [EncryptionMethod.SJCL]: () => { try { @@ -394,6 +403,26 @@ export default class EncryptionService { } }, + // New encryption method powered by native crypto libraries(node:crypto/react-native-quick-crypto). Using AES-256-GCM and pbkdf2 + // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem + // 2024-08: Set iteration count in pbkdf2 to 220000 as suggested by OWASP. https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 + [EncryptionMethod.KeyV1]: async () => { + return JSON.stringify(await crypto.encryptString(key, 220000, null, plainText, 'hex')); + }, + + // New encryption method powered by native crypto libraries(node:crypto/react-native-quick-crypto). Using AES-256-GCM and pbkdf2 + // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem + // The file content is base64 encoded. Decoding it before encryption to reduce the size overhead. + [EncryptionMethod.FileV1]: async () => { + return JSON.stringify(await crypto.encryptString(key, 200, null, plainText, 'base64')); + }, + + // New encryption method powered by native crypto libraries(node:crypto/react-native-quick-crypto). Using AES-256-GCM and pbkdf2 + // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem + [EncryptionMethod.StringV1]: async () => { + return JSON.stringify(await crypto.encryptString(key, 200, null, plainText, 'utf16le')); + }, + [EncryptionMethod.Custom]: () => { // This is handled elsewhere but as a sanity check, throw an exception throw new Error('Custom encryption method is not supported here'); @@ -408,19 +437,28 @@ export default class EncryptionService { if (!key) throw new Error('Encryption key is required'); const sjcl = shim.sjclModule; - if (!this.isValidEncryptionMethod(method)) throw new Error(`Unknown decryption method: ${method}`); - - try { - const output = sjcl.json.decrypt(key, cipherText); - - if (method === EncryptionMethod.SJCL1a || method === EncryptionMethod.SJCL1b) { - return unescape(output); - } else { - return output; + const crypto = shim.crypto; + if (method === EncryptionMethod.KeyV1) { + return (await crypto.decrypt(key, JSON.parse(cipherText))).toString('hex'); + } else if (method === EncryptionMethod.FileV1) { + return (await crypto.decrypt(key, JSON.parse(cipherText))).toString('base64'); + } else if (method === EncryptionMethod.StringV1) { + return (await crypto.decrypt(key, JSON.parse(cipherText))).toString('utf16le'); + } else if (this.isValidSjclEncryptionMethod(method)) { + try { + const output = sjcl.json.decrypt(key, cipherText); + + if (method === EncryptionMethod.SJCL1a || method === EncryptionMethod.SJCL1b) { + return unescape(output); + } else { + return output; + } + } catch (error) { + // SJCL returns a string as error which means stack trace is missing so convert to an error object here + throw new Error(error.message); } - } catch (error) { - // SJCL returns a string as error which means stack trace is missing so convert to an error object here - throw new Error(error.message); + } else { + throw new Error(`Unknown decryption method: ${method}`); } } @@ -560,6 +598,8 @@ export default class EncryptionService { } public async encryptFile(srcPath: string, destPath: string, options: EncryptOptions = null) { + options = { encryptionMethod: this.defaultFileEncryptionMethod(), ...options }; + let source = await this.fileReader_(srcPath, 'base64'); let destination = await this.fileWriter_(destPath, 'ascii'); @@ -680,7 +720,7 @@ export default class EncryptionService { return output; } - public isValidEncryptionMethod(method: EncryptionMethod) { + public isValidSjclEncryptionMethod(method: EncryptionMethod) { return [EncryptionMethod.SJCL, EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.SJCL2, EncryptionMethod.SJCL3, EncryptionMethod.SJCL4].indexOf(method) >= 0; } diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index 6c4a74aa717..ad3bcc7b3bc 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -113,4 +113,5 @@ Ionicon Stormlikes BYTV ripemd -rnqc \ No newline at end of file +rnqc +owasp \ No newline at end of file From 26faf260cf737a701dc68c016e0cd5df661446b0 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 4 Aug 2024 03:06:10 +0800 Subject: [PATCH 17/60] Add tests --- .eslintignore | 2 + .gitignore | 2 + packages/app-mobile/root.tsx | 2 + .../services/e2ee/EncryptionService.test.ts | 53 +++++--- packages/lib/services/e2ee/crypto.test.ts | 19 +++ packages/lib/services/e2ee/cryptoTestUtils.ts | 123 ++++++++++++++++++ 6 files changed, 181 insertions(+), 20 deletions(-) create mode 100644 packages/lib/services/e2ee/crypto.test.ts create mode 100644 packages/lib/services/e2ee/cryptoTestUtils.ts diff --git a/.eslintignore b/.eslintignore index 5de61c3d407..0fd3ae17e0e 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1035,7 +1035,9 @@ packages/lib/services/debug/populateDatabase.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js packages/lib/services/e2ee/RSA.node.js +packages/lib/services/e2ee/crypto.test.js packages/lib/services/e2ee/crypto.js +packages/lib/services/e2ee/cryptoTestUtils.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js packages/lib/services/e2ee/ppkTestUtils.js diff --git a/.gitignore b/.gitignore index 73e2d5b473f..4dcf77b473c 100644 --- a/.gitignore +++ b/.gitignore @@ -1014,7 +1014,9 @@ packages/lib/services/debug/populateDatabase.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js packages/lib/services/e2ee/RSA.node.js +packages/lib/services/e2ee/crypto.test.js packages/lib/services/e2ee/crypto.js +packages/lib/services/e2ee/cryptoTestUtils.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js packages/lib/services/e2ee/ppkTestUtils.js diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index 431ccab1a21..d8af1e1e975 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -111,6 +111,7 @@ import SyncTargetNone from '@joplin/lib/SyncTargetNone'; import { setRSA } from '@joplin/lib/services/e2ee/ppk'; import RSA from './services/e2ee/RSA.react-native'; import { runIntegrationTests as runRsaIntegrationTests } from '@joplin/lib/services/e2ee/ppkTestUtils'; +import { runIntegrationTests as runCryptoIntegrationTests } from '@joplin/lib/services/e2ee/cryptoTestUtils'; import { Theme, ThemeAppearance } from '@joplin/lib/themes/type'; import ProfileSwitcher from './components/ProfileSwitcher/ProfileSwitcher'; import ProfileEditor from './components/ProfileSwitcher/ProfileEditor'; @@ -827,6 +828,7 @@ async function initialize(dispatch: Dispatch) { // ---------------------------------------------------------------------------- if (Setting.value('env') === 'dev') { await runRsaIntegrationTests(); + await runCryptoIntegrationTests(); await runOnDeviceFsDriverTests(); } diff --git a/packages/lib/services/e2ee/EncryptionService.test.ts b/packages/lib/services/e2ee/EncryptionService.test.ts index 628939fb8fb..8023215b7d9 100644 --- a/packages/lib/services/e2ee/EncryptionService.test.ts +++ b/packages/lib/services/e2ee/EncryptionService.test.ts @@ -96,25 +96,31 @@ describe('services_EncryptionService', () => { })); it('should not require a checksum for new master keys', (async () => { - const masterKey = await service.generateMasterKey('123456', { - encryptionMethod: EncryptionMethod.SJCL4, - }); + const masterKeyEncryptionMethodList = [EncryptionMethod.SJCL4, EncryptionMethod.KeyV1]; + for (const masterKeyEncryptionMethod of masterKeyEncryptionMethodList) { + const masterKey = await service.generateMasterKey('123456', { + encryptionMethod: masterKeyEncryptionMethod, + }); - expect(!masterKey.checksum).toBe(true); - expect(!!masterKey.content).toBe(true); + expect(!masterKey.checksum).toBe(true); + expect(!!masterKey.content).toBe(true); - const decryptedMasterKey = await service.decryptMasterKeyContent(masterKey, '123456'); - expect(decryptedMasterKey.length).toBe(512); + const decryptedMasterKey = await service.decryptMasterKeyContent(masterKey, '123456'); + expect(decryptedMasterKey.length).toBe(512); + } })); it('should throw an error if master key decryption fails', (async () => { - const masterKey = await service.generateMasterKey('123456', { - encryptionMethod: EncryptionMethod.SJCL4, - }); + const masterKeyEncryptionMethodList = [EncryptionMethod.SJCL4, EncryptionMethod.KeyV1]; + for (const masterKeyEncryptionMethod of masterKeyEncryptionMethodList) { + const masterKey = await service.generateMasterKey('123456', { + encryptionMethod: masterKeyEncryptionMethod, + }); - const hasThrown = await checkThrowAsync(async () => await service.decryptMasterKeyContent(masterKey, 'wrong')); + const hasThrown = await checkThrowAsync(async () => await service.decryptMasterKeyContent(masterKey, 'wrong')); - expect(hasThrown).toBe(true); + expect(hasThrown).toBe(true); + } })); it('should return the master keys that need an upgrade', (async () => { @@ -252,11 +258,15 @@ describe('services_EncryptionService', () => { const encryptedPath = `${Setting.value('tempDir')}/photo.crypted`; const decryptedPath = `${Setting.value('tempDir')}/photo.jpg`; - await service.encryptFile(sourcePath, encryptedPath); - await service.decryptFile(encryptedPath, decryptedPath); + const fileEncryptionMethodList = [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.FileV1, EncryptionMethod.StringV1]; + for (const fileEncryptionMethod of fileEncryptionMethodList) { + service.defaultFileEncryptionMethod_ = fileEncryptionMethod; + await service.encryptFile(sourcePath, encryptedPath); + await service.decryptFile(encryptedPath, decryptedPath); - expect(fileContentEqual(sourcePath, encryptedPath)).toBe(false); - expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true); + expect(fileContentEqual(sourcePath, encryptedPath)).toBe(false); + expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true); + } })); it('should encrypt invalid UTF-8 data', (async () => { @@ -271,10 +281,13 @@ describe('services_EncryptionService', () => { expect(hasThrown).toBe(true); // Now check that the new one fixes the problem - service.defaultEncryptionMethod_ = EncryptionMethod.SJCL1a; - const cipherText = await service.encryptString('🐶🐶🐶'.substr(0, 5)); - const plainText = await service.decryptString(cipherText); - expect(plainText).toBe('🐶🐶🐶'.substr(0, 5)); + const newEncryptionMethodList = [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.StringV1]; + for (const newEncryptionMethod of newEncryptionMethodList) { + service.defaultEncryptionMethod_ = newEncryptionMethod; + const cipherText = await service.encryptString('🐶🐶🐶'.substr(0, 5)); + const plainText = await service.decryptString(cipherText); + expect(plainText).toBe('🐶🐶🐶'.substr(0, 5)); + } })); it('should check if a master key is loaded', (async () => { diff --git a/packages/lib/services/e2ee/crypto.test.ts b/packages/lib/services/e2ee/crypto.test.ts new file mode 100644 index 00000000000..86935428f63 --- /dev/null +++ b/packages/lib/services/e2ee/crypto.test.ts @@ -0,0 +1,19 @@ +import { afterAllCleanUp, expectNotThrow, setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils'; +import { runIntegrationTests } from './cryptoTestUtils'; + +describe('e2ee/ppk', () => { + + beforeEach(async () => { + await setupDatabaseAndSynchronizer(1); + await switchClient(1); + }); + + afterAll(async () => { + await afterAllCleanUp(); + }); + + it('should decrypt and encrypt data from different devices', (async () => { + await expectNotThrow(async () => runIntegrationTests(true)); + })); + +}); diff --git a/packages/lib/services/e2ee/cryptoTestUtils.ts b/packages/lib/services/e2ee/cryptoTestUtils.ts new file mode 100644 index 00000000000..9fe476ab59c --- /dev/null +++ b/packages/lib/services/e2ee/cryptoTestUtils.ts @@ -0,0 +1,123 @@ +import { EncryptionMethod } from './EncryptionService'; +import EncryptionService from './EncryptionService'; + +interface DecryptTestData { + method: EncryptionMethod; + password: string; + plaintext: string; + ciphertext: string; +} + +const serviceInstance = EncryptionService.instance(); + +// This is convenient to quickly generate some data to verify for example that +// react-native-quick-crypto and node:crypto can decrypt the same data. +export async function createDecryptTestData() { + const method = EncryptionMethod.StringV1; + const password = 'just testing'; + const plaintext = '中文にっぽんご한국어😀\uD83D\0\r\nenglish01234567890'; + const ciphertext = await serviceInstance.encrypt(method, password, plaintext); + + return { + method: method, + password: password, + plaintext: plaintext, + ciphertext: ciphertext, + }; +} + +interface CheckTestDataOptions { + throwOnError?: boolean; + silent?: boolean; + testLabel?: string; +} + +export async function checkDecryptTestData(data: DecryptTestData, options: CheckTestDataOptions = null) { + options = { + throwOnError: false, + silent: false, + ...options, + }; + + // Verify that the ciphertext decrypted on this device and producing the same plaintext. + const messages: string[] = []; + let hasError = false; + + try { + const decrypted = await EncryptionService.instance().decrypt(data.method, data.password, data.ciphertext); + if (decrypted !== data.plaintext) { + messages.push('Crypto Tests: Data could not be decrypted'); + messages.push('Crypto Tests: Expected:', data.plaintext); + messages.push('Crypto Tests: Got:', decrypted); + hasError = true; + } else { + messages.push('Crypto Tests: Data could be decrypted'); + } + } catch (error) { + hasError = true; + messages.push(`Crypto Tests: Failed to decrypt data: Error: ${error}`); + } + + if (hasError && options.throwOnError) { + const label = options.testLabel ? ` (test ${options.testLabel})` : ''; + throw new Error(`Testing Crypto failed${label}: \n${messages.join('\n')}`); + } else { + for (const msg of messages) { + if (hasError) { + console.warn(msg); + } else { + // eslint-disable-next-line no-console + if (!options.silent) console.info(msg); + } + } + } +} + +// cSpell:disable + +// Data generated on desktop, using node:crypto in packages/lib/services/e2ee/crypto.ts +const decryptTestData: Record = { + shortString: { + method: EncryptionMethod.StringV1, + password: '4BfJl8YbM,nXx.LVgs!AzkWWA]', + plaintext: '中文にっぽんご한국어😀\uD83D\0\r\nenglish01234567890', + ciphertext: + '{"iter":200,"salt":"EUChaTc2I6nIdJPSVCe9YIKUSURd/W8jjX4yzcNvAus=","iv":"RPb1xewALYrPe0hM","ct":"HFEyN3KMsqf/EY2yTTKk0sBm34byHnmJVoL20v5GCuBdCJBl3w7NWoxKAcTD3D1jY3Rt3Brn1mJWykjJMDmPj6tjEyU8ZUS84TuLIW7MTcFOx5xM"}', + }, + hexKey: { + method: EncryptionMethod.KeyV1, + password: '4BfJl8YbM,nXx.LVgs!AzkWWA]', + plaintext: 'ea5d8dd43823a812c9a64f4eb09b57e1aa2dbfcda50bf9e416dcd1f8c569a6462f7f61861c3ccda0c7c16bf4192ed9c7ecaf3e1248517bd6f397d1080d2632fc69e1ead59a398a07e7478c8d90142745c0dce39c2105b6d34117424697de03879caa3b6410325f14dc5755b69efa944eb83f06970d04444e83fe054576af20576c0f3a5dc23e0d6dcfa3e84ec05c21139070c0809bd2bdc86a7782368c9d99eb072c858c61ec8926136e6e50dfd57b7e8e0084ad08b2d984db0436f50433cab44b3c462ccd22a8567c8ff86675fff618b11526030f09f735646a9f189f54ba5485d069ee25ac468ec0a873c1223eed34962bd219972020cc147041d4b00a3ab8', + ciphertext: '{"iter":220000,"salt":"0J0KrV2DUWbbY/0T/Do8b2E0cQ2gOGUeVay/uRHnjhY=","iv":"4Z+V0Lh0ID5U7lzN","ct":"X+PZIHUmc3rCIq77fU3MNth2o2GrGd0jgl7P6xwXpK1qhkr/XLVZ5nx5Yo1DVMYivDeoVrPVmpZ9enQ3P667LtUjD2i/qJU0zxOcqdb2ZkAQssRPf7r+8yvhp6dK5d8zKB8gEKJK+z51vWyoEd9CE+NCUzPiNfrStRTuqDwxxXYiBE1gB4lzFWK1GLvfS4998g6rOjonn+SzuIc0JgSLrx9xoP4aVzolGAVRU6C9Hl37hSIudBUuUDskQcJao4BUQ25I5mQ27c9vISonHF2mqt0MbToVMTvYqDZFX48s9zdHlJ52fbgJnTlPD76OgbZEYeCMgPKge2Ic61nD+EYoacMWjS/+WncMbeVH3+5F0gg="}', + }, + base64File: { + method: EncryptionMethod.FileV1, + password: '4BfJl8YbM,nXx.LVgs!AzkWWA]', + plaintext: '5Lit5paH44Gr44Gj44G944KT44GU7ZWc6rWt7Ja08J+YgO+/vQANCmVuZ2xpc2gwMTIzNDU2Nzg5MA==', + ciphertext: '{"iter":200,"salt":"bTlrtQiEzgOY2RRzolmGI360+/j4ZIqn5U5dstwDWsI=","iv":"8zWMrp+ebcVyZB2N","ct":"bNSj8GHcTflaq2WkXoJstvyDgoBZJbojVAahdyRRU3Kn+WzVoeKZTTmyH+pfLsKkkAqxpGUFpZWZM8eFXmxKN83jQBPjrJCdyFY="}', + }, +}; + +// cSpell:enable + +// This can be used to run integration tests directly on device. It will throw +// an error if something cannot be decrypted, or else print info messages. +export const runIntegrationTests = async (silent = false) => { + const log = (s: string) => { + if (silent) return; + // eslint-disable-next-line no-console + console.info(s); + }; + + log('Crypto Tests: Running integration tests...'); + + log('Crypto Tests: Decrypting using known data...'); + for (const testLabel in decryptTestData) { + log(`Crypto Tests: Running decrypt test data case ${testLabel}...`); + await checkDecryptTestData(decryptTestData[testLabel], { silent, testLabel, throwOnError: true }); + } + + log('Crypto Tests: Decrypting using local data...'); + const newData = await createDecryptTestData(); + await checkDecryptTestData(newData, { silent, throwOnError: true }); +}; From 63f6ce96bd074004812a493b9d567c1fd2eca3c4 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 4 Aug 2024 03:06:45 +0800 Subject: [PATCH 18/60] Enable native encryption --- packages/lib/services/e2ee/EncryptionService.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index 7ae61bbeb94..5826716fc4d 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -75,11 +75,11 @@ export default class EncryptionService { // // So making the block 10 times smaller make it 100 times faster! So for now using 5KB. This can be // changed easily since the chunk size is incorporated into the encrypted data. - private chunkSize_ = 5000; + private chunkSize_ = 65536; private decryptedMasterKeys_: Record = {}; - public defaultEncryptionMethod_ = EncryptionMethod.SJCL1a; // public because used in tests - public defaultFileEncryptionMethod_ = EncryptionMethod.SJCL1a; // public because used in tests - private defaultMasterKeyEncryptionMethod_ = EncryptionMethod.SJCL4; + public defaultEncryptionMethod_ = EncryptionMethod.StringV1; // public because used in tests + public defaultFileEncryptionMethod_ = EncryptionMethod.FileV1; // public because used in tests + private defaultMasterKeyEncryptionMethod_ = EncryptionMethod.KeyV1; private headerTemplates_ = { // Template version 1 From 9d39eb79049fd9cfa57358d46f9fb6cbd03de22b Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 4 Aug 2024 18:47:55 +0800 Subject: [PATCH 19/60] Tuning parameters --- packages/lib/services/e2ee/EncryptionService.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index 5826716fc4d..d11e1eefb61 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -414,13 +414,13 @@ export default class EncryptionService { // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem // The file content is base64 encoded. Decoding it before encryption to reduce the size overhead. [EncryptionMethod.FileV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, 200, null, plainText, 'base64')); + return JSON.stringify(await crypto.encryptString(key, 5, null, plainText, 'base64')); }, // New encryption method powered by native crypto libraries(node:crypto/react-native-quick-crypto). Using AES-256-GCM and pbkdf2 // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem [EncryptionMethod.StringV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, 200, null, plainText, 'utf16le')); + return JSON.stringify(await crypto.encryptString(key, 5, null, plainText, 'utf16le')); }, [EncryptionMethod.Custom]: () => { From bfc3ecb926233787e1cc879d79304b98436777fe Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 4 Aug 2024 22:11:55 +0800 Subject: [PATCH 20/60] Add performance test for crypto --- packages/app-desktop/app.ts | 6 +- .../lib/services/e2ee/EncryptionService.ts | 2 +- packages/lib/services/e2ee/cryptoTestUtils.ts | 169 +++++++++++++++++- 3 files changed, 172 insertions(+), 5 deletions(-) diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 9c7a054f46d..1ccb266448d 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -72,7 +72,7 @@ import SearchEngine from '@joplin/lib/services/search/SearchEngine'; import { PackageInfo } from '@joplin/lib/versionInfo'; import { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols'; import { refreshFolders } from '@joplin/lib/folders-screen-utils'; - +import { runIntegrationTests as runCryptoIntegrationTests } from '@joplin/lib/services/e2ee/cryptoTestUtils'; const pluginClasses = [ require('./plugins/GotoAnything').default, ]; @@ -707,6 +707,10 @@ class Application extends BaseApplication { // await runIntegrationTests(); + if (Setting.value('env') === 'dev') { + await runCryptoIntegrationTests(); + } + return null; } diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index d11e1eefb61..11a06fd23f4 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -75,7 +75,7 @@ export default class EncryptionService { // // So making the block 10 times smaller make it 100 times faster! So for now using 5KB. This can be // changed easily since the chunk size is incorporated into the encrypted data. - private chunkSize_ = 65536; + public chunkSize_ = 65536; private decryptedMasterKeys_: Record = {}; public defaultEncryptionMethod_ = EncryptionMethod.StringV1; // public because used in tests public defaultFileEncryptionMethod_ = EncryptionMethod.FileV1; // public because used in tests diff --git a/packages/lib/services/e2ee/cryptoTestUtils.ts b/packages/lib/services/e2ee/cryptoTestUtils.ts index 9fe476ab59c..847d3827c6b 100644 --- a/packages/lib/services/e2ee/cryptoTestUtils.ts +++ b/packages/lib/services/e2ee/cryptoTestUtils.ts @@ -1,5 +1,11 @@ import { EncryptionMethod } from './EncryptionService'; import EncryptionService from './EncryptionService'; +import Folder from '../../models/Folder'; +import Note from '../../models/Note'; +import MasterKey from '../../models/MasterKey'; +import Setting from '../../models/Setting'; +import shim from '../..//shim'; +import { getEncryptionEnabled, setEncryptionEnabled } from '../synchronizer/syncInfoUtils'; interface DecryptTestData { method: EncryptionMethod; @@ -73,6 +79,127 @@ export async function checkDecryptTestData(data: DecryptTestData, options: Check } } +export async function testStringPerformance(chunkSize: number, method: EncryptionMethod, dataSize: number, count: number, options: CheckTestDataOptions = null) { + options = { + throwOnError: false, + silent: false, + ...options, + }; + + // Verify that the ciphertext decrypted on this device and producing the same plaintext. + const messages: string[] = []; + let hasError = false; + + try { + serviceInstance.defaultEncryptionMethod_ = method; + serviceInstance.chunkSize_ = chunkSize; + let masterKey = await serviceInstance.generateMasterKey('123456'); + masterKey = await MasterKey.save(masterKey); + await serviceInstance.loadMasterKey(masterKey, '123456', true); + + const crypto = shim.crypto; + + const content = (await crypto.randomBytes(dataSize / 2)).toString('hex'); + const folder = await Folder.save({ title: 'folder' }); + const note = await Note.save({ title: 'encrypted note', body: content, parent_id: folder.id }); + + let encryptTime = 0.0; + let decryptTime = 0.0; + for (let i = 0; i < count; i++) { + const tick1 = performance.now(); + const serialized = await Note.serializeForSync(note); + const tick2 = performance.now(); + const deserialized = await Note.unserialize(serialized); + const decryptedNote = await Note.decrypt(deserialized); + const tick3 = performance.now(); + (decryptedNote.title === note.title); + encryptTime += tick2 - tick1; + decryptTime += tick3 - tick2; + } + + messages.push(`Crypto Tests: testStringPerformance(): chunkSize: ${chunkSize}, method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`); + + } catch (error) { + hasError = true; + messages.push(`Crypto Tests: testStringPerformance() failed. Error: ${error}`); + } + + if (hasError && options.throwOnError) { + const label = options.testLabel ? ` (test ${options.testLabel})` : ''; + throw new Error(`Testing Crypto failed${label}: \n${messages.join('\n')}`); + } else { + for (const msg of messages) { + if (hasError) { + console.warn(msg); + } else { + // eslint-disable-next-line no-console + if (!options.silent) console.info(msg); + } + } + } +} + +export async function testFilePerformance(chunkSize: number, method: EncryptionMethod, dataSize: number, count: number, options: CheckTestDataOptions = null) { + options = { + throwOnError: false, + silent: false, + ...options, + }; + + // Verify that the ciphertext decrypted on this device and producing the same plaintext. + const messages: string[] = []; + let hasError = false; + + try { + serviceInstance.defaultFileEncryptionMethod_ = method; + serviceInstance.chunkSize_ = chunkSize; + let masterKey = await serviceInstance.generateMasterKey('123456'); + masterKey = await MasterKey.save(masterKey); + await serviceInstance.loadMasterKey(masterKey, '123456', true); + + const fsDriver = shim.fsDriver(); + const crypto = shim.crypto; + + const sourcePath = `${Setting.value('tempDir')}/testData.txt`; + const encryptedPath = `${Setting.value('tempDir')}/testData.crypted`; + const decryptedPath = `${Setting.value('tempDir')}/testData.decrypted`; + await fsDriver.writeFile(sourcePath, ''); + await fsDriver.appendFile(sourcePath, (await crypto.randomBytes(dataSize)).toString('base64'), 'base64'); + + let encryptTime = 0.0; + let decryptTime = 0.0; + for (let i = 0; i < count; i++) { + const tick1 = performance.now(); + await serviceInstance.encryptFile(sourcePath, encryptedPath); + const tick2 = performance.now(); + await serviceInstance.decryptFile(encryptedPath, decryptedPath); + const tick3 = performance.now(); + encryptTime += tick2 - tick1; + decryptTime += tick3 - tick2; + } + + messages.push(`Crypto Tests: testFilePerformance(): chunkSize: ${chunkSize}, method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`); + + } catch (error) { + hasError = true; + messages.push(`Crypto Tests: testFilePerformance() failed. Error: ${error}`); + } + + if (hasError && options.throwOnError) { + const label = options.testLabel ? ` (test ${options.testLabel})` : ''; + throw new Error(`Testing Crypto failed${label}: \n${messages.join('\n')}`); + } else { + for (const msg of messages) { + if (hasError) { + console.warn(msg); + } else { + // eslint-disable-next-line no-console + if (!options.silent) console.info(msg); + } + } + } +} + // cSpell:disable // Data generated on desktop, using node:crypto in packages/lib/services/e2ee/crypto.ts @@ -81,8 +208,7 @@ const decryptTestData: Record = { method: EncryptionMethod.StringV1, password: '4BfJl8YbM,nXx.LVgs!AzkWWA]', plaintext: '中文にっぽんご한국어😀\uD83D\0\r\nenglish01234567890', - ciphertext: - '{"iter":200,"salt":"EUChaTc2I6nIdJPSVCe9YIKUSURd/W8jjX4yzcNvAus=","iv":"RPb1xewALYrPe0hM","ct":"HFEyN3KMsqf/EY2yTTKk0sBm34byHnmJVoL20v5GCuBdCJBl3w7NWoxKAcTD3D1jY3Rt3Brn1mJWykjJMDmPj6tjEyU8ZUS84TuLIW7MTcFOx5xM"}', + ciphertext: '{"iter":200,"salt":"EUChaTc2I6nIdJPSVCe9YIKUSURd/W8jjX4yzcNvAus=","iv":"RPb1xewALYrPe0hM","ct":"HFEyN3KMsqf/EY2yTTKk0sBm34byHnmJVoL20v5GCuBdCJBl3w7NWoxKAcTD3D1jY3Rt3Brn1mJWykjJMDmPj6tjEyU8ZUS84TuLIW7MTcFOx5xM"}', }, hexKey: { method: EncryptionMethod.KeyV1, @@ -102,7 +228,7 @@ const decryptTestData: Record = { // This can be used to run integration tests directly on device. It will throw // an error if something cannot be decrypted, or else print info messages. -export const runIntegrationTests = async (silent = false) => { +export const runIntegrationTests = async (silent = false, testPerformance = false) => { const log = (s: string) => { if (silent) return; // eslint-disable-next-line no-console @@ -110,6 +236,8 @@ export const runIntegrationTests = async (silent = false) => { }; log('Crypto Tests: Running integration tests...'); + const encryptionEnabled = getEncryptionEnabled(); + setEncryptionEnabled(true); log('Crypto Tests: Decrypting using known data...'); for (const testLabel in decryptTestData) { @@ -120,4 +248,39 @@ export const runIntegrationTests = async (silent = false) => { log('Crypto Tests: Decrypting using local data...'); const newData = await createDecryptTestData(); await checkDecryptTestData(newData, { silent, throwOnError: true }); + + // The performance test is very slow so it is disabled by default. + if (testPerformance) { + log('Crypto Tests: Testing performance...'); + if (shim.mobilePlatform() === '') { + await testStringPerformance(65536, EncryptionMethod.StringV1, 100, 1000); + await testStringPerformance(65536, EncryptionMethod.StringV1, 1000000, 10); + await testStringPerformance(65536, EncryptionMethod.StringV1, 5000000, 10); + await testStringPerformance(5000, EncryptionMethod.SJCL1a, 100, 1000); + await testStringPerformance(5000, EncryptionMethod.SJCL1a, 1000000, 10); + await testStringPerformance(5000, EncryptionMethod.SJCL1a, 5000000, 10); + await testFilePerformance(65536, EncryptionMethod.FileV1, 100, 1000); + await testFilePerformance(65536, EncryptionMethod.FileV1, 1000000, 3); + await testFilePerformance(65536, EncryptionMethod.FileV1, 5000000, 3); + await testFilePerformance(5000, EncryptionMethod.SJCL1a, 100, 1000); + await testFilePerformance(5000, EncryptionMethod.SJCL1a, 1000000, 3); + await testFilePerformance(5000, EncryptionMethod.SJCL1a, 5000000, 3); + } else { + await testStringPerformance(65536, EncryptionMethod.StringV1, 100, 100); + await testStringPerformance(65536, EncryptionMethod.StringV1, 500000, 3); + await testStringPerformance(65536, EncryptionMethod.StringV1, 1000000, 3); + await testStringPerformance(5000, EncryptionMethod.SJCL1a, 100, 100); + await testStringPerformance(5000, EncryptionMethod.SJCL1a, 500000, 3); + await testStringPerformance(5000, EncryptionMethod.SJCL1a, 1000000, 3); + await testFilePerformance(65536, EncryptionMethod.FileV1, 100, 100); + await testFilePerformance(65536, EncryptionMethod.FileV1, 100000, 3); + await testFilePerformance(65536, EncryptionMethod.FileV1, 500000, 3); + await testFilePerformance(5000, EncryptionMethod.SJCL1a, 100, 100); + await testFilePerformance(5000, EncryptionMethod.SJCL1a, 100000, 3); + await testFilePerformance(5000, EncryptionMethod.SJCL1a, 500000, 3); + } + + } + + setEncryptionEnabled(encryptionEnabled); }; From 73b4c5af8321279d85aed25f5ae207aa22b3b6cd Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 6 Aug 2024 22:34:55 +0800 Subject: [PATCH 21/60] Resolve some review suggestions --- packages/app-desktop/app.ts | 5 ----- packages/app-mobile/root.tsx | 2 +- packages/lib/services/e2ee/EncryptionService.ts | 2 +- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 1ccb266448d..81a96444a5d 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -72,7 +72,6 @@ import SearchEngine from '@joplin/lib/services/search/SearchEngine'; import { PackageInfo } from '@joplin/lib/versionInfo'; import { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols'; import { refreshFolders } from '@joplin/lib/folders-screen-utils'; -import { runIntegrationTests as runCryptoIntegrationTests } from '@joplin/lib/services/e2ee/cryptoTestUtils'; const pluginClasses = [ require('./plugins/GotoAnything').default, ]; @@ -707,10 +706,6 @@ class Application extends BaseApplication { // await runIntegrationTests(); - if (Setting.value('env') === 'dev') { - await runCryptoIntegrationTests(); - } - return null; } diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index e96171f6354..06708999c6d 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -849,7 +849,7 @@ async function initialize(dispatch: Dispatch) { await runRsaIntegrationTests(); await runCryptoIntegrationTests(); } else { - logger.info('Skipping RSA tests -- not supported on mobile.'); + logger.info('Skipping encryption tests -- not supported on web.'); } await runOnDeviceFsDriverTests(); } diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index 11a06fd23f4..d479d1d4198 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -720,7 +720,7 @@ export default class EncryptionService { return output; } - public isValidSjclEncryptionMethod(method: EncryptionMethod) { + private isValidSjclEncryptionMethod(method: EncryptionMethod) { return [EncryptionMethod.SJCL, EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.SJCL2, EncryptionMethod.SJCL3, EncryptionMethod.SJCL4].indexOf(method) >= 0; } From bf0f12628e6522958e500ba93cfc678894ad1ca8 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sat, 10 Aug 2024 18:08:50 +0800 Subject: [PATCH 22/60] Use featureFlag for enabling the new encryption method --- packages/lib/models/settings/builtInMetadata.ts | 7 +++++++ packages/lib/services/e2ee/EncryptionService.ts | 8 ++++---- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index 1cbb5b14bc0..27234cd788d 100644 --- a/packages/lib/models/settings/builtInMetadata.ts +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -1537,6 +1537,13 @@ const builtInMetadata = (Setting: typeof SettingType) => { // storage: SettingStorage.File, // }, + 'featureFlag.useBetaEncryptionMethod': { + value: false, + type: SettingItemType.Bool, + public: true, + storage: SettingStorage.File, + }, + 'sync.allowUnsupportedProviders': { value: -1, type: SettingItemType.Int, diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index d479d1d4198..012c17eb3ae 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -75,11 +75,11 @@ export default class EncryptionService { // // So making the block 10 times smaller make it 100 times faster! So for now using 5KB. This can be // changed easily since the chunk size is incorporated into the encrypted data. - public chunkSize_ = 65536; + public chunkSize_ = 5000; private decryptedMasterKeys_: Record = {}; - public defaultEncryptionMethod_ = EncryptionMethod.StringV1; // public because used in tests - public defaultFileEncryptionMethod_ = EncryptionMethod.FileV1; // public because used in tests - private defaultMasterKeyEncryptionMethod_ = EncryptionMethod.KeyV1; + public defaultEncryptionMethod_ = Setting.value('featureFlag.useBetaEncryptionMethod') ? EncryptionMethod.StringV1 : EncryptionMethod.SJCL1a; // public because used in tests + public defaultFileEncryptionMethod_ = Setting.value('featureFlag.useBetaEncryptionMethod') ? EncryptionMethod.FileV1 : EncryptionMethod.SJCL1a; // public because used in tests + private defaultMasterKeyEncryptionMethod_ = Setting.value('featureFlag.useBetaEncryptionMethod') ? EncryptionMethod.KeyV1 : EncryptionMethod.SJCL4; private headerTemplates_ = { // Template version 1 From 61dd12d0033eecd5bbafdb2557a3b90726d16004 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sat, 10 Aug 2024 18:38:47 +0800 Subject: [PATCH 23/60] Set encryption chunk size to method-specific --- .../services/e2ee/EncryptionService.test.ts | 2 +- .../lib/services/e2ee/EncryptionService.ts | 50 ++++++++++------ packages/lib/services/e2ee/cryptoTestUtils.ts | 58 +++++++++---------- 3 files changed, 61 insertions(+), 49 deletions(-) diff --git a/packages/lib/services/e2ee/EncryptionService.test.ts b/packages/lib/services/e2ee/EncryptionService.test.ts index 8023215b7d9..aba82e333e9 100644 --- a/packages/lib/services/e2ee/EncryptionService.test.ts +++ b/packages/lib/services/e2ee/EncryptionService.test.ts @@ -154,7 +154,7 @@ describe('services_EncryptionService', () => { // Test that a long string, that is going to be split into multiple chunks, encrypt // and decrypt properly too. let veryLongSecret = ''; - for (let i = 0; i < service.chunkSize() * 3; i++) veryLongSecret += Math.floor(Math.random() * 9); + for (let i = 0; i < service.chunkSize(service.defaultEncryptionMethod()) * 3; i++) veryLongSecret += Math.floor(Math.random() * 9); const cipherText2 = await service.encryptString(veryLongSecret); const plainText2 = await service.decryptString(cipherText2); diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index 012c17eb3ae..b3ea87e495a 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -62,20 +62,6 @@ export default class EncryptionService { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Old code before rule was applied public static fsDriver_: any = null; - // Note: 1 MB is very slow with Node and probably even worse on mobile. - // - // On mobile the time it takes to decrypt increases exponentially for some reason, so it's important - // to have a relatively small size so as not to freeze the app. For example, on Android 7.1 simulator - // with 4.1 GB RAM, it takes this much to decrypt a block; - // - // 50KB => 1000 ms - // 25KB => 250ms - // 10KB => 200ms - // 5KB => 10ms - // - // So making the block 10 times smaller make it 100 times faster! So for now using 5KB. This can be - // changed easily since the chunk size is incorporated into the encrypted data. - public chunkSize_ = 5000; private decryptedMasterKeys_: Record = {}; public defaultEncryptionMethod_ = Setting.value('featureFlag.useBetaEncryptionMethod') ? EncryptionMethod.StringV1 : EncryptionMethod.SJCL1a; // public because used in tests public defaultFileEncryptionMethod_ = Setting.value('featureFlag.useBetaEncryptionMethod') ? EncryptionMethod.FileV1 : EncryptionMethod.SJCL1a; // public because used in tests @@ -103,8 +89,35 @@ export default class EncryptionService { return Object.keys(this.decryptedMasterKeys_).length; } - public chunkSize() { - return this.chunkSize_; + // Note: 1 MB is very slow with Node and probably even worse on mobile. + // + // On mobile the time it takes to decrypt increases exponentially for some reason, so it's important + // to have a relatively small size so as not to freeze the app. For example, on Android 7.1 simulator + // with 4.1 GB RAM, it takes this much to decrypt a block; + // + // 50KB => 1000 ms + // 25KB => 250ms + // 10KB => 200ms + // 5KB => 10ms + // + // So making the block 10 times smaller make it 100 times faster! So for now using 5KB. This can be + // changed easily since the chunk size is incorporated into the encrypted data. + public chunkSize(method: EncryptionMethod) { + type EncryptionMethodChunkSizeMap = Record; + const encryptionMethodChunkSizeMap: EncryptionMethodChunkSizeMap = { + [EncryptionMethod.SJCL]: 5000, + [EncryptionMethod.SJCL1a]: 5000, + [EncryptionMethod.SJCL1b]: 5000, + [EncryptionMethod.SJCL2]: 5000, + [EncryptionMethod.SJCL3]: 5000, + [EncryptionMethod.SJCL4]: 5000, + [EncryptionMethod.Custom]: 5000, + [EncryptionMethod.KeyV1]: 65536, + [EncryptionMethod.FileV1]: 65536, + [EncryptionMethod.StringV1]: 65536, + }; + + return encryptionMethodChunkSizeMap[method]; } public defaultEncryptionMethod() { @@ -469,6 +482,7 @@ export default class EncryptionService { const method = options.encryptionMethod; const masterKeyId = options.masterKeyId ? options.masterKeyId : this.activeMasterKeyId(); const masterKeyPlainText = this.loadedMasterKey(masterKeyId).plainText; + const chunkSize = this.chunkSize(method); const header = { encryptionMethod: method, @@ -480,10 +494,10 @@ export default class EncryptionService { let doneSize = 0; while (true) { - const block = await source.read(this.chunkSize_); + const block = await source.read(chunkSize); if (!block) break; - doneSize += this.chunkSize_; + doneSize += chunkSize; if (options.onProgress) options.onProgress({ doneSize: doneSize }); // Wait for a frame so that the app remains responsive in mobile. diff --git a/packages/lib/services/e2ee/cryptoTestUtils.ts b/packages/lib/services/e2ee/cryptoTestUtils.ts index 847d3827c6b..6cb3c451331 100644 --- a/packages/lib/services/e2ee/cryptoTestUtils.ts +++ b/packages/lib/services/e2ee/cryptoTestUtils.ts @@ -79,7 +79,7 @@ export async function checkDecryptTestData(data: DecryptTestData, options: Check } } -export async function testStringPerformance(chunkSize: number, method: EncryptionMethod, dataSize: number, count: number, options: CheckTestDataOptions = null) { +export async function testStringPerformance(method: EncryptionMethod, dataSize: number, count: number, options: CheckTestDataOptions = null) { options = { throwOnError: false, silent: false, @@ -92,7 +92,6 @@ export async function testStringPerformance(chunkSize: number, method: Encryptio try { serviceInstance.defaultEncryptionMethod_ = method; - serviceInstance.chunkSize_ = chunkSize; let masterKey = await serviceInstance.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); await serviceInstance.loadMasterKey(masterKey, '123456', true); @@ -117,7 +116,7 @@ export async function testStringPerformance(chunkSize: number, method: Encryptio decryptTime += tick3 - tick2; } - messages.push(`Crypto Tests: testStringPerformance(): chunkSize: ${chunkSize}, method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`); + messages.push(`Crypto Tests: testStringPerformance(): method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`); } catch (error) { hasError = true; @@ -139,7 +138,7 @@ export async function testStringPerformance(chunkSize: number, method: Encryptio } } -export async function testFilePerformance(chunkSize: number, method: EncryptionMethod, dataSize: number, count: number, options: CheckTestDataOptions = null) { +export async function testFilePerformance(method: EncryptionMethod, dataSize: number, count: number, options: CheckTestDataOptions = null) { options = { throwOnError: false, silent: false, @@ -152,7 +151,6 @@ export async function testFilePerformance(chunkSize: number, method: EncryptionM try { serviceInstance.defaultFileEncryptionMethod_ = method; - serviceInstance.chunkSize_ = chunkSize; let masterKey = await serviceInstance.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); await serviceInstance.loadMasterKey(masterKey, '123456', true); @@ -178,7 +176,7 @@ export async function testFilePerformance(chunkSize: number, method: EncryptionM decryptTime += tick3 - tick2; } - messages.push(`Crypto Tests: testFilePerformance(): chunkSize: ${chunkSize}, method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`); + messages.push(`Crypto Tests: testFilePerformance(): method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`); } catch (error) { hasError = true; @@ -253,31 +251,31 @@ export const runIntegrationTests = async (silent = false, testPerformance = fals if (testPerformance) { log('Crypto Tests: Testing performance...'); if (shim.mobilePlatform() === '') { - await testStringPerformance(65536, EncryptionMethod.StringV1, 100, 1000); - await testStringPerformance(65536, EncryptionMethod.StringV1, 1000000, 10); - await testStringPerformance(65536, EncryptionMethod.StringV1, 5000000, 10); - await testStringPerformance(5000, EncryptionMethod.SJCL1a, 100, 1000); - await testStringPerformance(5000, EncryptionMethod.SJCL1a, 1000000, 10); - await testStringPerformance(5000, EncryptionMethod.SJCL1a, 5000000, 10); - await testFilePerformance(65536, EncryptionMethod.FileV1, 100, 1000); - await testFilePerformance(65536, EncryptionMethod.FileV1, 1000000, 3); - await testFilePerformance(65536, EncryptionMethod.FileV1, 5000000, 3); - await testFilePerformance(5000, EncryptionMethod.SJCL1a, 100, 1000); - await testFilePerformance(5000, EncryptionMethod.SJCL1a, 1000000, 3); - await testFilePerformance(5000, EncryptionMethod.SJCL1a, 5000000, 3); + await testStringPerformance(EncryptionMethod.StringV1, 100, 1000); + await testStringPerformance(EncryptionMethod.StringV1, 1000000, 10); + await testStringPerformance(EncryptionMethod.StringV1, 5000000, 10); + await testStringPerformance(EncryptionMethod.SJCL1a, 100, 1000); + await testStringPerformance(EncryptionMethod.SJCL1a, 1000000, 10); + await testStringPerformance(EncryptionMethod.SJCL1a, 5000000, 10); + await testFilePerformance(EncryptionMethod.FileV1, 100, 1000); + await testFilePerformance(EncryptionMethod.FileV1, 1000000, 3); + await testFilePerformance(EncryptionMethod.FileV1, 5000000, 3); + await testFilePerformance(EncryptionMethod.SJCL1a, 100, 1000); + await testFilePerformance(EncryptionMethod.SJCL1a, 1000000, 3); + await testFilePerformance(EncryptionMethod.SJCL1a, 5000000, 3); } else { - await testStringPerformance(65536, EncryptionMethod.StringV1, 100, 100); - await testStringPerformance(65536, EncryptionMethod.StringV1, 500000, 3); - await testStringPerformance(65536, EncryptionMethod.StringV1, 1000000, 3); - await testStringPerformance(5000, EncryptionMethod.SJCL1a, 100, 100); - await testStringPerformance(5000, EncryptionMethod.SJCL1a, 500000, 3); - await testStringPerformance(5000, EncryptionMethod.SJCL1a, 1000000, 3); - await testFilePerformance(65536, EncryptionMethod.FileV1, 100, 100); - await testFilePerformance(65536, EncryptionMethod.FileV1, 100000, 3); - await testFilePerformance(65536, EncryptionMethod.FileV1, 500000, 3); - await testFilePerformance(5000, EncryptionMethod.SJCL1a, 100, 100); - await testFilePerformance(5000, EncryptionMethod.SJCL1a, 100000, 3); - await testFilePerformance(5000, EncryptionMethod.SJCL1a, 500000, 3); + await testStringPerformance(EncryptionMethod.StringV1, 100, 100); + await testStringPerformance(EncryptionMethod.StringV1, 500000, 3); + await testStringPerformance(EncryptionMethod.StringV1, 1000000, 3); + await testStringPerformance(EncryptionMethod.SJCL1a, 100, 100); + await testStringPerformance(EncryptionMethod.SJCL1a, 500000, 3); + await testStringPerformance(EncryptionMethod.SJCL1a, 1000000, 3); + await testFilePerformance(EncryptionMethod.FileV1, 100, 100); + await testFilePerformance(EncryptionMethod.FileV1, 100000, 3); + await testFilePerformance(EncryptionMethod.FileV1, 500000, 3); + await testFilePerformance(EncryptionMethod.SJCL1a, 100, 100); + await testFilePerformance(EncryptionMethod.SJCL1a, 100000, 3); + await testFilePerformance(EncryptionMethod.SJCL1a, 500000, 3); } } From 04dfc8bed4795dcb2fd011f44fc28f616e1497e3 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sun, 11 Aug 2024 23:40:52 +0800 Subject: [PATCH 24/60] Empty commit for triggering CI From b912dcaef54767f25cc6040dddc2ca5ad8aada46 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Mon, 12 Aug 2024 01:01:38 +0800 Subject: [PATCH 25/60] Move featureFlag.useBetaEncryptionMethod to sync section --- packages/lib/models/settings/builtInMetadata.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index 57dab3aa918..a7c6f039e99 100644 --- a/packages/lib/models/settings/builtInMetadata.ts +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -1571,6 +1571,10 @@ const builtInMetadata = (Setting: typeof SettingType) => { type: SettingItemType.Bool, public: true, storage: SettingStorage.File, + label: () => _('Use beta encryption'), + description: () => _('Set beta encryption methods as the default methods. This applies to all clients and takes effect after restarting the app'), + section: 'sync', + isGlobal: true, }, 'sync.allowUnsupportedProviders': { From 2adf43884532b1217705cca4c064eb4cfb882e69 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Mon, 12 Aug 2024 08:31:32 +0800 Subject: [PATCH 26/60] Remove translations in featureFlag item --- packages/lib/models/settings/builtInMetadata.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index a7c6f039e99..1e196be73dc 100644 --- a/packages/lib/models/settings/builtInMetadata.ts +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -1571,8 +1571,8 @@ const builtInMetadata = (Setting: typeof SettingType) => { type: SettingItemType.Bool, public: true, storage: SettingStorage.File, - label: () => _('Use beta encryption'), - description: () => _('Set beta encryption methods as the default methods. This applies to all clients and takes effect after restarting the app'), + label: () => 'Use beta encryption', + description: () => 'Set beta encryption methods as the default methods. This applies to all clients and takes effect after restarting the app', section: 'sync', isGlobal: true, }, From 95281e7460fbda1fb083443c1aa390bc9fc41d6e Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 13 Aug 2024 12:43:54 +0800 Subject: [PATCH 27/60] Fix tests --- packages/lib/services/e2ee/crypto.test.ts | 2 +- packages/lib/services/e2ee/cryptoTestUtils.ts | 10 ++++++---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/lib/services/e2ee/crypto.test.ts b/packages/lib/services/e2ee/crypto.test.ts index 86935428f63..86c4a3719f2 100644 --- a/packages/lib/services/e2ee/crypto.test.ts +++ b/packages/lib/services/e2ee/crypto.test.ts @@ -1,7 +1,7 @@ import { afterAllCleanUp, expectNotThrow, setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils'; import { runIntegrationTests } from './cryptoTestUtils'; -describe('e2ee/ppk', () => { +describe('e2ee/crypto', () => { beforeEach(async () => { await setupDatabaseAndSynchronizer(1); diff --git a/packages/lib/services/e2ee/cryptoTestUtils.ts b/packages/lib/services/e2ee/cryptoTestUtils.ts index 6cb3c451331..034ea2acb6a 100644 --- a/packages/lib/services/e2ee/cryptoTestUtils.ts +++ b/packages/lib/services/e2ee/cryptoTestUtils.ts @@ -1,8 +1,8 @@ -import { EncryptionMethod } from './EncryptionService'; -import EncryptionService from './EncryptionService'; +import EncryptionService, { EncryptionMethod } from './EncryptionService'; +import BaseItem from '../../models/BaseItem'; import Folder from '../../models/Folder'; -import Note from '../../models/Note'; import MasterKey from '../../models/MasterKey'; +import Note from '../../models/Note'; import Setting from '../../models/Setting'; import shim from '../..//shim'; import { getEncryptionEnabled, setEncryptionEnabled } from '../synchronizer/syncInfoUtils'; @@ -14,7 +14,7 @@ interface DecryptTestData { ciphertext: string; } -const serviceInstance = EncryptionService.instance(); +let serviceInstance: EncryptionService = null; // This is convenient to quickly generate some data to verify for example that // react-native-quick-crypto and node:crypto can decrypt the same data. @@ -235,6 +235,8 @@ export const runIntegrationTests = async (silent = false, testPerformance = fals log('Crypto Tests: Running integration tests...'); const encryptionEnabled = getEncryptionEnabled(); + serviceInstance = EncryptionService.instance(); + BaseItem.encryptionService_ = EncryptionService.instance(); setEncryptionEnabled(true); log('Crypto Tests: Decrypting using known data...'); From d780b09002adc955be9a7f3557cc54c8adec7276 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 20 Aug 2024 18:48:11 +0800 Subject: [PATCH 28/60] Remove internal methods from interface definition --- packages/app-mobile/services/e2ee/crypto.ts | 154 ++++++++++---------- packages/lib/services/e2ee/crypto.ts | 119 +++++++-------- packages/lib/services/e2ee/types.ts | 8 - 3 files changed, 127 insertions(+), 154 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index b0c02519ab4..c62d8c32297 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -6,41 +6,81 @@ import type { CipherGCMOptions, CipherGCM, DecipherGCM } from 'crypto'; type DigestNameMap = Record; -const crypto: Crypto = { +const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest): Promise => { + const digestNameMap: DigestNameMap = { + sha1: 'SHA-1', + sha224: 'SHA-224', + sha256: 'SHA-256', + sha384: 'SHA-384', + sha512: 'SHA-512', + ripemd160: 'RIPEMD-160', + }; + const rnqcDigestName = digestNameMap[digest]; + + return new Promise((resolve, reject) => { + QuickCrypto.pbkdf2(password, salt, iterations, keylen, rnqcDigestName, (error, result) => { + if (error) { + reject(error); + } else { + resolve(result); + } + }); + }); +}; - getCiphers: () => { - return QuickCrypto.getCiphers(); - }, +const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer | null, authTagLength: number, associatedData: CryptoBuffer | null) => { + if (associatedData === null) { + associatedData = Buffer.alloc(0); + } - getHashes: () => { - return QuickCrypto.getHashes(); - }, + let cipher = null; + if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { + cipher = QuickCrypto.createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; + iv = iv || QuickCrypto.randomBytes(12); // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D + } else { + throw new Error(_('Unknown cipher algorithm: %s', algorithm)); + } - randomBytes: async (size: number) => { - return new Promise((resolve, reject) => { - QuickCrypto.randomBytes(size, (error, result) => { - if (error) { - reject(error); - } else { - resolve(result); - } - }); - }); - }, + cipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); - pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest) => { - const digestNameMap: DigestNameMap = { - sha1: 'SHA-1', - sha224: 'SHA-224', - sha256: 'SHA-256', - sha384: 'SHA-384', - sha512: 'SHA-512', - ripemd160: 'RIPEMD-160', - }; - const rnqcDigestName = digestNameMap[digest]; + const encryptedData = Buffer.concat([cipher.update(data), cipher.final()]); + const authTag = cipher.getAuthTag(); + return Buffer.concat([encryptedData, authTag]); +}; + +const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { + if (associatedData === null) { + associatedData = Buffer.alloc(0); + } + + let decipher = null; + if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { + decipher = QuickCrypto.createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; + } else { + throw new Error(_('Unknown decipher algorithm: %s', algorithm)); + } + + const authTag = data.subarray(-authTagLength); + const encryptedData = data.subarray(0, data.byteLength - authTag.byteLength); + decipher.setAuthTag(authTag); + decipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); + + let decryptedData = null; + try { + decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]); + } catch (error) { + throw new Error(`Authentication failed! ${error}`); + } + + return decryptedData; +}; + +const crypto: Crypto = { + + randomBytes: async (size: number) => { return new Promise((resolve, reject) => { - QuickCrypto.pbkdf2(password, salt, iterations, keylen, rnqcDigestName, (error, result) => { + QuickCrypto.randomBytes(size, (error, result) => { if (error) { reject(error); } else { @@ -50,54 +90,6 @@ const crypto: Crypto = { }); }, - encryptRaw: (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer | null, authTagLength: number, associatedData: CryptoBuffer | null) => { - if (associatedData === null) { - associatedData = Buffer.alloc(0); - } - - let cipher = null; - if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { - cipher = QuickCrypto.createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; - iv = iv || QuickCrypto.randomBytes(12); // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D - } else { - throw new Error(_('Unknown cipher algorithm: %s', algorithm)); - } - - cipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); - - const encryptedData = Buffer.concat([cipher.update(data), cipher.final()]); - const authTag = cipher.getAuthTag(); - - return Buffer.concat([encryptedData, authTag]); - }, - - decryptRaw: (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { - if (associatedData === null) { - associatedData = Buffer.alloc(0); - } - - let decipher = null; - if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { - decipher = QuickCrypto.createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; - } else { - throw new Error(_('Unknown decipher algorithm: %s', algorithm)); - } - - const authTag = data.subarray(-authTagLength); - const encryptedData = data.subarray(0, data.byteLength - authTag.byteLength); - decipher.setAuthTag(authTag); - decipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); - - let decryptedData = null; - try { - decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]); - } catch (error) { - throw new Error(`Authentication failed! ${error}`); - } - - return decryptedData; - }, - encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer | null, data: CryptoBuffer) => { // default encryption parameters @@ -116,8 +108,8 @@ const crypto: Crypto = { salt = salt || await crypto.randomBytes(32); // 256 bits const iv = await crypto.randomBytes(12); // 96 bits - const key = await crypto.pbkdf2Raw(password, salt, iterationCount, keySize, digest); - const encrypted = crypto.encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); + const key = await pbkdf2Raw(password, salt, iterationCount, keySize, digest); + const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); result.salt = salt.toString('base64'); result.iv = iv.toString('base64'); @@ -137,8 +129,8 @@ const crypto: Crypto = { const salt = Buffer.from(data.salt, 'base64'); const iv = Buffer.from(data.iv, 'base64'); - const key = await crypto.pbkdf2Raw(password, salt, data.iter, keySize, digest); - const decrypted = crypto.decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, null); + const key = await pbkdf2Raw(password, salt, data.iter, keySize, digest); + const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, null); return decrypted; }, diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index ba32e4a8dea..449bef47b22 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -2,81 +2,70 @@ import { _ } from '../../locale'; import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult } from './types'; import { promisify } from 'util'; import { - getCiphers as nodeGetCiphers, getHashes as nodeGetHashes, randomBytes as nodeRandomBytes, pbkdf2 as nodePbkdf2, createCipheriv, createDecipheriv, CipherGCMOptions, CipherGCM, DecipherGCM, } from 'crypto'; -const crypto: Crypto = { - - getCiphers: () => { - return nodeGetCiphers(); - }, - - getHashes: () => { - return nodeGetHashes(); - }, - - randomBytes: async (size: number) => { - const randomBytesAsync = promisify(nodeRandomBytes); - return randomBytesAsync(size); - }, - - pbkdf2Raw: async (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest) => { - const pbkdf2Async = promisify(nodePbkdf2); - return pbkdf2Async(password, salt, iterations, keylen, digest); - }, +const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest) => { + const pbkdf2Async = promisify(nodePbkdf2); + return pbkdf2Async(password, salt, iterations, keylen, digest); +}; +const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer | null, authTagLength: number, associatedData: CryptoBuffer | null) => { + if (associatedData === null) { + associatedData = Buffer.alloc(0); + } - encryptRaw: (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer | null, authTagLength: number, associatedData: CryptoBuffer | null) => { + let cipher = null; + if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { + cipher = createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; + iv = iv || nodeRandomBytes(12); // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D + } else { + throw new Error(_('Unknown cipher algorithm: %s', algorithm)); + } - if (associatedData === null) { - associatedData = Buffer.alloc(0); - } + cipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); - let cipher = null; - if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { - cipher = createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; - iv = iv || nodeRandomBytes(12); // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D - } else { - throw new Error(_('Unknown cipher algorithm: %s', algorithm)); - } + const encryptedData = Buffer.concat([cipher.update(data), cipher.final()]); + const authTag = cipher.getAuthTag(); - cipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); + return Buffer.concat([encryptedData, authTag]); +}; - const encryptedData = Buffer.concat([cipher.update(data), cipher.final()]); - const authTag = cipher.getAuthTag(); +const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { + if (associatedData === null) { + associatedData = Buffer.alloc(0); + } + + let decipher = null; + if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { + decipher = createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; + } else { + throw new Error(_('Unknown decipher algorithm: %s', algorithm)); + } + + const authTag = data.subarray(-authTagLength); + const encryptedData = data.subarray(0, data.byteLength - authTag.byteLength); + decipher.setAuthTag(authTag); + decipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); + + let decryptedData = null; + try { + decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]); + } catch (error) { + throw new Error(`Authentication failed! ${error}`); + } + + return decryptedData; +}; - return Buffer.concat([encryptedData, authTag]); - }, +const crypto: Crypto = { - decryptRaw: (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { - if (associatedData === null) { - associatedData = Buffer.alloc(0); - } - - let decipher = null; - if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { - decipher = createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; - } else { - throw new Error(_('Unknown decipher algorithm: %s', algorithm)); - } - - const authTag = data.subarray(-authTagLength); - const encryptedData = data.subarray(0, data.byteLength - authTag.byteLength); - decipher.setAuthTag(authTag); - decipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); - - let decryptedData = null; - try { - decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]); - } catch (error) { - throw new Error(`Authentication failed! ${error}`); - } - - return decryptedData; + randomBytes: async (size: number) => { + const randomBytesAsync = promisify(nodeRandomBytes); + return randomBytesAsync(size); }, encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer | null, data: CryptoBuffer) => { @@ -97,8 +86,8 @@ const crypto: Crypto = { salt = salt || await crypto.randomBytes(32); // 256 bits const iv = await crypto.randomBytes(12); // 96 bits - const key = await crypto.pbkdf2Raw(password, salt, iterationCount, keySize, digest); - const encrypted = crypto.encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); + const key = await pbkdf2Raw(password, salt, iterationCount, keySize, digest); + const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); result.salt = salt.toString('base64'); result.iv = iv.toString('base64'); @@ -118,8 +107,8 @@ const crypto: Crypto = { const salt = Buffer.from(data.salt, 'base64'); const iv = Buffer.from(data.iv, 'base64'); - const key = await crypto.pbkdf2Raw(password, salt, data.iter, keySize, digest); - const decrypted = crypto.decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, null); + const key = await pbkdf2Raw(password, salt, data.iter, keySize, digest); + const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, null); return decrypted; }, diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index ea9981aafa7..8be75c4ecad 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -27,15 +27,7 @@ export interface RSA { } export interface Crypto { - // low level functions - getCiphers(): string[]; - getHashes(): string[]; randomBytes(size: number): Promise; - pbkdf2Raw(password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest): Promise; - encryptRaw(data: CryptoBuffer, algorithm: string, key: CryptoBuffer, iv: CryptoBuffer | null, authTagLength: number, associatedData: CryptoBuffer | null): Buffer; - decryptRaw(data: CryptoBuffer, algorithm: string, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null): Buffer; - - // convenient functions encrypt(password: string, iterationCount: number, salt: CryptoBuffer | null, data: CryptoBuffer): Promise; decrypt(password: string, data: EncryptionResult): Promise; encryptString(password: string, iterationCount: number, salt: CryptoBuffer | null, data: string, encoding: BufferEncoding): Promise; From cbf25635075bfeff2dafeb76a8c7fdad6723c5bc Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 20 Aug 2024 19:31:17 +0800 Subject: [PATCH 29/60] Remove null value in parameter "salt" --- packages/app-mobile/services/e2ee/crypto.ts | 8 +++----- packages/lib/services/e2ee/EncryptionService.ts | 6 +++--- packages/lib/services/e2ee/crypto.ts | 8 +++----- packages/lib/services/e2ee/types.ts | 4 ++-- 4 files changed, 11 insertions(+), 15 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index c62d8c32297..713a9d8be28 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -90,7 +90,7 @@ const crypto: Crypto = { }); }, - encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer | null, data: CryptoBuffer) => { + encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer) => { // default encryption parameters const cipherAlgorithm = CipherAlgorithm.AES_256_GCM; @@ -101,17 +101,15 @@ const crypto: Crypto = { // default encryption parameters won't appear in result const result: EncryptionResult = { iter: iterationCount, - salt: '', + salt: salt.toString('base64'), iv: '', ct: '', // cipherText }; - salt = salt || await crypto.randomBytes(32); // 256 bits const iv = await crypto.randomBytes(12); // 96 bits const key = await pbkdf2Raw(password, salt, iterationCount, keySize, digest); const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); - result.salt = salt.toString('base64'); result.iv = iv.toString('base64'); result.ct = encrypted.toString('base64'); @@ -135,7 +133,7 @@ const crypto: Crypto = { return decrypted; }, - encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer | null, data: string, encoding: BufferEncoding) => { + encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding) => { return crypto.encrypt(password, iterationCount, salt, Buffer.from(data, encoding)); }, }; diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index b3ea87e495a..b419801123c 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -420,20 +420,20 @@ export default class EncryptionService { // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem // 2024-08: Set iteration count in pbkdf2 to 220000 as suggested by OWASP. https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 [EncryptionMethod.KeyV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, 220000, null, plainText, 'hex')); + return JSON.stringify(await crypto.encryptString(key, 220000, await crypto.randomBytes(32), plainText, 'hex')); }, // New encryption method powered by native crypto libraries(node:crypto/react-native-quick-crypto). Using AES-256-GCM and pbkdf2 // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem // The file content is base64 encoded. Decoding it before encryption to reduce the size overhead. [EncryptionMethod.FileV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, 5, null, plainText, 'base64')); + return JSON.stringify(await crypto.encryptString(key, 5, await crypto.randomBytes(32), plainText, 'base64')); }, // New encryption method powered by native crypto libraries(node:crypto/react-native-quick-crypto). Using AES-256-GCM and pbkdf2 // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem [EncryptionMethod.StringV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, 5, null, plainText, 'utf16le')); + return JSON.stringify(await crypto.encryptString(key, 5, await crypto.randomBytes(32), plainText, 'utf16le')); }, [EncryptionMethod.Custom]: () => { diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 449bef47b22..452474ca2a8 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -68,7 +68,7 @@ const crypto: Crypto = { return randomBytesAsync(size); }, - encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer | null, data: CryptoBuffer) => { + encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer) => { // default encryption parameters const cipherAlgorithm = CipherAlgorithm.AES_256_GCM; @@ -79,17 +79,15 @@ const crypto: Crypto = { // default encryption parameters won't appear in result const result: EncryptionResult = { iter: iterationCount, - salt: '', + salt: salt.toString('base64'), iv: '', ct: '', // cipherText }; - salt = salt || await crypto.randomBytes(32); // 256 bits const iv = await crypto.randomBytes(12); // 96 bits const key = await pbkdf2Raw(password, salt, iterationCount, keySize, digest); const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); - result.salt = salt.toString('base64'); result.iv = iv.toString('base64'); result.ct = encrypted.toString('base64'); @@ -113,7 +111,7 @@ const crypto: Crypto = { return decrypted; }, - encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer | null, data: string, encoding: BufferEncoding) => { + encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding) => { return crypto.encrypt(password, iterationCount, salt, Buffer.from(data, encoding)); }, }; diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 8be75c4ecad..dd2c6556d9c 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -28,9 +28,9 @@ export interface RSA { export interface Crypto { randomBytes(size: number): Promise; - encrypt(password: string, iterationCount: number, salt: CryptoBuffer | null, data: CryptoBuffer): Promise; + encrypt(password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer): Promise; decrypt(password: string, data: EncryptionResult): Promise; - encryptString(password: string, iterationCount: number, salt: CryptoBuffer | null, data: string, encoding: BufferEncoding): Promise; + encryptString(password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding): Promise; } export interface CryptoBuffer extends Uint8Array { From d7a3b9c990c9885d12b0096d14c989ed2fefe33a Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 20 Aug 2024 19:49:41 +0800 Subject: [PATCH 30/60] Remove null value in parameter "iv" --- packages/app-mobile/services/e2ee/crypto.ts | 8 +++++--- packages/lib/services/e2ee/crypto.ts | 8 +++++--- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 713a9d8be28..1f3022ab935 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -28,7 +28,7 @@ const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, key }); }; -const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer | null, authTagLength: number, associatedData: CryptoBuffer | null) => { +const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { if (associatedData === null) { associatedData = Buffer.alloc(0); } @@ -36,7 +36,6 @@ const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoB let cipher = null; if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { cipher = QuickCrypto.createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; - iv = iv || QuickCrypto.randomBytes(12); // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D } else { throw new Error(_('Unknown cipher algorithm: %s', algorithm)); } @@ -105,7 +104,10 @@ const crypto: Crypto = { iv: '', ct: '', // cipherText }; - const iv = await crypto.randomBytes(12); // 96 bits + + // 96 bits IV + // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D + const iv = await crypto.randomBytes(12); const key = await pbkdf2Raw(password, salt, iterationCount, keySize, digest); const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 452474ca2a8..787e5cd2c9b 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -13,7 +13,7 @@ const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, key return pbkdf2Async(password, salt, iterations, keylen, digest); }; -const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer | null, authTagLength: number, associatedData: CryptoBuffer | null) => { +const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { if (associatedData === null) { associatedData = Buffer.alloc(0); } @@ -21,7 +21,6 @@ const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoB let cipher = null; if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { cipher = createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; - iv = iv || nodeRandomBytes(12); // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D } else { throw new Error(_('Unknown cipher algorithm: %s', algorithm)); } @@ -83,7 +82,10 @@ const crypto: Crypto = { iv: '', ct: '', // cipherText }; - const iv = await crypto.randomBytes(12); // 96 bits + + // 96 bits IV + // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D + const iv = await crypto.randomBytes(12); const key = await pbkdf2Raw(password, salt, iterationCount, keySize, digest); const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); From 885a4b6482feeff6eec817897e92d9c876c88f8d Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 20 Aug 2024 20:14:40 +0800 Subject: [PATCH 31/60] Remove null value in parameter "associatedData" --- packages/app-mobile/services/e2ee/crypto.ts | 14 ++++---------- packages/lib/services/e2ee/crypto.ts | 14 ++++---------- 2 files changed, 8 insertions(+), 20 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 1f3022ab935..15427184e09 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -28,10 +28,7 @@ const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, key }); }; -const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { - if (associatedData === null) { - associatedData = Buffer.alloc(0); - } +const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => { let cipher = null; if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { @@ -48,10 +45,7 @@ const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoB return Buffer.concat([encryptedData, authTag]); }; -const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { - if (associatedData === null) { - associatedData = Buffer.alloc(0); - } +const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => { let decipher = null; if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { @@ -110,7 +104,7 @@ const crypto: Crypto = { const iv = await crypto.randomBytes(12); const key = await pbkdf2Raw(password, salt, iterationCount, keySize, digest); - const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); + const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, Buffer.alloc(0)); result.iv = iv.toString('base64'); result.ct = encrypted.toString('base64'); @@ -130,7 +124,7 @@ const crypto: Crypto = { const iv = Buffer.from(data.iv, 'base64'); const key = await pbkdf2Raw(password, salt, data.iter, keySize, digest); - const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, null); + const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, Buffer.alloc(0)); return decrypted; }, diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 787e5cd2c9b..0f842ac72f5 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -13,10 +13,7 @@ const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, key return pbkdf2Async(password, salt, iterations, keylen, digest); }; -const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { - if (associatedData === null) { - associatedData = Buffer.alloc(0); - } +const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => { let cipher = null; if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { @@ -33,10 +30,7 @@ const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoB return Buffer.concat([encryptedData, authTag]); }; -const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer | null) => { - if (associatedData === null) { - associatedData = Buffer.alloc(0); - } +const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => { let decipher = null; if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { @@ -88,7 +82,7 @@ const crypto: Crypto = { const iv = await crypto.randomBytes(12); const key = await pbkdf2Raw(password, salt, iterationCount, keySize, digest); - const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, null); + const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, Buffer.alloc(0)); result.iv = iv.toString('base64'); result.ct = encrypted.toString('base64'); @@ -108,7 +102,7 @@ const crypto: Crypto = { const iv = Buffer.from(data.iv, 'base64'); const key = await pbkdf2Raw(password, salt, data.iter, keySize, digest); - const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, null); + const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, Buffer.alloc(0)); return decrypted; }, From b96bd0da2700622e25c970c1a144ad550c3ba885 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 20 Aug 2024 20:44:08 +0800 Subject: [PATCH 32/60] Move default parameters in crypto.ts to encrypt()/decrypt() implementation of EncryptionMethod --- packages/app-mobile/services/e2ee/crypto.ts | 32 +++++--------- .../lib/services/e2ee/EncryptionService.ts | 44 ++++++++++++++++--- packages/lib/services/e2ee/crypto.ts | 32 +++++--------- packages/lib/services/e2ee/types.ts | 16 ++++--- 4 files changed, 67 insertions(+), 57 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 15427184e09..60b296f29ea 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,5 +1,5 @@ import { _ } from '@joplin/lib/locale'; -import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult } from '@joplin/lib/services/e2ee/types'; +import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, EncryptionOptions } from '@joplin/lib/services/e2ee/types'; import QuickCrypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; import type { CipherGCMOptions, CipherGCM, DecipherGCM } from 'crypto'; @@ -83,15 +83,9 @@ const crypto: Crypto = { }); }, - encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer) => { + encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionOptions) => { - // default encryption parameters - const cipherAlgorithm = CipherAlgorithm.AES_256_GCM; - const authTagLength = 16; // 128 bits - const digest = Digest.sha512; - const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes - - // default encryption parameters won't appear in result + // Parameters in EncryptionOptions won't appear in result const result: EncryptionResult = { iter: iterationCount, salt: salt.toString('base64'), @@ -103,8 +97,8 @@ const crypto: Crypto = { // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D const iv = await crypto.randomBytes(12); - const key = await pbkdf2Raw(password, salt, iterationCount, keySize, digest); - const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, Buffer.alloc(0)); + const key = await pbkdf2Raw(password, salt, iterationCount, options.keyLength, options.digestAlgorithm); + const encrypted = encryptRaw(data, options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); result.iv = iv.toString('base64'); result.ct = encrypted.toString('base64'); @@ -112,25 +106,19 @@ const crypto: Crypto = { return result; }, - decrypt: async (password: string, data: EncryptionResult) => { - - // default encryption parameters - const cipherAlgorithm = data.algo || CipherAlgorithm.AES_256_GCM; - const authTagLength = data.ts || 16; // 128 bits - const digest = data.digest || Digest.sha512; - const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes + decrypt: async (password: string, data: EncryptionResult, options: EncryptionOptions) => { const salt = Buffer.from(data.salt, 'base64'); const iv = Buffer.from(data.iv, 'base64'); - const key = await pbkdf2Raw(password, salt, data.iter, keySize, digest); - const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, Buffer.alloc(0)); + const key = await pbkdf2Raw(password, salt, data.iter, options.keyLength, options.digestAlgorithm); + const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); return decrypted; }, - encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding) => { - return crypto.encrypt(password, iterationCount, salt, Buffer.from(data, encoding)); + encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionOptions) => { + return crypto.encrypt(password, iterationCount, salt, Buffer.from(data, encoding), options); }, }; diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index b419801123c..7cbf94ee55b 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -1,4 +1,4 @@ -import { MasterKeyEntity } from './types'; +import { CipherAlgorithm, Digest, MasterKeyEntity } from './types'; import Logger from '@joplin/utils/Logger'; import shim from '../../shim'; import Setting from '../../models/Setting'; @@ -420,20 +420,35 @@ export default class EncryptionService { // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem // 2024-08: Set iteration count in pbkdf2 to 220000 as suggested by OWASP. https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 [EncryptionMethod.KeyV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, 220000, await crypto.randomBytes(32), plainText, 'hex')); + return JSON.stringify(await crypto.encryptString(key, 220000, await crypto.randomBytes(32), plainText, 'hex', { + cipherAlgorithm: CipherAlgorithm.AES_256_GCM, + authTagLength: 16, + digestAlgorithm: Digest.sha512, + keyLength: 32, + })); }, // New encryption method powered by native crypto libraries(node:crypto/react-native-quick-crypto). Using AES-256-GCM and pbkdf2 // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem // The file content is base64 encoded. Decoding it before encryption to reduce the size overhead. [EncryptionMethod.FileV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, 5, await crypto.randomBytes(32), plainText, 'base64')); + return JSON.stringify(await crypto.encryptString(key, 5, await crypto.randomBytes(32), plainText, 'base64', { + cipherAlgorithm: CipherAlgorithm.AES_256_GCM, + authTagLength: 16, + digestAlgorithm: Digest.sha512, + keyLength: 32, + })); }, // New encryption method powered by native crypto libraries(node:crypto/react-native-quick-crypto). Using AES-256-GCM and pbkdf2 // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem [EncryptionMethod.StringV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, 5, await crypto.randomBytes(32), plainText, 'utf16le')); + return JSON.stringify(await crypto.encryptString(key, 5, await crypto.randomBytes(32), plainText, 'utf16le', { + cipherAlgorithm: CipherAlgorithm.AES_256_GCM, + authTagLength: 16, + digestAlgorithm: Digest.sha512, + keyLength: 32, + })); }, [EncryptionMethod.Custom]: () => { @@ -452,11 +467,26 @@ export default class EncryptionService { const sjcl = shim.sjclModule; const crypto = shim.crypto; if (method === EncryptionMethod.KeyV1) { - return (await crypto.decrypt(key, JSON.parse(cipherText))).toString('hex'); + return (await crypto.decrypt(key, JSON.parse(cipherText), { + cipherAlgorithm: CipherAlgorithm.AES_256_GCM, + authTagLength: 16, + digestAlgorithm: Digest.sha512, + keyLength: 32, + })).toString('hex'); } else if (method === EncryptionMethod.FileV1) { - return (await crypto.decrypt(key, JSON.parse(cipherText))).toString('base64'); + return (await crypto.decrypt(key, JSON.parse(cipherText), { + cipherAlgorithm: CipherAlgorithm.AES_256_GCM, + authTagLength: 16, + digestAlgorithm: Digest.sha512, + keyLength: 32, + })).toString('base64'); } else if (method === EncryptionMethod.StringV1) { - return (await crypto.decrypt(key, JSON.parse(cipherText))).toString('utf16le'); + return (await crypto.decrypt(key, JSON.parse(cipherText), { + cipherAlgorithm: CipherAlgorithm.AES_256_GCM, + authTagLength: 16, + digestAlgorithm: Digest.sha512, + keyLength: 32, + })).toString('utf16le'); } else if (this.isValidSjclEncryptionMethod(method)) { try { const output = sjcl.json.decrypt(key, cipherText); diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 0f842ac72f5..43818e97f8c 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,5 +1,5 @@ import { _ } from '../../locale'; -import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult } from './types'; +import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, EncryptionOptions } from './types'; import { promisify } from 'util'; import { randomBytes as nodeRandomBytes, @@ -61,15 +61,9 @@ const crypto: Crypto = { return randomBytesAsync(size); }, - encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer) => { + encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionOptions) => { - // default encryption parameters - const cipherAlgorithm = CipherAlgorithm.AES_256_GCM; - const authTagLength = 16; // 128 bits - const digest = Digest.sha512; - const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes - - // default encryption parameters won't appear in result + // Parameters in EncryptionOptions won't appear in result const result: EncryptionResult = { iter: iterationCount, salt: salt.toString('base64'), @@ -81,8 +75,8 @@ const crypto: Crypto = { // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D const iv = await crypto.randomBytes(12); - const key = await pbkdf2Raw(password, salt, iterationCount, keySize, digest); - const encrypted = encryptRaw(data, cipherAlgorithm, key, iv, authTagLength, Buffer.alloc(0)); + const key = await pbkdf2Raw(password, salt, iterationCount, options.keyLength, options.digestAlgorithm); + const encrypted = encryptRaw(data, options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); result.iv = iv.toString('base64'); result.ct = encrypted.toString('base64'); @@ -90,25 +84,19 @@ const crypto: Crypto = { return result; }, - decrypt: async (password: string, data: EncryptionResult) => { - - // default encryption parameters - const cipherAlgorithm = data.algo || CipherAlgorithm.AES_256_GCM; - const authTagLength = data.ts || 16; // 128 bits - const digest = data.digest || Digest.sha512; - const keySize = 32; // For CipherAlgorithm.AES_256_GCM, 256 bits -> 32 bytes + decrypt: async (password: string, data: EncryptionResult, options: EncryptionOptions) => { const salt = Buffer.from(data.salt, 'base64'); const iv = Buffer.from(data.iv, 'base64'); - const key = await pbkdf2Raw(password, salt, data.iter, keySize, digest); - const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), cipherAlgorithm, key, iv, authTagLength, Buffer.alloc(0)); + const key = await pbkdf2Raw(password, salt, data.iter, options.keyLength, options.digestAlgorithm); + const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); return decrypted; }, - encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding) => { - return crypto.encrypt(password, iterationCount, salt, Buffer.from(data, encoding)); + encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionOptions) => { + return crypto.encrypt(password, iterationCount, salt, Buffer.from(data, encoding), options); }, }; diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index dd2c6556d9c..7491f0c1fad 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -28,9 +28,9 @@ export interface RSA { export interface Crypto { randomBytes(size: number): Promise; - encrypt(password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer): Promise; - decrypt(password: string, data: EncryptionResult): Promise; - encryptString(password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding): Promise; + encrypt(password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionOptions): Promise; + decrypt(password: string, data: EncryptionResult, options: EncryptionOptions): Promise; + encryptString(password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionOptions): Promise; } export interface CryptoBuffer extends Uint8Array { @@ -54,11 +54,15 @@ export enum CipherAlgorithm { } export interface EncryptionResult { - algo?: CipherAlgorithm; // cipher algorithm, default: aes-256-gcm - ts?: number; // authTagLength in bytes, default: 16 - digest?: Digest; // digestAlgorithm, default: sha512 iter: number; // iteration count salt: string; // base64 encoded iv: string; // base64 encoded ct: string; // cipherText, base64 encoded } + +export interface EncryptionOptions { + cipherAlgorithm: CipherAlgorithm; + authTagLength: number; // in bytes + digestAlgorithm: Digest; + keyLength: number; // in bytes +} From b99f9fc4db4746a5af30d49c36b5471c9b912225 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 20 Aug 2024 21:15:25 +0800 Subject: [PATCH 33/60] Modify EncryptionOptions Move iterationCount from EncryptionResult to EncryptionOptions (This changes the format of ciphertext) Modify decryptTestData to apply the latest code changes Rename EncryptionOptions to EncryptionParameters --- packages/app-mobile/services/e2ee/crypto.ts | 17 ++++++++--------- packages/lib/services/e2ee/EncryptionService.ts | 12 +++++++++--- packages/lib/services/e2ee/crypto.ts | 17 ++++++++--------- packages/lib/services/e2ee/cryptoTestUtils.ts | 6 +++--- packages/lib/services/e2ee/types.ts | 10 +++++----- 5 files changed, 33 insertions(+), 29 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 60b296f29ea..feee84779e2 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,5 +1,5 @@ import { _ } from '@joplin/lib/locale'; -import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, EncryptionOptions } from '@joplin/lib/services/e2ee/types'; +import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, EncryptionParameters } from '@joplin/lib/services/e2ee/types'; import QuickCrypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; import type { CipherGCMOptions, CipherGCM, DecipherGCM } from 'crypto'; @@ -83,11 +83,10 @@ const crypto: Crypto = { }); }, - encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionOptions) => { + encrypt: async (password: string, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionParameters) => { - // Parameters in EncryptionOptions won't appear in result + // Parameters in EncryptionParameters won't appear in result const result: EncryptionResult = { - iter: iterationCount, salt: salt.toString('base64'), iv: '', ct: '', // cipherText @@ -97,7 +96,7 @@ const crypto: Crypto = { // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D const iv = await crypto.randomBytes(12); - const key = await pbkdf2Raw(password, salt, iterationCount, options.keyLength, options.digestAlgorithm); + const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); const encrypted = encryptRaw(data, options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); result.iv = iv.toString('base64'); @@ -106,19 +105,19 @@ const crypto: Crypto = { return result; }, - decrypt: async (password: string, data: EncryptionResult, options: EncryptionOptions) => { + decrypt: async (password: string, data: EncryptionResult, options: EncryptionParameters) => { const salt = Buffer.from(data.salt, 'base64'); const iv = Buffer.from(data.iv, 'base64'); - const key = await pbkdf2Raw(password, salt, data.iter, options.keyLength, options.digestAlgorithm); + const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); return decrypted; }, - encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionOptions) => { - return crypto.encrypt(password, iterationCount, salt, Buffer.from(data, encoding), options); + encryptString: async (password: string, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionParameters) => { + return crypto.encrypt(password, salt, Buffer.from(data, encoding), options); }, }; diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index 7cbf94ee55b..dd72bfd703d 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -420,11 +420,12 @@ export default class EncryptionService { // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem // 2024-08: Set iteration count in pbkdf2 to 220000 as suggested by OWASP. https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 [EncryptionMethod.KeyV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, 220000, await crypto.randomBytes(32), plainText, 'hex', { + return JSON.stringify(await crypto.encryptString(key, await crypto.randomBytes(32), plainText, 'hex', { cipherAlgorithm: CipherAlgorithm.AES_256_GCM, authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + iterationCount: 220000, })); }, @@ -432,22 +433,24 @@ export default class EncryptionService { // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem // The file content is base64 encoded. Decoding it before encryption to reduce the size overhead. [EncryptionMethod.FileV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, 5, await crypto.randomBytes(32), plainText, 'base64', { + return JSON.stringify(await crypto.encryptString(key, await crypto.randomBytes(32), plainText, 'base64', { cipherAlgorithm: CipherAlgorithm.AES_256_GCM, authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + iterationCount: 5, })); }, // New encryption method powered by native crypto libraries(node:crypto/react-native-quick-crypto). Using AES-256-GCM and pbkdf2 // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem [EncryptionMethod.StringV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, 5, await crypto.randomBytes(32), plainText, 'utf16le', { + return JSON.stringify(await crypto.encryptString(key, await crypto.randomBytes(32), plainText, 'utf16le', { cipherAlgorithm: CipherAlgorithm.AES_256_GCM, authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + iterationCount: 5, })); }, @@ -472,6 +475,7 @@ export default class EncryptionService { authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + iterationCount: 220000, })).toString('hex'); } else if (method === EncryptionMethod.FileV1) { return (await crypto.decrypt(key, JSON.parse(cipherText), { @@ -479,6 +483,7 @@ export default class EncryptionService { authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + iterationCount: 5, })).toString('base64'); } else if (method === EncryptionMethod.StringV1) { return (await crypto.decrypt(key, JSON.parse(cipherText), { @@ -486,6 +491,7 @@ export default class EncryptionService { authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + iterationCount: 5, })).toString('utf16le'); } else if (this.isValidSjclEncryptionMethod(method)) { try { diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 43818e97f8c..3c6aaf9b4f4 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,5 +1,5 @@ import { _ } from '../../locale'; -import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, EncryptionOptions } from './types'; +import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, EncryptionParameters } from './types'; import { promisify } from 'util'; import { randomBytes as nodeRandomBytes, @@ -61,11 +61,10 @@ const crypto: Crypto = { return randomBytesAsync(size); }, - encrypt: async (password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionOptions) => { + encrypt: async (password: string, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionParameters) => { - // Parameters in EncryptionOptions won't appear in result + // Parameters in EncryptionParameters won't appear in result const result: EncryptionResult = { - iter: iterationCount, salt: salt.toString('base64'), iv: '', ct: '', // cipherText @@ -75,7 +74,7 @@ const crypto: Crypto = { // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D const iv = await crypto.randomBytes(12); - const key = await pbkdf2Raw(password, salt, iterationCount, options.keyLength, options.digestAlgorithm); + const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); const encrypted = encryptRaw(data, options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); result.iv = iv.toString('base64'); @@ -84,19 +83,19 @@ const crypto: Crypto = { return result; }, - decrypt: async (password: string, data: EncryptionResult, options: EncryptionOptions) => { + decrypt: async (password: string, data: EncryptionResult, options: EncryptionParameters) => { const salt = Buffer.from(data.salt, 'base64'); const iv = Buffer.from(data.iv, 'base64'); - const key = await pbkdf2Raw(password, salt, data.iter, options.keyLength, options.digestAlgorithm); + const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); return decrypted; }, - encryptString: async (password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionOptions) => { - return crypto.encrypt(password, iterationCount, salt, Buffer.from(data, encoding), options); + encryptString: async (password: string, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionParameters) => { + return crypto.encrypt(password, salt, Buffer.from(data, encoding), options); }, }; diff --git a/packages/lib/services/e2ee/cryptoTestUtils.ts b/packages/lib/services/e2ee/cryptoTestUtils.ts index 034ea2acb6a..3509cf975e0 100644 --- a/packages/lib/services/e2ee/cryptoTestUtils.ts +++ b/packages/lib/services/e2ee/cryptoTestUtils.ts @@ -206,19 +206,19 @@ const decryptTestData: Record = { method: EncryptionMethod.StringV1, password: '4BfJl8YbM,nXx.LVgs!AzkWWA]', plaintext: '中文にっぽんご한국어😀\uD83D\0\r\nenglish01234567890', - ciphertext: '{"iter":200,"salt":"EUChaTc2I6nIdJPSVCe9YIKUSURd/W8jjX4yzcNvAus=","iv":"RPb1xewALYrPe0hM","ct":"HFEyN3KMsqf/EY2yTTKk0sBm34byHnmJVoL20v5GCuBdCJBl3w7NWoxKAcTD3D1jY3Rt3Brn1mJWykjJMDmPj6tjEyU8ZUS84TuLIW7MTcFOx5xM"}', + ciphertext: '{"salt":"6A3HyEPrxxNEgDq1m43BRrzEehxaIpcRRABBCWX3z9o=","iv":"FZNIQvkHiHolFdK7","ct":"uea3tJg8VJ/JuC2H62cOfEOeMVjLw0trphr4F08VsmKNpv46uYtZJHSx5Jz/5sP/MHNPsmFqXcxzrSdYFOLjRhqyxf71KFFpxGFJ44nMwwRbKgUw"}', }, hexKey: { method: EncryptionMethod.KeyV1, password: '4BfJl8YbM,nXx.LVgs!AzkWWA]', plaintext: 'ea5d8dd43823a812c9a64f4eb09b57e1aa2dbfcda50bf9e416dcd1f8c569a6462f7f61861c3ccda0c7c16bf4192ed9c7ecaf3e1248517bd6f397d1080d2632fc69e1ead59a398a07e7478c8d90142745c0dce39c2105b6d34117424697de03879caa3b6410325f14dc5755b69efa944eb83f06970d04444e83fe054576af20576c0f3a5dc23e0d6dcfa3e84ec05c21139070c0809bd2bdc86a7782368c9d99eb072c858c61ec8926136e6e50dfd57b7e8e0084ad08b2d984db0436f50433cab44b3c462ccd22a8567c8ff86675fff618b11526030f09f735646a9f189f54ba5485d069ee25ac468ec0a873c1223eed34962bd219972020cc147041d4b00a3ab8', - ciphertext: '{"iter":220000,"salt":"0J0KrV2DUWbbY/0T/Do8b2E0cQ2gOGUeVay/uRHnjhY=","iv":"4Z+V0Lh0ID5U7lzN","ct":"X+PZIHUmc3rCIq77fU3MNth2o2GrGd0jgl7P6xwXpK1qhkr/XLVZ5nx5Yo1DVMYivDeoVrPVmpZ9enQ3P667LtUjD2i/qJU0zxOcqdb2ZkAQssRPf7r+8yvhp6dK5d8zKB8gEKJK+z51vWyoEd9CE+NCUzPiNfrStRTuqDwxxXYiBE1gB4lzFWK1GLvfS4998g6rOjonn+SzuIc0JgSLrx9xoP4aVzolGAVRU6C9Hl37hSIudBUuUDskQcJao4BUQ25I5mQ27c9vISonHF2mqt0MbToVMTvYqDZFX48s9zdHlJ52fbgJnTlPD76OgbZEYeCMgPKge2Ic61nD+EYoacMWjS/+WncMbeVH3+5F0gg="}', + ciphertext: '{"salt":"iv5kKP+scMyXKO2jqzef3q9y9p/o/mj6zAoKVltbPx0=","iv":"Gz1PtbDzoaZzRx5m","ct":"YSS4ga1Q0MeVpFbMn2V45TaAbbuJM12vU/qYQ/VEGYPXhNQ4YchfRt7+LjhVxwpvfc/rD0znt6kpAh4ROGS2CLDy27/n5VICgTVY2VVff+YjPAma6odKn2pm4Z88fZlkoJVLi7QyU96Mvb6bYVbuNjQ16hOjFQ3iIIztLcafsHaW6v6gUFrDWYVQPi1/xovmmCe/GaM3JeMye5QQiFrmLQIxEJMNv8YiNyppMVf5b1YGFtDlOjm3XE2H9bb+wWNAd82mcwcAe+ZUedz3AH9PKlsRyBGHfGQ/rfFzeoFj+Wjm4fvPniPa1muRSMQDHU2Zw5YGWQwMNVUHt+y7lDoqPF2NQv4DvPmY1kLz2yohIzc="}', }, base64File: { method: EncryptionMethod.FileV1, password: '4BfJl8YbM,nXx.LVgs!AzkWWA]', plaintext: '5Lit5paH44Gr44Gj44G944KT44GU7ZWc6rWt7Ja08J+YgO+/vQANCmVuZ2xpc2gwMTIzNDU2Nzg5MA==', - ciphertext: '{"iter":200,"salt":"bTlrtQiEzgOY2RRzolmGI360+/j4ZIqn5U5dstwDWsI=","iv":"8zWMrp+ebcVyZB2N","ct":"bNSj8GHcTflaq2WkXoJstvyDgoBZJbojVAahdyRRU3Kn+WzVoeKZTTmyH+pfLsKkkAqxpGUFpZWZM8eFXmxKN83jQBPjrJCdyFY="}', + ciphertext: '{"salt":"wG/hFCK2IZTgi0JR5D1eZr09vnS17hDftWYim0FkU2w=","iv":"bpjlR7owtQzMYJwg","ct":"/CoRA2X3aiPaWdtkotcacpiutN3YJh0ebzplP1bOsukzeqA4ZgbkhKxMLg841nuNq1T8NoX+cQASHMdO5DNzrQP8raVLywd3MIg="}', }, }; diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 7491f0c1fad..659a21558a0 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -28,9 +28,9 @@ export interface RSA { export interface Crypto { randomBytes(size: number): Promise; - encrypt(password: string, iterationCount: number, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionOptions): Promise; - decrypt(password: string, data: EncryptionResult, options: EncryptionOptions): Promise; - encryptString(password: string, iterationCount: number, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionOptions): Promise; + encrypt(password: string, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionParameters): Promise; + decrypt(password: string, data: EncryptionResult, options: EncryptionParameters): Promise; + encryptString(password: string, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionParameters): Promise; } export interface CryptoBuffer extends Uint8Array { @@ -54,15 +54,15 @@ export enum CipherAlgorithm { } export interface EncryptionResult { - iter: number; // iteration count salt: string; // base64 encoded iv: string; // base64 encoded ct: string; // cipherText, base64 encoded } -export interface EncryptionOptions { +export interface EncryptionParameters { cipherAlgorithm: CipherAlgorithm; authTagLength: number; // in bytes digestAlgorithm: Digest; keyLength: number; // in bytes + iterationCount: number; } From 7026dc2e74e4f776c111d4245deca84b6b12004d Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 20 Aug 2024 21:45:21 +0800 Subject: [PATCH 34/60] Move associatedData from encrypt()/decrypt() to EncryptionParameters --- packages/app-mobile/services/e2ee/crypto.ts | 4 ++-- packages/lib/services/e2ee/EncryptionService.ts | 8 ++++++++ packages/lib/services/e2ee/crypto.ts | 4 ++-- packages/lib/services/e2ee/types.ts | 1 + 4 files changed, 13 insertions(+), 4 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index feee84779e2..372555513bb 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -97,7 +97,7 @@ const crypto: Crypto = { const iv = await crypto.randomBytes(12); const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); - const encrypted = encryptRaw(data, options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); + const encrypted = encryptRaw(data, options.cipherAlgorithm, key, iv, options.authTagLength, options.associatedData); result.iv = iv.toString('base64'); result.ct = encrypted.toString('base64'); @@ -111,7 +111,7 @@ const crypto: Crypto = { const iv = Buffer.from(data.iv, 'base64'); const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); - const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); + const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), options.cipherAlgorithm, key, iv, options.authTagLength, options.associatedData); return decrypted; }, diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index dd72bfd703d..d2db1a05a79 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -10,6 +10,8 @@ const { padLeft } = require('../../string-utils.js'); const logger = Logger.create('EncryptionService'); +const emptyUint8Array = new Uint8Array(0); + function hexPad(s: string, length: number) { return padLeft(s, length, '0'); } @@ -425,6 +427,7 @@ export default class EncryptionService { authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + associatedData: emptyUint8Array, iterationCount: 220000, })); }, @@ -438,6 +441,7 @@ export default class EncryptionService { authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + associatedData: emptyUint8Array, iterationCount: 5, })); }, @@ -450,6 +454,7 @@ export default class EncryptionService { authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + associatedData: emptyUint8Array, iterationCount: 5, })); }, @@ -475,6 +480,7 @@ export default class EncryptionService { authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + associatedData: emptyUint8Array, iterationCount: 220000, })).toString('hex'); } else if (method === EncryptionMethod.FileV1) { @@ -483,6 +489,7 @@ export default class EncryptionService { authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + associatedData: emptyUint8Array, iterationCount: 5, })).toString('base64'); } else if (method === EncryptionMethod.StringV1) { @@ -491,6 +498,7 @@ export default class EncryptionService { authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, + associatedData: emptyUint8Array, iterationCount: 5, })).toString('utf16le'); } else if (this.isValidSjclEncryptionMethod(method)) { diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 3c6aaf9b4f4..7805ae05db8 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -75,7 +75,7 @@ const crypto: Crypto = { const iv = await crypto.randomBytes(12); const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); - const encrypted = encryptRaw(data, options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); + const encrypted = encryptRaw(data, options.cipherAlgorithm, key, iv, options.authTagLength, options.associatedData); result.iv = iv.toString('base64'); result.ct = encrypted.toString('base64'); @@ -89,7 +89,7 @@ const crypto: Crypto = { const iv = Buffer.from(data.iv, 'base64'); const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); - const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), options.cipherAlgorithm, key, iv, options.authTagLength, Buffer.alloc(0)); + const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), options.cipherAlgorithm, key, iv, options.authTagLength, options.associatedData); return decrypted; }, diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 659a21558a0..b444704dd5d 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -64,5 +64,6 @@ export interface EncryptionParameters { authTagLength: number; // in bytes digestAlgorithm: Digest; keyLength: number; // in bytes + associatedData: Uint8Array; iterationCount: number; } From bca6e4da94e8736d21b09b6ecd954ee2a7b8d613 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 20 Aug 2024 22:04:47 +0800 Subject: [PATCH 35/60] Clean up --- packages/app-mobile/services/e2ee/crypto.ts | 15 ++------------- packages/lib/services/e2ee/crypto.ts | 15 ++------------- 2 files changed, 4 insertions(+), 26 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 372555513bb..b858ad22cda 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,4 +1,3 @@ -import { _ } from '@joplin/lib/locale'; import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, EncryptionParameters } from '@joplin/lib/services/e2ee/types'; import QuickCrypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; @@ -30,12 +29,7 @@ const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, key const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => { - let cipher = null; - if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { - cipher = QuickCrypto.createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; - } else { - throw new Error(_('Unknown cipher algorithm: %s', algorithm)); - } + const cipher = QuickCrypto.createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; cipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); @@ -47,12 +41,7 @@ const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoB const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => { - let decipher = null; - if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { - decipher = QuickCrypto.createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; - } else { - throw new Error(_('Unknown decipher algorithm: %s', algorithm)); - } + const decipher = QuickCrypto.createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; const authTag = data.subarray(-authTagLength); const encryptedData = data.subarray(0, data.byteLength - authTag.byteLength); diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 7805ae05db8..7c6a05290c7 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,4 +1,3 @@ -import { _ } from '../../locale'; import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, EncryptionParameters } from './types'; import { promisify } from 'util'; import { @@ -15,12 +14,7 @@ const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, key const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => { - let cipher = null; - if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { - cipher = createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; - } else { - throw new Error(_('Unknown cipher algorithm: %s', algorithm)); - } + const cipher = createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; cipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); @@ -32,12 +26,7 @@ const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoB const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => { - let decipher = null; - if (algorithm === CipherAlgorithm.AES_256_GCM || algorithm === CipherAlgorithm.AES_192_GCM || algorithm === CipherAlgorithm.AES_128_GCM) { - decipher = createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; - } else { - throw new Error(_('Unknown decipher algorithm: %s', algorithm)); - } + const decipher = createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; const authTag = data.subarray(-authTagLength); const encryptedData = data.subarray(0, data.byteLength - authTag.byteLength); From d9d8eaee061a4d6fcf88001d352b10bfaf7a207a Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 20 Aug 2024 22:13:13 +0800 Subject: [PATCH 36/60] Rename a method parameter --- packages/app-mobile/services/e2ee/crypto.ts | 16 ++++++++-------- packages/lib/services/e2ee/crypto.ts | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index b858ad22cda..de34e506177 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -72,7 +72,7 @@ const crypto: Crypto = { }); }, - encrypt: async (password: string, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionParameters) => { + encrypt: async (password: string, salt: CryptoBuffer, data: CryptoBuffer, encryptionParameters: EncryptionParameters) => { // Parameters in EncryptionParameters won't appear in result const result: EncryptionResult = { @@ -85,8 +85,8 @@ const crypto: Crypto = { // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D const iv = await crypto.randomBytes(12); - const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); - const encrypted = encryptRaw(data, options.cipherAlgorithm, key, iv, options.authTagLength, options.associatedData); + const key = await pbkdf2Raw(password, salt, encryptionParameters.iterationCount, encryptionParameters.keyLength, encryptionParameters.digestAlgorithm); + const encrypted = encryptRaw(data, encryptionParameters.cipherAlgorithm, key, iv, encryptionParameters.authTagLength, encryptionParameters.associatedData); result.iv = iv.toString('base64'); result.ct = encrypted.toString('base64'); @@ -94,19 +94,19 @@ const crypto: Crypto = { return result; }, - decrypt: async (password: string, data: EncryptionResult, options: EncryptionParameters) => { + decrypt: async (password: string, data: EncryptionResult, encryptionParameters: EncryptionParameters) => { const salt = Buffer.from(data.salt, 'base64'); const iv = Buffer.from(data.iv, 'base64'); - const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); - const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), options.cipherAlgorithm, key, iv, options.authTagLength, options.associatedData); + const key = await pbkdf2Raw(password, salt, encryptionParameters.iterationCount, encryptionParameters.keyLength, encryptionParameters.digestAlgorithm); + const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), encryptionParameters.cipherAlgorithm, key, iv, encryptionParameters.authTagLength, encryptionParameters.associatedData); return decrypted; }, - encryptString: async (password: string, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionParameters) => { - return crypto.encrypt(password, salt, Buffer.from(data, encoding), options); + encryptString: async (password: string, salt: CryptoBuffer, data: string, encoding: BufferEncoding, encryptionParameters: EncryptionParameters) => { + return crypto.encrypt(password, salt, Buffer.from(data, encoding), encryptionParameters); }, }; diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 7c6a05290c7..a56f52b2d10 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -50,7 +50,7 @@ const crypto: Crypto = { return randomBytesAsync(size); }, - encrypt: async (password: string, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionParameters) => { + encrypt: async (password: string, salt: CryptoBuffer, data: CryptoBuffer, encryptionParameters: EncryptionParameters) => { // Parameters in EncryptionParameters won't appear in result const result: EncryptionResult = { @@ -63,8 +63,8 @@ const crypto: Crypto = { // "For IVs, it is recommended that implementations restrict support to the length of 96 bits, to promote interoperability, efficiency, and simplicity of design." - NIST SP 800-38D const iv = await crypto.randomBytes(12); - const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); - const encrypted = encryptRaw(data, options.cipherAlgorithm, key, iv, options.authTagLength, options.associatedData); + const key = await pbkdf2Raw(password, salt, encryptionParameters.iterationCount, encryptionParameters.keyLength, encryptionParameters.digestAlgorithm); + const encrypted = encryptRaw(data, encryptionParameters.cipherAlgorithm, key, iv, encryptionParameters.authTagLength, encryptionParameters.associatedData); result.iv = iv.toString('base64'); result.ct = encrypted.toString('base64'); @@ -72,19 +72,19 @@ const crypto: Crypto = { return result; }, - decrypt: async (password: string, data: EncryptionResult, options: EncryptionParameters) => { + decrypt: async (password: string, data: EncryptionResult, encryptionParameters: EncryptionParameters) => { const salt = Buffer.from(data.salt, 'base64'); const iv = Buffer.from(data.iv, 'base64'); - const key = await pbkdf2Raw(password, salt, options.iterationCount, options.keyLength, options.digestAlgorithm); - const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), options.cipherAlgorithm, key, iv, options.authTagLength, options.associatedData); + const key = await pbkdf2Raw(password, salt, encryptionParameters.iterationCount, encryptionParameters.keyLength, encryptionParameters.digestAlgorithm); + const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), encryptionParameters.cipherAlgorithm, key, iv, encryptionParameters.authTagLength, encryptionParameters.associatedData); return decrypted; }, - encryptString: async (password: string, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionParameters) => { - return crypto.encrypt(password, salt, Buffer.from(data, encoding), options); + encryptString: async (password: string, salt: CryptoBuffer, data: string, encoding: BufferEncoding, encryptionParameters: EncryptionParameters) => { + return crypto.encrypt(password, salt, Buffer.from(data, encoding), encryptionParameters); }, }; From bb69f50bfb80897d5569f701b2418b8bc7f031ee Mon Sep 17 00:00:00 2001 From: wh201906 Date: Tue, 20 Aug 2024 23:36:13 +0800 Subject: [PATCH 37/60] Move featureFlag.useBetaEncryptionMethod to sync.advanced --- packages/lib/models/settings/builtInMetadata.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index 1e196be73dc..bf8fbf253df 100644 --- a/packages/lib/models/settings/builtInMetadata.ts +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -1572,7 +1572,8 @@ const builtInMetadata = (Setting: typeof SettingType) => { public: true, storage: SettingStorage.File, label: () => 'Use beta encryption', - description: () => 'Set beta encryption methods as the default methods. This applies to all clients and takes effect after restarting the app', + description: () => 'Set beta encryption methods as the default methods. This applies to all clients and takes effect after restarting the app.', + advanced: true, section: 'sync', isGlobal: true, }, From bc9605eb6e68dea2e675a3d6e953c2d1def177fa Mon Sep 17 00:00:00 2001 From: wh201906 Date: Wed, 21 Aug 2024 01:17:42 +0800 Subject: [PATCH 38/60] Better chunkSize --- packages/lib/services/e2ee/EncryptionService.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index d2db1a05a79..1adbeeb5a85 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -91,7 +91,9 @@ export default class EncryptionService { return Object.keys(this.decryptedMasterKeys_).length; } - // Note: 1 MB is very slow with Node and probably even worse on mobile. + // Note for methods using SJCL: + // + // 1 MB is very slow with Node and probably even worse on mobile. // // On mobile the time it takes to decrypt increases exponentially for some reason, so it's important // to have a relatively small size so as not to freeze the app. For example, on Android 7.1 simulator @@ -114,9 +116,9 @@ export default class EncryptionService { [EncryptionMethod.SJCL3]: 5000, [EncryptionMethod.SJCL4]: 5000, [EncryptionMethod.Custom]: 5000, - [EncryptionMethod.KeyV1]: 65536, - [EncryptionMethod.FileV1]: 65536, - [EncryptionMethod.StringV1]: 65536, + [EncryptionMethod.KeyV1]: 5000, // Master key is not encrypted by chunks so this value will not be used. + [EncryptionMethod.FileV1]: 524288, // 512k + [EncryptionMethod.StringV1]: 65536, // 64k }; return encryptionMethodChunkSizeMap[method]; From 91f9c9b81144747532a1b92c19bbb23ef30ca654 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Wed, 21 Aug 2024 01:25:54 +0800 Subject: [PATCH 39/60] Add type for encryption handler --- packages/lib/services/e2ee/EncryptionService.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index 1adbeeb5a85..fb058f21295 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -305,7 +305,8 @@ export default class EncryptionService { const sjcl = shim.sjclModule; const crypto = shim.crypto; - const handlers: Record Promise> = { + type EncryptionMethodHandler = (()=> Promise); + const handlers: Record = { // 2020-01-23: Deprecated and no longer secure due to the use og OCB2 mode - do not use. [EncryptionMethod.SJCL]: () => { try { From b329c0cf7d8a538936ca1b694a6811718b83e9c2 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Wed, 21 Aug 2024 03:59:42 +0800 Subject: [PATCH 40/60] Add test cases for invalid ciphertext --- .../services/e2ee/EncryptionService.test.ts | 48 +++++++++++++++++-- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/packages/lib/services/e2ee/EncryptionService.test.ts b/packages/lib/services/e2ee/EncryptionService.test.ts index aba82e333e9..343bdd507a8 100644 --- a/packages/lib/services/e2ee/EncryptionService.test.ts +++ b/packages/lib/services/e2ee/EncryptionService.test.ts @@ -218,6 +218,48 @@ describe('services_EncryptionService', () => { expect(hasThrown).toBe(true); })); + it('should fail to decrypt if ciphertext is not a valid JSON string', (async () => { + const masterKey = await service.generateMasterKey('123456'); + const masterKeyContent = await service.decryptMasterKeyContent(masterKey, '123456'); + + const jsonCipherTextMethodList = [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.SJCL4, EncryptionMethod.KeyV1, EncryptionMethod.FileV1, EncryptionMethod.StringV1]; + for (const jsonCipherTextMethod of jsonCipherTextMethodList) { + const cipherTextString = await service.encrypt(jsonCipherTextMethod, masterKeyContent, 'e21de21d'); // 'e21de21d' is a valid base64/hex string + + // Check if decryption is working + const plainText = await service.decrypt(jsonCipherTextMethod, masterKeyContent, cipherTextString); + expect(plainText).toBe('e21de21d'); + + // Make invalid JSON + const invalidCipherText = cipherTextString.replace('{', '{,'); + const hasThrown = await checkThrowAsync(async () => await service.decrypt(jsonCipherTextMethod, masterKeyContent, invalidCipherText)); + expect(hasThrown).toBe(true); + } + })); + + it('should fail to decrypt if ciphertext authentication failed', (async () => { + const masterKey = await service.generateMasterKey('123456'); + const masterKeyContent = await service.decryptMasterKeyContent(masterKey, '123456'); + + const authenticatedEncryptionMethodList = [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.SJCL4, EncryptionMethod.KeyV1, EncryptionMethod.FileV1, EncryptionMethod.StringV1]; + for (const authenticatedEncryptionMethod of authenticatedEncryptionMethodList) { + const cipherTextObject = JSON.parse(await service.encrypt(authenticatedEncryptionMethod, masterKeyContent, 'e21de21d')); // 'e21de21d' is a valid base64/hex string + expect(cipherTextObject).toHaveProperty('ct'); + const ct = Buffer.from(cipherTextObject['ct'], 'base64'); + + // Should not fail if the binary data of ct is not modified + const oldCipherTextObject = { ...cipherTextObject, ct: ct.toString('base64') }; + const plainText = await service.decrypt(authenticatedEncryptionMethod, masterKeyContent, JSON.stringify(oldCipherTextObject)); + expect(plainText).toBe('e21de21d'); + + // The encrypted data part is changed so it doesn't match the authentication tag. Decryption should fail. + ct[0] ^= 0x55; + const newCipherTextObject = { ...cipherTextObject, ct: ct.toString('base64') }; + const hasThrown = await checkThrowAsync(async () => service.decrypt(authenticatedEncryptionMethod, masterKeyContent, JSON.stringify(newCipherTextObject))); + expect(hasThrown).toBe(true); + } + })); + it('should encrypt and decrypt notes and folders', (async () => { let masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); @@ -281,9 +323,9 @@ describe('services_EncryptionService', () => { expect(hasThrown).toBe(true); // Now check that the new one fixes the problem - const newEncryptionMethodList = [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.StringV1]; - for (const newEncryptionMethod of newEncryptionMethodList) { - service.defaultEncryptionMethod_ = newEncryptionMethod; + const stringEncryptionMethodList = [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.StringV1]; + for (const stringEncryptionMethod of stringEncryptionMethodList) { + service.defaultEncryptionMethod_ = stringEncryptionMethod; const cipherText = await service.encryptString('🐶🐶🐶'.substr(0, 5)); const plainText = await service.decryptString(cipherText); expect(plainText).toBe('🐶🐶🐶'.substr(0, 5)); From d0140da87c6e689dad25f0c8b24883824d119bf9 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 20 Aug 2024 19:47:28 -0700 Subject: [PATCH 41/60] Migrate to web crypto from Node crypto --- .eslintignore | 1 + .gitignore | 1 + packages/app-mobile/services/e2ee/crypto.ts | 12 +-- packages/lib/services/e2ee/constants.ts | 12 +++ packages/lib/services/e2ee/crypto.ts | 88 ++++++++++++--------- 5 files changed, 66 insertions(+), 48 deletions(-) create mode 100644 packages/lib/services/e2ee/constants.ts diff --git a/.eslintignore b/.eslintignore index 603ad087680..8146f37bbb7 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1071,6 +1071,7 @@ packages/lib/services/debug/populateDatabase.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js packages/lib/services/e2ee/RSA.node.js +packages/lib/services/e2ee/constants.js packages/lib/services/e2ee/crypto.test.js packages/lib/services/e2ee/crypto.js packages/lib/services/e2ee/cryptoTestUtils.js diff --git a/.gitignore b/.gitignore index 3db4b3bb2be..e8571c87fde 100644 --- a/.gitignore +++ b/.gitignore @@ -1048,6 +1048,7 @@ packages/lib/services/debug/populateDatabase.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js packages/lib/services/e2ee/RSA.node.js +packages/lib/services/e2ee/constants.js packages/lib/services/e2ee/crypto.test.js packages/lib/services/e2ee/crypto.js packages/lib/services/e2ee/cryptoTestUtils.js diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index de34e506177..4fd4b9fa216 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,20 +1,12 @@ import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, EncryptionParameters } from '@joplin/lib/services/e2ee/types'; +import { digestNameMap } from '@joplin/lib/services/e2ee/constants'; import QuickCrypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; import type { CipherGCMOptions, CipherGCM, DecipherGCM } from 'crypto'; -type DigestNameMap = Record; const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest): Promise => { - const digestNameMap: DigestNameMap = { - sha1: 'SHA-1', - sha224: 'SHA-224', - sha256: 'SHA-256', - sha384: 'SHA-384', - sha512: 'SHA-512', - ripemd160: 'RIPEMD-160', - }; - const rnqcDigestName = digestNameMap[digest]; + const rnqcDigestName = digestNameMap[digest] as HashAlgorithm; return new Promise((resolve, reject) => { QuickCrypto.pbkdf2(password, salt, iterations, keylen, rnqcDigestName, (error, result) => { diff --git a/packages/lib/services/e2ee/constants.ts b/packages/lib/services/e2ee/constants.ts new file mode 100644 index 00000000000..071aad07219 --- /dev/null +++ b/packages/lib/services/e2ee/constants.ts @@ -0,0 +1,12 @@ +import { Digest } from './types'; + +export const digestNameMap: Record = { + sha1: 'SHA-1', + sha224: 'SHA-224', + sha256: 'SHA-256', + sha384: 'SHA-384', + sha512: 'SHA-512', + ripemd160: 'RIPEMD-160', +}; + +export default digestNameMap; diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index a56f52b2d10..c592a978519 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,56 +1,67 @@ -import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, EncryptionParameters } from './types'; -import { promisify } from 'util'; +import { Crypto, CryptoBuffer, Digest, EncryptionResult, EncryptionParameters, CipherAlgorithm } from './types'; import { - randomBytes as nodeRandomBytes, - pbkdf2 as nodePbkdf2, - createCipheriv, createDecipheriv, - CipherGCMOptions, CipherGCM, DecipherGCM, + webcrypto, } from 'crypto'; - -const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest) => { - const pbkdf2Async = promisify(nodePbkdf2); - return pbkdf2Async(password, salt, iterations, keylen, digest); +import { Buffer } from 'buffer'; +import digestNameMap from './constants'; + +const pbkdf2Raw = async (password: string, salt: Uint8Array, iterations: number, keylenBytes: number, digest: Digest) => { + const digestName = digestNameMap[digest]; + const encoder = new TextEncoder(); + const key = await webcrypto.subtle.importKey( + 'raw', encoder.encode(password), { name: 'PBKDF2' }, false, ['deriveBits'], + ); + return Buffer.from(await webcrypto.subtle.deriveBits( + { name: 'PBKDF2', salt, iterations, hash: digestName }, key, keylenBytes * 8, + )); }; -const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => { - - const cipher = createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM; - - cipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); - - const encryptedData = Buffer.concat([cipher.update(data), cipher.final()]); - const authTag = cipher.getAuthTag(); - - return Buffer.concat([encryptedData, authTag]); +const loadEncryptDecryptKey = async (keyData: Uint8Array) => { + return await webcrypto.subtle.importKey( + 'raw', + keyData, + { name: 'AES-GCM' }, + false, + ['encrypt', 'decrypt'], + ); }; -const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => { - - const decipher = createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as DecipherGCM; +const encryptRaw = async (data: Uint8Array, key: Uint8Array, iv: Uint8Array, authTagLengthBytes: number, additionalData: Uint8Array) => { + const loadedKey = await loadEncryptDecryptKey(key); + return Buffer.from(await webcrypto.subtle.encrypt({ + name: 'AES-GCM', + iv, + additionalData, + tagLength: authTagLengthBytes * 8, + }, loadedKey, data)); +}; - const authTag = data.subarray(-authTagLength); - const encryptedData = data.subarray(0, data.byteLength - authTag.byteLength); - decipher.setAuthTag(authTag); - decipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); +const decryptRaw = async (data: Uint8Array, key: Uint8Array, iv: Uint8Array, authTagLengthBytes: number, associatedData: Uint8Array) => { + const loadedKey = await loadEncryptDecryptKey(key); + return Buffer.from(await webcrypto.subtle.decrypt({ + name: 'AES-GCM', + iv, + additionalData: associatedData, + tagLength: authTagLengthBytes * 8, + }, loadedKey, data)); +}; - let decryptedData = null; - try { - decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]); - } catch (error) { - throw new Error(`Authentication failed! ${error}`); +const validateEncryptionParameters = ({ cipherAlgorithm }: EncryptionParameters) => { + if (cipherAlgorithm !== CipherAlgorithm.AES_256_GCM) { + throw new Error(`Unsupported cipherAlgorithm: ${cipherAlgorithm}. Must be AES 256 GCM.`); } - - return decryptedData; }; const crypto: Crypto = { randomBytes: async (size: number) => { - const randomBytesAsync = promisify(nodeRandomBytes); - return randomBytesAsync(size); + const result = new Uint8Array(size); + webcrypto.getRandomValues(result); + return Buffer.from(result); }, encrypt: async (password: string, salt: CryptoBuffer, data: CryptoBuffer, encryptionParameters: EncryptionParameters) => { + validateEncryptionParameters(encryptionParameters); // Parameters in EncryptionParameters won't appear in result const result: EncryptionResult = { @@ -64,7 +75,7 @@ const crypto: Crypto = { const iv = await crypto.randomBytes(12); const key = await pbkdf2Raw(password, salt, encryptionParameters.iterationCount, encryptionParameters.keyLength, encryptionParameters.digestAlgorithm); - const encrypted = encryptRaw(data, encryptionParameters.cipherAlgorithm, key, iv, encryptionParameters.authTagLength, encryptionParameters.associatedData); + const encrypted = await encryptRaw(data, key, iv, encryptionParameters.authTagLength, encryptionParameters.associatedData); result.iv = iv.toString('base64'); result.ct = encrypted.toString('base64'); @@ -73,12 +84,13 @@ const crypto: Crypto = { }, decrypt: async (password: string, data: EncryptionResult, encryptionParameters: EncryptionParameters) => { + validateEncryptionParameters(encryptionParameters); const salt = Buffer.from(data.salt, 'base64'); const iv = Buffer.from(data.iv, 'base64'); const key = await pbkdf2Raw(password, salt, encryptionParameters.iterationCount, encryptionParameters.keyLength, encryptionParameters.digestAlgorithm); - const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), encryptionParameters.cipherAlgorithm, key, iv, encryptionParameters.authTagLength, encryptionParameters.associatedData); + const decrypted = decryptRaw(Buffer.from(data.ct, 'base64'), key, iv, encryptionParameters.authTagLength, encryptionParameters.associatedData); return decrypted; }, From 41e7918ed085179c300b67cc43b8108c2e6a8244 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Tue, 20 Aug 2024 20:01:12 -0700 Subject: [PATCH 42/60] Enable integrated tests on web --- packages/app-mobile/root.tsx | 2 +- packages/app-mobile/utils/shim-init-react/index.web.ts | 2 ++ packages/app-mobile/web/mocks/nodeCrypto.js | 2 ++ packages/app-mobile/web/webpack.config.js | 1 + packages/lib/services/e2ee/crypto.ts | 4 +--- 5 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 packages/app-mobile/web/mocks/nodeCrypto.js diff --git a/packages/app-mobile/root.tsx b/packages/app-mobile/root.tsx index b11043a491f..56ac0be7253 100644 --- a/packages/app-mobile/root.tsx +++ b/packages/app-mobile/root.tsx @@ -847,10 +847,10 @@ async function initialize(dispatch: Dispatch) { if (Setting.value('env') === 'dev') { if (Platform.OS !== 'web') { await runRsaIntegrationTests(); - await runCryptoIntegrationTests(); } else { logger.info('Skipping encryption tests -- not supported on web.'); } + await runCryptoIntegrationTests(); await runOnDeviceFsDriverTests(); } diff --git a/packages/app-mobile/utils/shim-init-react/index.web.ts b/packages/app-mobile/utils/shim-init-react/index.web.ts index c92e1e0d06f..e0b648a22d4 100644 --- a/packages/app-mobile/utils/shim-init-react/index.web.ts +++ b/packages/app-mobile/utils/shim-init-react/index.web.ts @@ -5,6 +5,7 @@ import shimInitShared from './shimInitShared'; import FsDriverWeb from '../fs-driver/fs-driver-rn.web'; import { FetchBlobOptions } from '@joplin/lib/types'; import JoplinError from '@joplin/lib/JoplinError'; +import joplinCrypto from '@joplin/lib/services/e2ee/crypto'; const shimInit = () => { type GetLocationOptions = { timeout?: number }; @@ -41,6 +42,7 @@ const shimInit = () => { return fsDriver_; }; shim.fsDriver = fsDriver; + shim.crypto = joplinCrypto; shim.randomBytes = async (count: number) => { const buffer = new Uint8Array(count); diff --git a/packages/app-mobile/web/mocks/nodeCrypto.js b/packages/app-mobile/web/mocks/nodeCrypto.js new file mode 100644 index 00000000000..25d0c48dd75 --- /dev/null +++ b/packages/app-mobile/web/mocks/nodeCrypto.js @@ -0,0 +1,2 @@ + +exports.webcrypto = crypto; diff --git a/packages/app-mobile/web/webpack.config.js b/packages/app-mobile/web/webpack.config.js index 479dac36d8e..76af35cdc6e 100644 --- a/packages/app-mobile/web/webpack.config.js +++ b/packages/app-mobile/web/webpack.config.js @@ -61,6 +61,7 @@ module.exports = { resolve: { alias: { 'react-native$': 'react-native-web', + 'crypto': path.resolve(__dirname, 'mocks/nodeCrypto.js'), // Map some modules that don't work on web to the empty dictionary. 'react-native-fingerprint-scanner': emptyLibraryMock, diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index c592a978519..af6ec1eccf7 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,7 +1,5 @@ import { Crypto, CryptoBuffer, Digest, EncryptionResult, EncryptionParameters, CipherAlgorithm } from './types'; -import { - webcrypto, -} from 'crypto'; +import { webcrypto } from 'crypto'; import { Buffer } from 'buffer'; import digestNameMap from './constants'; From 6087f771b63139d7950cae5e8abaa4a0a7a0ca7f Mon Sep 17 00:00:00 2001 From: wh201906 Date: Thu, 22 Aug 2024 00:49:29 +0800 Subject: [PATCH 43/60] Revert an unintended change --- packages/app-desktop/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/app-desktop/app.ts b/packages/app-desktop/app.ts index 23172a85640..e10eea5bc8f 100644 --- a/packages/app-desktop/app.ts +++ b/packages/app-desktop/app.ts @@ -73,6 +73,7 @@ import SearchEngine from '@joplin/lib/services/search/SearchEngine'; import { PackageInfo } from '@joplin/lib/versionInfo'; import { CustomProtocolHandler } from './utils/customProtocols/handleCustomProtocols'; import { refreshFolders } from '@joplin/lib/folders-screen-utils'; + const pluginClasses = [ require('./plugins/GotoAnything').default, ]; From dfefff792a41a2045c9961ef79f4c2859cb61448 Mon Sep 17 00:00:00 2001 From: Henry Heino Date: Thu, 22 Aug 2024 02:27:40 -0700 Subject: [PATCH 44/60] Fix randomBytes for a large output size --- packages/lib/services/e2ee/crypto.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index af6ec1eccf7..51163e18001 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -53,8 +53,20 @@ const validateEncryptionParameters = ({ cipherAlgorithm }: EncryptionParameters) const crypto: Crypto = { randomBytes: async (size: number) => { + // .getRandomValues has a maximum output size + const maxChunkSize = 65536; const result = new Uint8Array(size); - webcrypto.getRandomValues(result); + + if (size < maxChunkSize) { + webcrypto.getRandomValues(result); + } else { + const fullSizeChunk = new Uint8Array(maxChunkSize); + for (let offset = 0; offset < size; offset += maxChunkSize) { + const chunk = offset + maxChunkSize > size ? new Uint8Array(size - offset) : fullSizeChunk; + webcrypto.getRandomValues(chunk); + result.set(chunk, offset); + } + } return Buffer.from(result); }, From 1f9525daa7f033db93b4d4dddc89504fd42638d2 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Thu, 22 Aug 2024 20:52:50 +0800 Subject: [PATCH 45/60] Remove digest name mapping The digest (hash) algorithm name style of react-native-quick-crypto and Web Crypto API is the same, so the name mapping is useless. --- .eslintignore | 1 - .gitignore | 1 - packages/app-mobile/services/e2ee/crypto.ts | 5 +---- packages/lib/services/e2ee/constants.ts | 12 ------------ packages/lib/services/e2ee/crypto.ts | 4 +--- packages/lib/services/e2ee/types.ts | 12 +++++------- packages/tools/cspell/dictionary4.txt | 1 - 7 files changed, 7 insertions(+), 29 deletions(-) delete mode 100644 packages/lib/services/e2ee/constants.ts diff --git a/.eslintignore b/.eslintignore index a345acaa365..c34132bab24 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1075,7 +1075,6 @@ packages/lib/services/debug/populateDatabase.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js packages/lib/services/e2ee/RSA.node.js -packages/lib/services/e2ee/constants.js packages/lib/services/e2ee/crypto.test.js packages/lib/services/e2ee/crypto.js packages/lib/services/e2ee/cryptoTestUtils.js diff --git a/.gitignore b/.gitignore index 045f147eac9..6fa94574561 100644 --- a/.gitignore +++ b/.gitignore @@ -1052,7 +1052,6 @@ packages/lib/services/debug/populateDatabase.js packages/lib/services/e2ee/EncryptionService.test.js packages/lib/services/e2ee/EncryptionService.js packages/lib/services/e2ee/RSA.node.js -packages/lib/services/e2ee/constants.js packages/lib/services/e2ee/crypto.test.js packages/lib/services/e2ee/crypto.js packages/lib/services/e2ee/cryptoTestUtils.js diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 4fd4b9fa216..d03bc4d5ef0 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -1,15 +1,12 @@ import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, EncryptionParameters } from '@joplin/lib/services/e2ee/types'; -import { digestNameMap } from '@joplin/lib/services/e2ee/constants'; import QuickCrypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; import type { CipherGCMOptions, CipherGCM, DecipherGCM } from 'crypto'; const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest): Promise => { - const rnqcDigestName = digestNameMap[digest] as HashAlgorithm; - return new Promise((resolve, reject) => { - QuickCrypto.pbkdf2(password, salt, iterations, keylen, rnqcDigestName, (error, result) => { + QuickCrypto.pbkdf2(password, salt, iterations, keylen, digest as HashAlgorithm, (error, result) => { if (error) { reject(error); } else { diff --git a/packages/lib/services/e2ee/constants.ts b/packages/lib/services/e2ee/constants.ts deleted file mode 100644 index 071aad07219..00000000000 --- a/packages/lib/services/e2ee/constants.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Digest } from './types'; - -export const digestNameMap: Record = { - sha1: 'SHA-1', - sha224: 'SHA-224', - sha256: 'SHA-256', - sha384: 'SHA-384', - sha512: 'SHA-512', - ripemd160: 'RIPEMD-160', -}; - -export default digestNameMap; diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 51163e18001..82818e07d9f 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,16 +1,14 @@ import { Crypto, CryptoBuffer, Digest, EncryptionResult, EncryptionParameters, CipherAlgorithm } from './types'; import { webcrypto } from 'crypto'; import { Buffer } from 'buffer'; -import digestNameMap from './constants'; const pbkdf2Raw = async (password: string, salt: Uint8Array, iterations: number, keylenBytes: number, digest: Digest) => { - const digestName = digestNameMap[digest]; const encoder = new TextEncoder(); const key = await webcrypto.subtle.importKey( 'raw', encoder.encode(password), { name: 'PBKDF2' }, false, ['deriveBits'], ); return Buffer.from(await webcrypto.subtle.deriveBits( - { name: 'PBKDF2', salt, iterations, hash: digestName }, key, keylenBytes * 8, + { name: 'PBKDF2', salt, iterations, hash: digest }, key, keylenBytes * 8, )); }; diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index b444704dd5d..0fa45e0fbcb 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -37,14 +37,12 @@ export interface CryptoBuffer extends Uint8Array { toString(encoding?: BufferEncoding, start?: number, end?: number): string; } -// From react-native-quick-crypto.HashAlgorithm, but use the hash name style in node:crypto +// A subset of react-native-quick-crypto.HashAlgorithm, supported by Web Crypto API export enum Digest { - sha1 = 'sha1', - sha224 = 'sha224', - sha256 = 'sha256', - sha384 = 'sha384', - sha512 = 'sha512', - ripemd160 = 'ripemd160', + sha1 = 'SHA-1', + sha256 = 'SHA-256', + sha384 = 'SHA-384', + sha512 = 'SHA-512', } export enum CipherAlgorithm { diff --git a/packages/tools/cspell/dictionary4.txt b/packages/tools/cspell/dictionary4.txt index ad3a258e759..cb2191383c9 100644 --- a/packages/tools/cspell/dictionary4.txt +++ b/packages/tools/cspell/dictionary4.txt @@ -123,6 +123,5 @@ BYTV keyval traineddata Famegear -ripemd rnqc owasp From 10c185fb7c5d651250e22ba790e32c6e57350ffa Mon Sep 17 00:00:00 2001 From: wh201906 Date: Thu, 22 Aug 2024 22:09:33 +0800 Subject: [PATCH 46/60] Less calculation and branches in loop --- packages/lib/services/e2ee/crypto.ts | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 82818e07d9f..bd45ab84fd9 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -55,14 +55,21 @@ const crypto: Crypto = { const maxChunkSize = 65536; const result = new Uint8Array(size); - if (size < maxChunkSize) { + if (size <= maxChunkSize) { webcrypto.getRandomValues(result); } else { const fullSizeChunk = new Uint8Array(maxChunkSize); - for (let offset = 0; offset < size; offset += maxChunkSize) { - const chunk = offset + maxChunkSize > size ? new Uint8Array(size - offset) : fullSizeChunk; - webcrypto.getRandomValues(chunk); - result.set(chunk, offset); + const lastChunkSize = size % maxChunkSize; + const maxOffset = size - lastChunkSize; + let offset = 0; + while (offset < maxOffset) { + webcrypto.getRandomValues(fullSizeChunk); + result.set(fullSizeChunk, offset); + offset += maxChunkSize; + } + if (lastChunkSize > 0) { + const lastChunk = webcrypto.getRandomValues(new Uint8Array(lastChunkSize)); + result.set(lastChunk, offset); } } return Buffer.from(result); From d56dfc04ff60fe25ce9ea3ed126dc8bbd5ddc32b Mon Sep 17 00:00:00 2001 From: wh201906 Date: Thu, 22 Aug 2024 22:18:52 +0800 Subject: [PATCH 47/60] Remove parameter validation in hot path The parameter validation here could slow down the process, and the Web Crypto API actually supports AES-128 and AES-192. https://developer.mozilla.org/en-US/docs/Web/API/AesKeyGenParams#instance_properties --- packages/lib/services/e2ee/crypto.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index bd45ab84fd9..5e0b3649f27 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,4 +1,4 @@ -import { Crypto, CryptoBuffer, Digest, EncryptionResult, EncryptionParameters, CipherAlgorithm } from './types'; +import { Crypto, CryptoBuffer, Digest, EncryptionResult, EncryptionParameters } from './types'; import { webcrypto } from 'crypto'; import { Buffer } from 'buffer'; @@ -42,12 +42,6 @@ const decryptRaw = async (data: Uint8Array, key: Uint8Array, iv: Uint8Array, aut }, loadedKey, data)); }; -const validateEncryptionParameters = ({ cipherAlgorithm }: EncryptionParameters) => { - if (cipherAlgorithm !== CipherAlgorithm.AES_256_GCM) { - throw new Error(`Unsupported cipherAlgorithm: ${cipherAlgorithm}. Must be AES 256 GCM.`); - } -}; - const crypto: Crypto = { randomBytes: async (size: number) => { @@ -76,7 +70,6 @@ const crypto: Crypto = { }, encrypt: async (password: string, salt: CryptoBuffer, data: CryptoBuffer, encryptionParameters: EncryptionParameters) => { - validateEncryptionParameters(encryptionParameters); // Parameters in EncryptionParameters won't appear in result const result: EncryptionResult = { @@ -99,7 +92,6 @@ const crypto: Crypto = { }, decrypt: async (password: string, data: EncryptionResult, encryptionParameters: EncryptionParameters) => { - validateEncryptionParameters(encryptionParameters); const salt = Buffer.from(data.salt, 'base64'); const iv = Buffer.from(data.iv, 'base64'); From ab22f0450e0dd5722b998ffe57663c1adafb9b7a Mon Sep 17 00:00:00 2001 From: wh201906 Date: Thu, 22 Aug 2024 23:12:25 +0800 Subject: [PATCH 48/60] Reduce overhead --- packages/app-mobile/services/e2ee/crypto.ts | 9 +++---- packages/lib/services/e2ee/crypto.ts | 26 ++++++--------------- 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index d03bc4d5ef0..13aecc03480 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -22,10 +22,10 @@ const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoB cipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); - const encryptedData = Buffer.concat([cipher.update(data), cipher.final()]); + const encryptedData = [cipher.update(data), cipher.final()]; const authTag = cipher.getAuthTag(); - return Buffer.concat([encryptedData, authTag]); + return Buffer.concat([encryptedData[0], encryptedData[1], authTag]); }; const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => { @@ -37,14 +37,11 @@ const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoB decipher.setAuthTag(authTag); decipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) }); - let decryptedData = null; try { - decryptedData = Buffer.concat([decipher.update(encryptedData), decipher.final()]); + return Buffer.concat([decipher.update(encryptedData), decipher.final()]); } catch (error) { throw new Error(`Authentication failed! ${error}`); } - - return decryptedData; }; const crypto: Crypto = { diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 5e0b3649f27..b3e6d0d2c05 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -5,41 +5,29 @@ import { Buffer } from 'buffer'; const pbkdf2Raw = async (password: string, salt: Uint8Array, iterations: number, keylenBytes: number, digest: Digest) => { const encoder = new TextEncoder(); const key = await webcrypto.subtle.importKey( - 'raw', encoder.encode(password), { name: 'PBKDF2' }, false, ['deriveBits'], + 'raw', encoder.encode(password), { name: 'PBKDF2' }, false, ['deriveKey'], ); - return Buffer.from(await webcrypto.subtle.deriveBits( - { name: 'PBKDF2', salt, iterations, hash: digest }, key, keylenBytes * 8, - )); -}; - -const loadEncryptDecryptKey = async (keyData: Uint8Array) => { - return await webcrypto.subtle.importKey( - 'raw', - keyData, - { name: 'AES-GCM' }, - false, - ['encrypt', 'decrypt'], + return webcrypto.subtle.deriveKey( + { name: 'PBKDF2', salt, iterations, hash: digest }, key, { name: 'AES-GCM', length: keylenBytes * 8 }, false, ['encrypt', 'decrypt'], ); }; -const encryptRaw = async (data: Uint8Array, key: Uint8Array, iv: Uint8Array, authTagLengthBytes: number, additionalData: Uint8Array) => { - const loadedKey = await loadEncryptDecryptKey(key); +const encryptRaw = async (data: Uint8Array, key: webcrypto.CryptoKey, iv: Uint8Array, authTagLengthBytes: number, additionalData: Uint8Array) => { return Buffer.from(await webcrypto.subtle.encrypt({ name: 'AES-GCM', iv, additionalData, tagLength: authTagLengthBytes * 8, - }, loadedKey, data)); + }, key, data)); }; -const decryptRaw = async (data: Uint8Array, key: Uint8Array, iv: Uint8Array, authTagLengthBytes: number, associatedData: Uint8Array) => { - const loadedKey = await loadEncryptDecryptKey(key); +const decryptRaw = async (data: Uint8Array, key: webcrypto.CryptoKey, iv: Uint8Array, authTagLengthBytes: number, associatedData: Uint8Array) => { return Buffer.from(await webcrypto.subtle.decrypt({ name: 'AES-GCM', iv, additionalData: associatedData, tagLength: authTagLengthBytes * 8, - }, loadedKey, data)); + }, key, data)); }; const crypto: Crypto = { From 701c313d5e9750e3db87de82811e91f742fd1149 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Thu, 22 Aug 2024 23:21:03 +0800 Subject: [PATCH 49/60] Clean up .eslintrc.js --- .eslintrc.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.eslintrc.js b/.eslintrc.js index 75d01cc4d2c..7e4a0632cd6 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -291,7 +291,7 @@ module.exports = { selector: 'enumMember', format: null, 'filter': { - 'regex': '^(sha1|sha224|sha256|sha384|sha512|ripemd160|AES_128_GCM|AES_192_GCM|AES_256_GCM)$', + 'regex': '^(sha1|sha256|sha384|sha512|AES_128_GCM|AES_192_GCM|AES_256_GCM)$', 'match': true, }, }, From b637071fd451a226a2595dad55b357167d81f193 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sat, 24 Aug 2024 00:44:40 +0800 Subject: [PATCH 50/60] Enhance nonce generation Introduce timestamp and counter to avoid collision --- packages/app-mobile/services/e2ee/crypto.ts | 41 +++++++++ .../lib/services/e2ee/EncryptionService.ts | 27 ++++-- packages/lib/services/e2ee/crypto.test.ts | 86 ++++++++++++++++++- packages/lib/services/e2ee/crypto.ts | 40 +++++++++ packages/lib/services/e2ee/cryptoTestUtils.ts | 4 +- packages/lib/services/e2ee/types.ts | 3 + 6 files changed, 191 insertions(+), 10 deletions(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 13aecc03480..02c5f68eecf 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -3,6 +3,8 @@ import QuickCrypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; import type { CipherGCMOptions, CipherGCM, DecipherGCM } from 'crypto'; +const nonceCounterLength = 8; +const nonceTimestampLength = 7; const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest): Promise => { return new Promise((resolve, reject) => { @@ -58,6 +60,12 @@ const crypto: Crypto = { }); }, + digest: async (algorithm: Digest, data: Uint8Array) => { + const hash = QuickCrypto.createHash(algorithm); + hash.update(data); + return hash.digest(); + }, + encrypt: async (password: string, salt: CryptoBuffer, data: CryptoBuffer, encryptionParameters: EncryptionParameters) => { // Parameters in EncryptionParameters won't appear in result @@ -94,6 +102,39 @@ const crypto: Crypto = { encryptString: async (password: string, salt: CryptoBuffer, data: string, encoding: BufferEncoding, encryptionParameters: EncryptionParameters) => { return crypto.encrypt(password, salt, Buffer.from(data, encoding), encryptionParameters); }, + + generateNonce: async (nonce: Uint8Array) => { + const randomLength = nonce.length - nonceTimestampLength - nonceCounterLength; + if (randomLength < 1) { + throw new Error(`Nonce length should be greater than ${(nonceTimestampLength + nonceCounterLength) * 8} bits`); + } + nonce.set(await crypto.randomBytes(randomLength)); + const timestampArray = new Uint8Array(nonceTimestampLength); + let timestamp = Date.now(); + for (let i = 0; i < nonceTimestampLength; i++) { + timestampArray[i] = timestamp & 0xFF; + timestamp >>= 8; + } + nonce.set(timestampArray, randomLength); + nonce.set(new Uint8Array(nonceCounterLength), randomLength + nonceTimestampLength); + return nonce; + }, + + increaseNonce: async (nonce: Uint8Array) => { + const carry = 1; + const end = nonce.length - nonceCounterLength; + let i = nonce.length; + while (i-- > end) { + nonce[i] += carry; + if (nonce[i] !== 0 || carry !== 1) { + break; + } + } + if (i < end) { + await crypto.generateNonce(nonce); + } + return nonce; + }, }; export default crypto; diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index fb058f21295..4b4a02bc28b 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -69,6 +69,8 @@ export default class EncryptionService { public defaultFileEncryptionMethod_ = Setting.value('featureFlag.useBetaEncryptionMethod') ? EncryptionMethod.FileV1 : EncryptionMethod.SJCL1a; // public because used in tests private defaultMasterKeyEncryptionMethod_ = Setting.value('featureFlag.useBetaEncryptionMethod') ? EncryptionMethod.KeyV1 : EncryptionMethod.SJCL4; + private encryptionNonce_: Uint8Array = null; + private headerTemplates_ = { // Template version 1 1: { @@ -77,6 +79,15 @@ export default class EncryptionService { }, }; + public constructor() { + const crypto = shim.crypto; + crypto.generateNonce(new Uint8Array(32)) + // eslint-disable-next-line promise/prefer-await-to-then + .then((nonce) => this.encryptionNonce_ = nonce) + // eslint-disable-next-line promise/prefer-await-to-then + .catch((error) => logger.error(error)); + } + public static instance() { if (this.instance_) return this.instance_; this.instance_ = new EncryptionService(); @@ -425,7 +436,7 @@ export default class EncryptionService { // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem // 2024-08: Set iteration count in pbkdf2 to 220000 as suggested by OWASP. https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 [EncryptionMethod.KeyV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, await crypto.randomBytes(32), plainText, 'hex', { + return JSON.stringify(await crypto.encryptString(key, await crypto.digest(Digest.sha256, this.encryptionNonce_), plainText, 'hex', { cipherAlgorithm: CipherAlgorithm.AES_256_GCM, authTagLength: 16, digestAlgorithm: Digest.sha512, @@ -439,26 +450,26 @@ export default class EncryptionService { // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem // The file content is base64 encoded. Decoding it before encryption to reduce the size overhead. [EncryptionMethod.FileV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, await crypto.randomBytes(32), plainText, 'base64', { + return JSON.stringify(await crypto.encryptString(key, await crypto.digest(Digest.sha256, this.encryptionNonce_), plainText, 'base64', { cipherAlgorithm: CipherAlgorithm.AES_256_GCM, authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, associatedData: emptyUint8Array, - iterationCount: 5, + iterationCount: 3, })); }, // New encryption method powered by native crypto libraries(node:crypto/react-native-quick-crypto). Using AES-256-GCM and pbkdf2 // The master key is not directly used. A new data key is generated from the master key and a 256 bits random salt to prevent nonce reuse problem [EncryptionMethod.StringV1]: async () => { - return JSON.stringify(await crypto.encryptString(key, await crypto.randomBytes(32), plainText, 'utf16le', { + return JSON.stringify(await crypto.encryptString(key, await crypto.digest(Digest.sha256, this.encryptionNonce_), plainText, 'utf16le', { cipherAlgorithm: CipherAlgorithm.AES_256_GCM, authTagLength: 16, digestAlgorithm: Digest.sha512, keyLength: 32, associatedData: emptyUint8Array, - iterationCount: 5, + iterationCount: 3, })); }, @@ -493,7 +504,7 @@ export default class EncryptionService { digestAlgorithm: Digest.sha512, keyLength: 32, associatedData: emptyUint8Array, - iterationCount: 5, + iterationCount: 3, })).toString('base64'); } else if (method === EncryptionMethod.StringV1) { return (await crypto.decrypt(key, JSON.parse(cipherText), { @@ -502,7 +513,7 @@ export default class EncryptionService { digestAlgorithm: Digest.sha512, keyLength: 32, associatedData: emptyUint8Array, - iterationCount: 5, + iterationCount: 3, })).toString('utf16le'); } else if (this.isValidSjclEncryptionMethod(method)) { try { @@ -530,6 +541,7 @@ export default class EncryptionService { const masterKeyId = options.masterKeyId ? options.masterKeyId : this.activeMasterKeyId(); const masterKeyPlainText = this.loadedMasterKey(masterKeyId).plainText; const chunkSize = this.chunkSize(method); + const crypto = shim.crypto; const header = { encryptionMethod: method, @@ -552,6 +564,7 @@ export default class EncryptionService { await shim.waitForFrame(); const encrypted = await this.encrypt(method, masterKeyPlainText, block); + await crypto.increaseNonce(this.encryptionNonce_); await destination.append(padLeft(encrypted.length.toString(16), 6, '0')); await destination.append(encrypted); diff --git a/packages/lib/services/e2ee/crypto.test.ts b/packages/lib/services/e2ee/crypto.test.ts index 86c4a3719f2..41f1510b925 100644 --- a/packages/lib/services/e2ee/crypto.test.ts +++ b/packages/lib/services/e2ee/crypto.test.ts @@ -1,9 +1,11 @@ import { afterAllCleanUp, expectNotThrow, setupDatabaseAndSynchronizer, switchClient } from '../../testing/test-utils'; import { runIntegrationTests } from './cryptoTestUtils'; +import crypto from './crypto'; describe('e2ee/crypto', () => { beforeEach(async () => { + jest.useRealTimers(); await setupDatabaseAndSynchronizer(1); await switchClient(1); }); @@ -12,8 +14,90 @@ describe('e2ee/crypto', () => { await afterAllCleanUp(); }); - it('should decrypt and encrypt data from different devices', (async () => { + it('should decrypt data from different devices', (async () => { await expectNotThrow(async () => runIntegrationTests(true)); })); + it('should not generate new nonce if counter does not overflow', (async () => { + jest.useFakeTimers(); + + const nonce = await crypto.generateNonce(new Uint8Array(32)); + expect(nonce.subarray(-8)).toEqual(new Uint8Array(8)); + const nonCounterPart = nonce.slice(0, 24); + + jest.advanceTimersByTime(1); + await crypto.increaseNonce(nonce); + // Counter should have expected value + expect(nonce.subarray(-8)).toEqual(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1])); + // Non-counter part should stay the same + expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + + jest.advanceTimersByTime(1); + await crypto.increaseNonce(nonce); + // Counter should have expected value + expect(nonce.subarray(-8)).toEqual(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2])); + // Non-counter part should stay the same + expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + + jest.advanceTimersByTime(1); + nonce.set(new Uint8Array([248, 249, 250, 251, 252, 253, 254, 255]), 24); + await crypto.increaseNonce(nonce); + // Counter should have expected value + expect(nonce.subarray(-8)).toEqual(new Uint8Array([248, 249, 250, 251, 252, 253, 255, 0])); + // Non-counter part should stay the same + expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + + jest.advanceTimersByTime(1); + nonce.set(new Uint8Array([249, 250, 251, 252, 253, 254, 255, 255]), 24); + await crypto.increaseNonce(nonce); + // Counter should have expected value + expect(nonce.subarray(-8)).toEqual(new Uint8Array([249, 250, 251, 252, 253, 255, 0, 0])); + // Non-counter part should stay the same + expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + + jest.advanceTimersByTime(1); + nonce.set(new Uint8Array([253, 254, 255, 255, 255, 255, 255, 255]), 24); + await crypto.increaseNonce(nonce); + // Counter should have expected value + expect(nonce.subarray(-8)).toEqual(new Uint8Array([253, 255, 0, 0, 0, 0, 0, 0])); + // Non-counter part should stay the same + expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + + jest.advanceTimersByTime(1); + nonce.set(new Uint8Array([254, 255, 255, 255, 255, 255, 255, 255]), 24); + await crypto.increaseNonce(nonce); + // Counter should have expected value + expect(nonce.subarray(-8)).toEqual(new Uint8Array([255, 0, 0, 0, 0, 0, 0, 0])); + // Non-counter part should stay the same + expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + })); + + it('should generate new nonce if counter overflow', (async () => { + jest.useFakeTimers(); + + const nonce = await crypto.generateNonce(new Uint8Array(32)); + expect(nonce.subarray(-8)).toEqual(new Uint8Array(8)); + const nonCounterPart = nonce.slice(0, 24); + const randomPart = nonce.slice(0, 17); + const timestampPart = nonce.slice(17, 24); + + jest.advanceTimersByTime(1); + await crypto.increaseNonce(nonce); + // Counter should have expected value + expect(nonce.subarray(-8)).toEqual(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1])); + // Non-counter part should stay the same + expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + + jest.advanceTimersByTime(1); + nonce.set(new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255]), 24); + await crypto.increaseNonce(nonce); + // Counter should have expected value + expect(nonce.subarray(-8)).toEqual(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0])); + // Random part should be changed + expect(nonce.subarray(0, 17)).not.toEqual(randomPart); + // Timestamp part should have expected value + expect(nonce[17]).toBe(timestampPart[0] + 2); + expect(nonce.subarray(18, 24)).toEqual(timestampPart.subarray(1)); + })); + }); diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index b3e6d0d2c05..84901e43983 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -2,6 +2,9 @@ import { Crypto, CryptoBuffer, Digest, EncryptionResult, EncryptionParameters } import { webcrypto } from 'crypto'; import { Buffer } from 'buffer'; +const nonceCounterLength = 8; +const nonceTimestampLength = 7; + const pbkdf2Raw = async (password: string, salt: Uint8Array, iterations: number, keylenBytes: number, digest: Digest) => { const encoder = new TextEncoder(); const key = await webcrypto.subtle.importKey( @@ -57,6 +60,10 @@ const crypto: Crypto = { return Buffer.from(result); }, + digest: async (algorithm: Digest, data: Uint8Array) => { + return Buffer.from(await webcrypto.subtle.digest(algorithm, data)); + }, + encrypt: async (password: string, salt: CryptoBuffer, data: CryptoBuffer, encryptionParameters: EncryptionParameters) => { // Parameters in EncryptionParameters won't appear in result @@ -93,6 +100,39 @@ const crypto: Crypto = { encryptString: async (password: string, salt: CryptoBuffer, data: string, encoding: BufferEncoding, encryptionParameters: EncryptionParameters) => { return crypto.encrypt(password, salt, Buffer.from(data, encoding), encryptionParameters); }, + + generateNonce: async (nonce: Uint8Array) => { + const randomLength = nonce.length - nonceTimestampLength - nonceCounterLength; + if (randomLength < 1) { + throw new Error(`Nonce length should be greater than ${(nonceTimestampLength + nonceCounterLength) * 8} bits`); + } + nonce.set(await crypto.randomBytes(randomLength)); + const timestampArray = new Uint8Array(nonceTimestampLength); + let timestamp = Date.now(); + for (let i = 0; i < nonceTimestampLength; i++) { + timestampArray[i] = timestamp & 0xFF; + timestamp >>= 8; + } + nonce.set(timestampArray, randomLength); + nonce.set(new Uint8Array(nonceCounterLength), randomLength + nonceTimestampLength); + return nonce; + }, + + increaseNonce: async (nonce: Uint8Array) => { + const carry = 1; + const end = nonce.length - nonceCounterLength; + let i = nonce.length; + while (i-- > end) { + nonce[i] += carry; + if (nonce[i] !== 0 || carry !== 1) { + break; + } + } + if (i < end) { + await crypto.generateNonce(nonce); + } + return nonce; + }, }; export default crypto; diff --git a/packages/lib/services/e2ee/cryptoTestUtils.ts b/packages/lib/services/e2ee/cryptoTestUtils.ts index 3509cf975e0..18651c6a801 100644 --- a/packages/lib/services/e2ee/cryptoTestUtils.ts +++ b/packages/lib/services/e2ee/cryptoTestUtils.ts @@ -206,7 +206,7 @@ const decryptTestData: Record = { method: EncryptionMethod.StringV1, password: '4BfJl8YbM,nXx.LVgs!AzkWWA]', plaintext: '中文にっぽんご한국어😀\uD83D\0\r\nenglish01234567890', - ciphertext: '{"salt":"6A3HyEPrxxNEgDq1m43BRrzEehxaIpcRRABBCWX3z9o=","iv":"FZNIQvkHiHolFdK7","ct":"uea3tJg8VJ/JuC2H62cOfEOeMVjLw0trphr4F08VsmKNpv46uYtZJHSx5Jz/5sP/MHNPsmFqXcxzrSdYFOLjRhqyxf71KFFpxGFJ44nMwwRbKgUw"}', + ciphertext: '{"salt":"6NKebMdcrFSGSzEWusbY8JUfG9rB98PxNZtk0QkYEFQ=","iv":"zHdXu8V5SYsO+vR/","ct":"4C3uyjOtRsNQZxCECvCeRaP+oPXMxjUMxsND67odnyiFg2A+fG6QW6O8axb6RHWU7QKHRG9/kHEs283DHL3hOAJbl4LS47R/dEDJbl8kWmGtLAsn"}', }, hexKey: { method: EncryptionMethod.KeyV1, @@ -218,7 +218,7 @@ const decryptTestData: Record = { method: EncryptionMethod.FileV1, password: '4BfJl8YbM,nXx.LVgs!AzkWWA]', plaintext: '5Lit5paH44Gr44Gj44G944KT44GU7ZWc6rWt7Ja08J+YgO+/vQANCmVuZ2xpc2gwMTIzNDU2Nzg5MA==', - ciphertext: '{"salt":"wG/hFCK2IZTgi0JR5D1eZr09vnS17hDftWYim0FkU2w=","iv":"bpjlR7owtQzMYJwg","ct":"/CoRA2X3aiPaWdtkotcacpiutN3YJh0ebzplP1bOsukzeqA4ZgbkhKxMLg841nuNq1T8NoX+cQASHMdO5DNzrQP8raVLywd3MIg="}', + ciphertext: '{"salt":"19Zx/+hxpZ+Trc7MGBt837SsTOJjHe9aiY5UPnXP6Oo=","iv":"N4PTzsyh4wONNJWa","ct":"LxAibOnVox1q2WBtLAKxeZxIIxKOEd6xdD3NKAn5mgHhv4i60yPiyPbr8rS+MzHmeq7Z3BhHjR7540rtdeBugbmf1+b3tYuRudI="}', }, }; diff --git a/packages/lib/services/e2ee/types.ts b/packages/lib/services/e2ee/types.ts index 0fa45e0fbcb..5a5ed7f20b9 100644 --- a/packages/lib/services/e2ee/types.ts +++ b/packages/lib/services/e2ee/types.ts @@ -28,6 +28,9 @@ export interface RSA { export interface Crypto { randomBytes(size: number): Promise; + digest(algorithm: Digest, data: Uint8Array): Promise; + generateNonce(nonce: Uint8Array): Promise; + increaseNonce(nonce: Uint8Array): Promise; encrypt(password: string, salt: CryptoBuffer, data: CryptoBuffer, options: EncryptionParameters): Promise; decrypt(password: string, data: EncryptionResult, options: EncryptionParameters): Promise; encryptString(password: string, salt: CryptoBuffer, data: string, encoding: BufferEncoding, options: EncryptionParameters): Promise; From cb9b077b433a83e6422aa773fde60c966a731e4d Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sat, 24 Aug 2024 10:47:23 +0800 Subject: [PATCH 51/60] Add digest name mapping for QuickCrypto.createHash() --- packages/app-mobile/services/e2ee/crypto.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index 02c5f68eecf..a0bdc096242 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -6,6 +6,14 @@ import type { CipherGCMOptions, CipherGCM, DecipherGCM } from 'crypto'; const nonceCounterLength = 8; const nonceTimestampLength = 7; +type DigestNameMap = Record; +const digestNameMap: DigestNameMap = { + [Digest.sha1]: 'sha1', + [Digest.sha256]: 'sha256', + [Digest.sha384]: 'sha384', + [Digest.sha512]: 'sha512', +}; + const pbkdf2Raw = (password: string, salt: CryptoBuffer, iterations: number, keylen: number, digest: Digest): Promise => { return new Promise((resolve, reject) => { QuickCrypto.pbkdf2(password, salt, iterations, keylen, digest as HashAlgorithm, (error, result) => { @@ -61,7 +69,7 @@ const crypto: Crypto = { }, digest: async (algorithm: Digest, data: Uint8Array) => { - const hash = QuickCrypto.createHash(algorithm); + const hash = QuickCrypto.createHash(digestNameMap[algorithm]); hash.update(data); return hash.digest(); }, From 1a2c3d2415a50142bfa9c52f9cbf3ada1cf88370 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sat, 24 Aug 2024 12:06:06 +0800 Subject: [PATCH 52/60] Reduce chunk size to prevent potential freezing --- packages/lib/services/e2ee/EncryptionService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index 4b4a02bc28b..cf07ddc6030 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -128,7 +128,7 @@ export default class EncryptionService { [EncryptionMethod.SJCL4]: 5000, [EncryptionMethod.Custom]: 5000, [EncryptionMethod.KeyV1]: 5000, // Master key is not encrypted by chunks so this value will not be used. - [EncryptionMethod.FileV1]: 524288, // 512k + [EncryptionMethod.FileV1]: 131072, // 128k [EncryptionMethod.StringV1]: 65536, // 64k }; From 2855ec7322d820262fa2828b37ab9fc78c679fc4 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Sat, 24 Aug 2024 17:44:01 +0800 Subject: [PATCH 53/60] Increase nonce size before hashing --- .../lib/services/e2ee/EncryptionService.ts | 2 +- packages/lib/services/e2ee/crypto.test.ts | 42 +++++++++---------- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/lib/services/e2ee/EncryptionService.ts b/packages/lib/services/e2ee/EncryptionService.ts index cf07ddc6030..dc14f4e4aa3 100644 --- a/packages/lib/services/e2ee/EncryptionService.ts +++ b/packages/lib/services/e2ee/EncryptionService.ts @@ -81,7 +81,7 @@ export default class EncryptionService { public constructor() { const crypto = shim.crypto; - crypto.generateNonce(new Uint8Array(32)) + crypto.generateNonce(new Uint8Array(36)) // eslint-disable-next-line promise/prefer-await-to-then .then((nonce) => this.encryptionNonce_ = nonce) // eslint-disable-next-line promise/prefer-await-to-then diff --git a/packages/lib/services/e2ee/crypto.test.ts b/packages/lib/services/e2ee/crypto.test.ts index 41f1510b925..76f50e69a21 100644 --- a/packages/lib/services/e2ee/crypto.test.ts +++ b/packages/lib/services/e2ee/crypto.test.ts @@ -21,83 +21,83 @@ describe('e2ee/crypto', () => { it('should not generate new nonce if counter does not overflow', (async () => { jest.useFakeTimers(); - const nonce = await crypto.generateNonce(new Uint8Array(32)); + const nonce = await crypto.generateNonce(new Uint8Array(36)); expect(nonce.subarray(-8)).toEqual(new Uint8Array(8)); - const nonCounterPart = nonce.slice(0, 24); + const nonCounterPart = nonce.slice(0, 28); jest.advanceTimersByTime(1); await crypto.increaseNonce(nonce); // Counter should have expected value expect(nonce.subarray(-8)).toEqual(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1])); // Non-counter part should stay the same - expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + expect(nonce.subarray(0, 28)).toEqual(nonCounterPart); jest.advanceTimersByTime(1); await crypto.increaseNonce(nonce); // Counter should have expected value expect(nonce.subarray(-8)).toEqual(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 2])); // Non-counter part should stay the same - expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + expect(nonce.subarray(0, 28)).toEqual(nonCounterPart); jest.advanceTimersByTime(1); - nonce.set(new Uint8Array([248, 249, 250, 251, 252, 253, 254, 255]), 24); + nonce.set(new Uint8Array([248, 249, 250, 251, 252, 253, 254, 255]), 28); await crypto.increaseNonce(nonce); // Counter should have expected value expect(nonce.subarray(-8)).toEqual(new Uint8Array([248, 249, 250, 251, 252, 253, 255, 0])); // Non-counter part should stay the same - expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + expect(nonce.subarray(0, 28)).toEqual(nonCounterPart); jest.advanceTimersByTime(1); - nonce.set(new Uint8Array([249, 250, 251, 252, 253, 254, 255, 255]), 24); + nonce.set(new Uint8Array([249, 250, 251, 252, 253, 254, 255, 255]), 28); await crypto.increaseNonce(nonce); // Counter should have expected value expect(nonce.subarray(-8)).toEqual(new Uint8Array([249, 250, 251, 252, 253, 255, 0, 0])); // Non-counter part should stay the same - expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + expect(nonce.subarray(0, 28)).toEqual(nonCounterPart); jest.advanceTimersByTime(1); - nonce.set(new Uint8Array([253, 254, 255, 255, 255, 255, 255, 255]), 24); + nonce.set(new Uint8Array([253, 254, 255, 255, 255, 255, 255, 255]), 28); await crypto.increaseNonce(nonce); // Counter should have expected value expect(nonce.subarray(-8)).toEqual(new Uint8Array([253, 255, 0, 0, 0, 0, 0, 0])); // Non-counter part should stay the same - expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + expect(nonce.subarray(0, 28)).toEqual(nonCounterPart); jest.advanceTimersByTime(1); - nonce.set(new Uint8Array([254, 255, 255, 255, 255, 255, 255, 255]), 24); + nonce.set(new Uint8Array([254, 255, 255, 255, 255, 255, 255, 255]), 28); await crypto.increaseNonce(nonce); // Counter should have expected value expect(nonce.subarray(-8)).toEqual(new Uint8Array([255, 0, 0, 0, 0, 0, 0, 0])); // Non-counter part should stay the same - expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + expect(nonce.subarray(0, 28)).toEqual(nonCounterPart); })); it('should generate new nonce if counter overflow', (async () => { jest.useFakeTimers(); - const nonce = await crypto.generateNonce(new Uint8Array(32)); + const nonce = await crypto.generateNonce(new Uint8Array(36)); expect(nonce.subarray(-8)).toEqual(new Uint8Array(8)); - const nonCounterPart = nonce.slice(0, 24); - const randomPart = nonce.slice(0, 17); - const timestampPart = nonce.slice(17, 24); + const nonCounterPart = nonce.slice(0, 28); + const randomPart = nonce.slice(0, 21); + const timestampPart = nonce.slice(21, 28); jest.advanceTimersByTime(1); await crypto.increaseNonce(nonce); // Counter should have expected value expect(nonce.subarray(-8)).toEqual(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 1])); // Non-counter part should stay the same - expect(nonce.subarray(0, 24)).toEqual(nonCounterPart); + expect(nonce.subarray(0, 28)).toEqual(nonCounterPart); jest.advanceTimersByTime(1); - nonce.set(new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255]), 24); + nonce.set(new Uint8Array([255, 255, 255, 255, 255, 255, 255, 255]), 28); await crypto.increaseNonce(nonce); // Counter should have expected value expect(nonce.subarray(-8)).toEqual(new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0])); // Random part should be changed - expect(nonce.subarray(0, 17)).not.toEqual(randomPart); + expect(nonce.subarray(0, 21)).not.toEqual(randomPart); // Timestamp part should have expected value - expect(nonce[17]).toBe(timestampPart[0] + 2); - expect(nonce.subarray(18, 24)).toEqual(timestampPart.subarray(1)); + expect(nonce[21]).toBe(timestampPart[0] + 2); + expect(nonce.subarray(22, 28)).toEqual(timestampPart.subarray(1)); })); }); From 7dc4eec8059f06ee3b702bda4b6a10b9e0cfcad1 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Wed, 28 Aug 2024 11:31:04 +0800 Subject: [PATCH 54/60] Replace console with logger in crypto integration tests --- packages/lib/services/e2ee/cryptoTestUtils.ts | 49 +++++++++---------- 1 file changed, 24 insertions(+), 25 deletions(-) diff --git a/packages/lib/services/e2ee/cryptoTestUtils.ts b/packages/lib/services/e2ee/cryptoTestUtils.ts index 18651c6a801..0c56f9311f5 100644 --- a/packages/lib/services/e2ee/cryptoTestUtils.ts +++ b/packages/lib/services/e2ee/cryptoTestUtils.ts @@ -6,6 +6,7 @@ import Note from '../../models/Note'; import Setting from '../../models/Setting'; import shim from '../..//shim'; import { getEncryptionEnabled, setEncryptionEnabled } from '../synchronizer/syncInfoUtils'; +import Logger from '@joplin/utils/Logger'; interface DecryptTestData { method: EncryptionMethod; @@ -16,6 +17,8 @@ interface DecryptTestData { let serviceInstance: EncryptionService = null; +const logger = Logger.create('Crypto Tests'); + // This is convenient to quickly generate some data to verify for example that // react-native-quick-crypto and node:crypto can decrypt the same data. export async function createDecryptTestData() { @@ -52,16 +55,16 @@ export async function checkDecryptTestData(data: DecryptTestData, options: Check try { const decrypted = await EncryptionService.instance().decrypt(data.method, data.password, data.ciphertext); if (decrypted !== data.plaintext) { - messages.push('Crypto Tests: Data could not be decrypted'); - messages.push('Crypto Tests: Expected:', data.plaintext); - messages.push('Crypto Tests: Got:', decrypted); + messages.push('Data could not be decrypted'); + messages.push('Expected:', data.plaintext); + messages.push('Got:', decrypted); hasError = true; } else { - messages.push('Crypto Tests: Data could be decrypted'); + messages.push('Data could be decrypted'); } } catch (error) { hasError = true; - messages.push(`Crypto Tests: Failed to decrypt data: Error: ${error}`); + messages.push(`Failed to decrypt data: Error: ${error}`); } if (hasError && options.throwOnError) { @@ -70,10 +73,9 @@ export async function checkDecryptTestData(data: DecryptTestData, options: Check } else { for (const msg of messages) { if (hasError) { - console.warn(msg); + logger.warn(msg); } else { - // eslint-disable-next-line no-console - if (!options.silent) console.info(msg); + if (!options.silent) logger.info(msg); } } } @@ -116,11 +118,11 @@ export async function testStringPerformance(method: EncryptionMethod, dataSize: decryptTime += tick3 - tick2; } - messages.push(`Crypto Tests: testStringPerformance(): method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`); + messages.push(`testStringPerformance(): method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`); } catch (error) { hasError = true; - messages.push(`Crypto Tests: testStringPerformance() failed. Error: ${error}`); + messages.push(`testStringPerformance() failed. Error: ${error}`); } if (hasError && options.throwOnError) { @@ -129,10 +131,9 @@ export async function testStringPerformance(method: EncryptionMethod, dataSize: } else { for (const msg of messages) { if (hasError) { - console.warn(msg); + logger.warn(msg); } else { - // eslint-disable-next-line no-console - if (!options.silent) console.info(msg); + if (!options.silent) logger.info(msg); } } } @@ -176,11 +177,11 @@ export async function testFilePerformance(method: EncryptionMethod, dataSize: nu decryptTime += tick3 - tick2; } - messages.push(`Crypto Tests: testFilePerformance(): method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`); + messages.push(`testFilePerformance(): method: ${method}, count: ${count}, dataSize: ${dataSize}, encryptTime: ${encryptTime}, decryptTime: ${decryptTime}, encryptTime/count: ${encryptTime / count}, decryptTime/count: ${decryptTime / count}.`); } catch (error) { hasError = true; - messages.push(`Crypto Tests: testFilePerformance() failed. Error: ${error}`); + messages.push(`testFilePerformance() failed. Error: ${error}`); } if (hasError && options.throwOnError) { @@ -189,10 +190,9 @@ export async function testFilePerformance(method: EncryptionMethod, dataSize: nu } else { for (const msg of messages) { if (hasError) { - console.warn(msg); + logger.warn(msg); } else { - // eslint-disable-next-line no-console - if (!options.silent) console.info(msg); + if (!options.silent) logger.info(msg); } } } @@ -229,29 +229,28 @@ const decryptTestData: Record = { export const runIntegrationTests = async (silent = false, testPerformance = false) => { const log = (s: string) => { if (silent) return; - // eslint-disable-next-line no-console - console.info(s); + logger.info(s); }; - log('Crypto Tests: Running integration tests...'); + log('Running integration tests...'); const encryptionEnabled = getEncryptionEnabled(); serviceInstance = EncryptionService.instance(); BaseItem.encryptionService_ = EncryptionService.instance(); setEncryptionEnabled(true); - log('Crypto Tests: Decrypting using known data...'); + log('Decrypting using known data...'); for (const testLabel in decryptTestData) { - log(`Crypto Tests: Running decrypt test data case ${testLabel}...`); + log(`Running decrypt test data case ${testLabel}...`); await checkDecryptTestData(decryptTestData[testLabel], { silent, testLabel, throwOnError: true }); } - log('Crypto Tests: Decrypting using local data...'); + log('Decrypting using local data...'); const newData = await createDecryptTestData(); await checkDecryptTestData(newData, { silent, throwOnError: true }); // The performance test is very slow so it is disabled by default. if (testPerformance) { - log('Crypto Tests: Testing performance...'); + log('Testing performance...'); if (shim.mobilePlatform() === '') { await testStringPerformance(EncryptionMethod.StringV1, 100, 1000); await testStringPerformance(EncryptionMethod.StringV1, 1000000, 10); From 9a7b3103a5c8b34e9e9b8f150c41340add80acab Mon Sep 17 00:00:00 2001 From: wh201906 Date: Wed, 28 Aug 2024 16:15:18 +0800 Subject: [PATCH 55/60] Move duplicate code to cryptoShared.ts --- .eslintignore | 1 + .gitignore | 1 + packages/app-mobile/services/e2ee/crypto.ts | 43 +++++--------------- packages/lib/services/e2ee/crypto.ts | 43 +++++--------------- packages/lib/services/e2ee/cryptoShared.ts | 45 +++++++++++++++++++++ 5 files changed, 65 insertions(+), 68 deletions(-) create mode 100644 packages/lib/services/e2ee/cryptoShared.ts diff --git a/.eslintignore b/.eslintignore index 08d16ad328c..8abcc6fb270 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1075,6 +1075,7 @@ packages/lib/services/e2ee/EncryptionService.js packages/lib/services/e2ee/RSA.node.js packages/lib/services/e2ee/crypto.test.js packages/lib/services/e2ee/crypto.js +packages/lib/services/e2ee/cryptoShared.js packages/lib/services/e2ee/cryptoTestUtils.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js diff --git a/.gitignore b/.gitignore index b9a435f397d..05c2e7bada7 100644 --- a/.gitignore +++ b/.gitignore @@ -1052,6 +1052,7 @@ packages/lib/services/e2ee/EncryptionService.js packages/lib/services/e2ee/RSA.node.js packages/lib/services/e2ee/crypto.test.js packages/lib/services/e2ee/crypto.js +packages/lib/services/e2ee/cryptoShared.js packages/lib/services/e2ee/cryptoTestUtils.js packages/lib/services/e2ee/ppk.test.js packages/lib/services/e2ee/ppk.js diff --git a/packages/app-mobile/services/e2ee/crypto.ts b/packages/app-mobile/services/e2ee/crypto.ts index a0bdc096242..221103cfd93 100644 --- a/packages/app-mobile/services/e2ee/crypto.ts +++ b/packages/app-mobile/services/e2ee/crypto.ts @@ -2,9 +2,11 @@ import { Crypto, CryptoBuffer, Digest, CipherAlgorithm, EncryptionResult, Encryp import QuickCrypto from 'react-native-quick-crypto'; import { HashAlgorithm } from 'react-native-quick-crypto/lib/typescript/keys'; import type { CipherGCMOptions, CipherGCM, DecipherGCM } from 'crypto'; - -const nonceCounterLength = 8; -const nonceTimestampLength = 7; +import { + generateNonce as generateNonceShared, + increaseNonce as increaseNonceShared, + setRandomBytesImplementation, +} from '@joplin/lib/services/e2ee/cryptoShared'; type DigestNameMap = Record; const digestNameMap: DigestNameMap = { @@ -111,38 +113,11 @@ const crypto: Crypto = { return crypto.encrypt(password, salt, Buffer.from(data, encoding), encryptionParameters); }, - generateNonce: async (nonce: Uint8Array) => { - const randomLength = nonce.length - nonceTimestampLength - nonceCounterLength; - if (randomLength < 1) { - throw new Error(`Nonce length should be greater than ${(nonceTimestampLength + nonceCounterLength) * 8} bits`); - } - nonce.set(await crypto.randomBytes(randomLength)); - const timestampArray = new Uint8Array(nonceTimestampLength); - let timestamp = Date.now(); - for (let i = 0; i < nonceTimestampLength; i++) { - timestampArray[i] = timestamp & 0xFF; - timestamp >>= 8; - } - nonce.set(timestampArray, randomLength); - nonce.set(new Uint8Array(nonceCounterLength), randomLength + nonceTimestampLength); - return nonce; - }, + generateNonce: generateNonceShared, - increaseNonce: async (nonce: Uint8Array) => { - const carry = 1; - const end = nonce.length - nonceCounterLength; - let i = nonce.length; - while (i-- > end) { - nonce[i] += carry; - if (nonce[i] !== 0 || carry !== 1) { - break; - } - } - if (i < end) { - await crypto.generateNonce(nonce); - } - return nonce; - }, + increaseNonce: increaseNonceShared, }; +setRandomBytesImplementation(crypto.randomBytes); + export default crypto; diff --git a/packages/lib/services/e2ee/crypto.ts b/packages/lib/services/e2ee/crypto.ts index 84901e43983..2e0da083340 100644 --- a/packages/lib/services/e2ee/crypto.ts +++ b/packages/lib/services/e2ee/crypto.ts @@ -1,9 +1,11 @@ import { Crypto, CryptoBuffer, Digest, EncryptionResult, EncryptionParameters } from './types'; import { webcrypto } from 'crypto'; import { Buffer } from 'buffer'; - -const nonceCounterLength = 8; -const nonceTimestampLength = 7; +import { + generateNonce as generateNonceShared, + increaseNonce as increaseNonceShared, + setRandomBytesImplementation, +} from './cryptoShared'; const pbkdf2Raw = async (password: string, salt: Uint8Array, iterations: number, keylenBytes: number, digest: Digest) => { const encoder = new TextEncoder(); @@ -101,38 +103,11 @@ const crypto: Crypto = { return crypto.encrypt(password, salt, Buffer.from(data, encoding), encryptionParameters); }, - generateNonce: async (nonce: Uint8Array) => { - const randomLength = nonce.length - nonceTimestampLength - nonceCounterLength; - if (randomLength < 1) { - throw new Error(`Nonce length should be greater than ${(nonceTimestampLength + nonceCounterLength) * 8} bits`); - } - nonce.set(await crypto.randomBytes(randomLength)); - const timestampArray = new Uint8Array(nonceTimestampLength); - let timestamp = Date.now(); - for (let i = 0; i < nonceTimestampLength; i++) { - timestampArray[i] = timestamp & 0xFF; - timestamp >>= 8; - } - nonce.set(timestampArray, randomLength); - nonce.set(new Uint8Array(nonceCounterLength), randomLength + nonceTimestampLength); - return nonce; - }, + generateNonce: generateNonceShared, - increaseNonce: async (nonce: Uint8Array) => { - const carry = 1; - const end = nonce.length - nonceCounterLength; - let i = nonce.length; - while (i-- > end) { - nonce[i] += carry; - if (nonce[i] !== 0 || carry !== 1) { - break; - } - } - if (i < end) { - await crypto.generateNonce(nonce); - } - return nonce; - }, + increaseNonce: increaseNonceShared, }; +setRandomBytesImplementation(crypto.randomBytes); + export default crypto; diff --git a/packages/lib/services/e2ee/cryptoShared.ts b/packages/lib/services/e2ee/cryptoShared.ts new file mode 100644 index 00000000000..ae8c2b23816 --- /dev/null +++ b/packages/lib/services/e2ee/cryptoShared.ts @@ -0,0 +1,45 @@ +import { CryptoBuffer } from './types'; + +const nonceCounterLength = 8; +const nonceTimestampLength = 7; + +type RandomBytesImplementation = (size: number)=> Promise; + +let randomBytesImplementation: RandomBytesImplementation = null; + +export const setRandomBytesImplementation = (implementation: RandomBytesImplementation) => { + randomBytesImplementation = implementation; +}; + +export const generateNonce = async (nonce: Uint8Array) => { + const randomLength = nonce.length - nonceTimestampLength - nonceCounterLength; + if (randomLength < 1) { + throw new Error(`Nonce length should be greater than ${(nonceTimestampLength + nonceCounterLength) * 8} bits`); + } + nonce.set(await randomBytesImplementation(randomLength)); + const timestampArray = new Uint8Array(nonceTimestampLength); + let timestamp = Date.now(); + for (let i = 0; i < nonceTimestampLength; i++) { + timestampArray[i] = timestamp & 0xFF; + timestamp >>= 8; + } + nonce.set(timestampArray, randomLength); + nonce.set(new Uint8Array(nonceCounterLength), randomLength + nonceTimestampLength); + return nonce; +}; + +export const increaseNonce = async (nonce: Uint8Array) => { + const carry = 1; + const end = nonce.length - nonceCounterLength; + let i = nonce.length; + while (i-- > end) { + nonce[i] += carry; + if (nonce[i] !== 0 || carry !== 1) { + break; + } + } + if (i < end) { + await generateNonce(nonce); + } + return nonce; +}; From 2b42bbf2a193f21100c34197507ea772d96f7904 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Wed, 28 Aug 2024 16:23:21 +0800 Subject: [PATCH 56/60] Fix timestamp bytes in nonce --- packages/lib/services/e2ee/cryptoShared.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/packages/lib/services/e2ee/cryptoShared.ts b/packages/lib/services/e2ee/cryptoShared.ts index ae8c2b23816..829d0e5083f 100644 --- a/packages/lib/services/e2ee/cryptoShared.ts +++ b/packages/lib/services/e2ee/cryptoShared.ts @@ -19,9 +19,16 @@ export const generateNonce = async (nonce: Uint8Array) => { nonce.set(await randomBytesImplementation(randomLength)); const timestampArray = new Uint8Array(nonceTimestampLength); let timestamp = Date.now(); - for (let i = 0; i < nonceTimestampLength; i++) { + let timestampMsb = Math.floor(timestamp / 2 ** 32); + const lsbCount = Math.min(4, nonceTimestampLength); + for (let i = 0; i < lsbCount; i++) { timestampArray[i] = timestamp & 0xFF; - timestamp >>= 8; + timestamp >>>= 8; + } + // The bitwise operators in Typescript only take the 32 LSBs to calculate, so we need to extract the MSBs manually. + for (let i = 4; i < nonceTimestampLength; i++) { + timestampArray[i] = timestampMsb & 0xFF; + timestampMsb >>>= 8; } nonce.set(timestampArray, randomLength); nonce.set(new Uint8Array(nonceCounterLength), randomLength + nonceTimestampLength); From bf0d512353c6205db309bb3df17a1826a25a883c Mon Sep 17 00:00:00 2001 From: wh201906 Date: Fri, 6 Sep 2024 20:46:38 +0800 Subject: [PATCH 57/60] Clean up Fix description in crypto.test.ts Remove featureFlag.useBetaEncryptionMethod.advanced because it is set to true elsewhere --- packages/lib/models/settings/builtInMetadata.ts | 1 - packages/lib/services/e2ee/crypto.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/lib/models/settings/builtInMetadata.ts b/packages/lib/models/settings/builtInMetadata.ts index 6d93b5f9e73..eb9fbfb538c 100644 --- a/packages/lib/models/settings/builtInMetadata.ts +++ b/packages/lib/models/settings/builtInMetadata.ts @@ -1598,7 +1598,6 @@ const builtInMetadata = (Setting: typeof SettingType) => { storage: SettingStorage.File, label: () => 'Use beta encryption', description: () => 'Set beta encryption methods as the default methods. This applies to all clients and takes effect after restarting the app.', - advanced: true, section: 'sync', isGlobal: true, }, diff --git a/packages/lib/services/e2ee/crypto.test.ts b/packages/lib/services/e2ee/crypto.test.ts index 76f50e69a21..ff39b311016 100644 --- a/packages/lib/services/e2ee/crypto.test.ts +++ b/packages/lib/services/e2ee/crypto.test.ts @@ -14,7 +14,7 @@ describe('e2ee/crypto', () => { await afterAllCleanUp(); }); - it('should decrypt data from different devices', (async () => { + it('should encrypt and decrypt data from different devices', (async () => { await expectNotThrow(async () => runIntegrationTests(true)); })); From 115b5024c8546f290b1c5c612b7854a21ea8db57 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Wed, 11 Sep 2024 11:37:06 +0800 Subject: [PATCH 58/60] Fix yarn.lock Co-authored-by: Henry Heino --- yarn.lock | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/yarn.lock b/yarn.lock index 33189dd2a20..5604f573e5e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12245,7 +12245,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:18.19.34": +"@types/node@npm:18.19.34, @types/node@npm:^18.0.0": version: 18.19.34 resolution: "@types/node@npm:18.19.34" dependencies: @@ -12261,15 +12261,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^18.0.0": - version: 18.19.33 - resolution: "@types/node@npm:18.19.33" - dependencies: - undici-types: ~5.26.4 - checksum: b6db87d095bc541d64a410fa323a35c22c6113220b71b608bbe810b2397932d0f0a51c3c0f3ef90c20d8180a1502d950a7c5314b907e182d9cc10b36efd2a44e - languageName: node - linkType: hard - "@types/node@npm:^20.10.6": version: 20.11.16 resolution: "@types/node@npm:20.11.16" From 8fcc8ee3276a7223dd377ee09eb90f8f56b47180 Mon Sep 17 00:00:00 2001 From: wh201906 Date: Thu, 12 Sep 2024 21:43:34 +0800 Subject: [PATCH 59/60] Use it.each to make test cases independent of each other --- .../services/e2ee/EncryptionService.test.ts | 145 ++++++++++-------- 1 file changed, 78 insertions(+), 67 deletions(-) diff --git a/packages/lib/services/e2ee/EncryptionService.test.ts b/packages/lib/services/e2ee/EncryptionService.test.ts index 343bdd507a8..ffeb76c338d 100644 --- a/packages/lib/services/e2ee/EncryptionService.test.ts +++ b/packages/lib/services/e2ee/EncryptionService.test.ts @@ -95,32 +95,32 @@ describe('services_EncryptionService', () => { expect(!!masterKey.content).toBe(true); })); - it('should not require a checksum for new master keys', (async () => { - const masterKeyEncryptionMethodList = [EncryptionMethod.SJCL4, EncryptionMethod.KeyV1]; - for (const masterKeyEncryptionMethod of masterKeyEncryptionMethodList) { - const masterKey = await service.generateMasterKey('123456', { - encryptionMethod: masterKeyEncryptionMethod, - }); + it.each([ + EncryptionMethod.SJCL4, + EncryptionMethod.KeyV1, + ])('should not require a checksum for new master keys', (async (masterKeyEncryptionMethod) => { + const masterKey = await service.generateMasterKey('123456', { + encryptionMethod: masterKeyEncryptionMethod, + }); - expect(!masterKey.checksum).toBe(true); - expect(!!masterKey.content).toBe(true); + expect(!masterKey.checksum).toBe(true); + expect(!!masterKey.content).toBe(true); - const decryptedMasterKey = await service.decryptMasterKeyContent(masterKey, '123456'); - expect(decryptedMasterKey.length).toBe(512); - } + const decryptedMasterKey = await service.decryptMasterKeyContent(masterKey, '123456'); + expect(decryptedMasterKey.length).toBe(512); })); - it('should throw an error if master key decryption fails', (async () => { - const masterKeyEncryptionMethodList = [EncryptionMethod.SJCL4, EncryptionMethod.KeyV1]; - for (const masterKeyEncryptionMethod of masterKeyEncryptionMethodList) { - const masterKey = await service.generateMasterKey('123456', { - encryptionMethod: masterKeyEncryptionMethod, - }); + it.each([ + EncryptionMethod.SJCL4, + EncryptionMethod.KeyV1, + ])('should throw an error if master key decryption fails', (async (masterKeyEncryptionMethod) => { + const masterKey = await service.generateMasterKey('123456', { + encryptionMethod: masterKeyEncryptionMethod, + }); - const hasThrown = await checkThrowAsync(async () => await service.decryptMasterKeyContent(masterKey, 'wrong')); + const hasThrown = await checkThrowAsync(async () => await service.decryptMasterKeyContent(masterKey, 'wrong')); - expect(hasThrown).toBe(true); - } + expect(hasThrown).toBe(true); })); it('should return the master keys that need an upgrade', (async () => { @@ -218,46 +218,54 @@ describe('services_EncryptionService', () => { expect(hasThrown).toBe(true); })); - it('should fail to decrypt if ciphertext is not a valid JSON string', (async () => { + it.each([ + EncryptionMethod.SJCL1a, + EncryptionMethod.SJCL1b, + EncryptionMethod.SJCL4, + EncryptionMethod.KeyV1, + EncryptionMethod.FileV1, + EncryptionMethod.StringV1, + ])('should fail to decrypt if ciphertext is not a valid JSON string', (async (jsonCipherTextMethod) => { const masterKey = await service.generateMasterKey('123456'); const masterKeyContent = await service.decryptMasterKeyContent(masterKey, '123456'); - const jsonCipherTextMethodList = [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.SJCL4, EncryptionMethod.KeyV1, EncryptionMethod.FileV1, EncryptionMethod.StringV1]; - for (const jsonCipherTextMethod of jsonCipherTextMethodList) { - const cipherTextString = await service.encrypt(jsonCipherTextMethod, masterKeyContent, 'e21de21d'); // 'e21de21d' is a valid base64/hex string + const cipherTextString = await service.encrypt(jsonCipherTextMethod, masterKeyContent, 'e21de21d'); // 'e21de21d' is a valid base64/hex string - // Check if decryption is working - const plainText = await service.decrypt(jsonCipherTextMethod, masterKeyContent, cipherTextString); - expect(plainText).toBe('e21de21d'); + // Check if decryption is working + const plainText = await service.decrypt(jsonCipherTextMethod, masterKeyContent, cipherTextString); + expect(plainText).toBe('e21de21d'); - // Make invalid JSON - const invalidCipherText = cipherTextString.replace('{', '{,'); - const hasThrown = await checkThrowAsync(async () => await service.decrypt(jsonCipherTextMethod, masterKeyContent, invalidCipherText)); - expect(hasThrown).toBe(true); - } + // Make invalid JSON + const invalidCipherText = cipherTextString.replace('{', '{,'); + const hasThrown = await checkThrowAsync(async () => await service.decrypt(jsonCipherTextMethod, masterKeyContent, invalidCipherText)); + expect(hasThrown).toBe(true); })); - it('should fail to decrypt if ciphertext authentication failed', (async () => { + it.each([ + EncryptionMethod.SJCL1a, + EncryptionMethod.SJCL1b, + EncryptionMethod.SJCL4, + EncryptionMethod.KeyV1, + EncryptionMethod.FileV1, + EncryptionMethod.StringV1, + ])('should fail to decrypt if ciphertext authentication failed', (async (authenticatedEncryptionMethod) => { const masterKey = await service.generateMasterKey('123456'); const masterKeyContent = await service.decryptMasterKeyContent(masterKey, '123456'); - const authenticatedEncryptionMethodList = [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.SJCL4, EncryptionMethod.KeyV1, EncryptionMethod.FileV1, EncryptionMethod.StringV1]; - for (const authenticatedEncryptionMethod of authenticatedEncryptionMethodList) { - const cipherTextObject = JSON.parse(await service.encrypt(authenticatedEncryptionMethod, masterKeyContent, 'e21de21d')); // 'e21de21d' is a valid base64/hex string - expect(cipherTextObject).toHaveProperty('ct'); - const ct = Buffer.from(cipherTextObject['ct'], 'base64'); - - // Should not fail if the binary data of ct is not modified - const oldCipherTextObject = { ...cipherTextObject, ct: ct.toString('base64') }; - const plainText = await service.decrypt(authenticatedEncryptionMethod, masterKeyContent, JSON.stringify(oldCipherTextObject)); - expect(plainText).toBe('e21de21d'); - - // The encrypted data part is changed so it doesn't match the authentication tag. Decryption should fail. - ct[0] ^= 0x55; - const newCipherTextObject = { ...cipherTextObject, ct: ct.toString('base64') }; - const hasThrown = await checkThrowAsync(async () => service.decrypt(authenticatedEncryptionMethod, masterKeyContent, JSON.stringify(newCipherTextObject))); - expect(hasThrown).toBe(true); - } + const cipherTextObject = JSON.parse(await service.encrypt(authenticatedEncryptionMethod, masterKeyContent, 'e21de21d')); // 'e21de21d' is a valid base64/hex string + expect(cipherTextObject).toHaveProperty('ct'); + const ct = Buffer.from(cipherTextObject['ct'], 'base64'); + + // Should not fail if the binary data of ct is not modified + const oldCipherTextObject = { ...cipherTextObject, ct: ct.toString('base64') }; + const plainText = await service.decrypt(authenticatedEncryptionMethod, masterKeyContent, JSON.stringify(oldCipherTextObject)); + expect(plainText).toBe('e21de21d'); + + // The encrypted data part is changed so it doesn't match the authentication tag. Decryption should fail. + ct[0] ^= 0x55; + const newCipherTextObject = { ...cipherTextObject, ct: ct.toString('base64') }; + const hasThrown = await checkThrowAsync(async () => service.decrypt(authenticatedEncryptionMethod, masterKeyContent, JSON.stringify(newCipherTextObject))); + expect(hasThrown).toBe(true); })); it('should encrypt and decrypt notes and folders', (async () => { @@ -291,7 +299,12 @@ describe('services_EncryptionService', () => { expect(decryptedNote.parent_id).toBe(note.parent_id); })); - it('should encrypt and decrypt files', (async () => { + it.each([ + EncryptionMethod.SJCL1a, + EncryptionMethod.SJCL1b, + EncryptionMethod.FileV1, + EncryptionMethod.StringV1, + ])('should encrypt and decrypt files', (async (fileEncryptionMethod) => { let masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); await service.loadMasterKey(masterKey, '123456', true); @@ -300,18 +313,19 @@ describe('services_EncryptionService', () => { const encryptedPath = `${Setting.value('tempDir')}/photo.crypted`; const decryptedPath = `${Setting.value('tempDir')}/photo.jpg`; - const fileEncryptionMethodList = [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.FileV1, EncryptionMethod.StringV1]; - for (const fileEncryptionMethod of fileEncryptionMethodList) { - service.defaultFileEncryptionMethod_ = fileEncryptionMethod; - await service.encryptFile(sourcePath, encryptedPath); - await service.decryptFile(encryptedPath, decryptedPath); + service.defaultFileEncryptionMethod_ = fileEncryptionMethod; + await service.encryptFile(sourcePath, encryptedPath); + await service.decryptFile(encryptedPath, decryptedPath); - expect(fileContentEqual(sourcePath, encryptedPath)).toBe(false); - expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true); - } + expect(fileContentEqual(sourcePath, encryptedPath)).toBe(false); + expect(fileContentEqual(sourcePath, decryptedPath)).toBe(true); })); - it('should encrypt invalid UTF-8 data', (async () => { + it.each([ + EncryptionMethod.SJCL1a, + EncryptionMethod.SJCL1b, + EncryptionMethod.StringV1, + ])('should encrypt invalid UTF-8 data', (async (stringEncryptionMethod) => { let masterKey = await service.generateMasterKey('123456'); masterKey = await MasterKey.save(masterKey); @@ -323,13 +337,10 @@ describe('services_EncryptionService', () => { expect(hasThrown).toBe(true); // Now check that the new one fixes the problem - const stringEncryptionMethodList = [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1b, EncryptionMethod.StringV1]; - for (const stringEncryptionMethod of stringEncryptionMethodList) { - service.defaultEncryptionMethod_ = stringEncryptionMethod; - const cipherText = await service.encryptString('🐶🐶🐶'.substr(0, 5)); - const plainText = await service.decryptString(cipherText); - expect(plainText).toBe('🐶🐶🐶'.substr(0, 5)); - } + service.defaultEncryptionMethod_ = stringEncryptionMethod; + const cipherText = await service.encryptString('🐶🐶🐶'.substr(0, 5)); + const plainText = await service.decryptString(cipherText); + expect(plainText).toBe('🐶🐶🐶'.substr(0, 5)); })); it('should check if a master key is loaded', (async () => { From edd2a9002a31b0fee4f11ded0a37951543e289ab Mon Sep 17 00:00:00 2001 From: wh201906 Date: Thu, 12 Sep 2024 22:47:53 +0800 Subject: [PATCH 60/60] Add test cases for new encryption methods in Synchronizer.e2ee.test.ts --- .../synchronizer/Synchronizer.e2ee.test.ts | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/packages/lib/services/synchronizer/Synchronizer.e2ee.test.ts b/packages/lib/services/synchronizer/Synchronizer.e2ee.test.ts index e50c0db9323..3a878e1a57f 100644 --- a/packages/lib/services/synchronizer/Synchronizer.e2ee.test.ts +++ b/packages/lib/services/synchronizer/Synchronizer.e2ee.test.ts @@ -12,6 +12,7 @@ import Synchronizer from '../../Synchronizer'; import { fetchSyncInfo, getEncryptionEnabled, localSyncInfo, setEncryptionEnabled } from '../synchronizer/syncInfoUtils'; import { loadMasterKeysFromSettings, setupAndDisableEncryption, setupAndEnableEncryption } from '../e2ee/utils'; import { remoteNotesAndFolders } from '../../testing/test-utils-synchronizer'; +import { EncryptionMethod } from '../e2ee/EncryptionService'; let insideBeforeEach = false; @@ -31,8 +32,12 @@ describe('Synchronizer.e2ee', () => { insideBeforeEach = false; }); - it('notes and folders should get encrypted when encryption is enabled', (async () => { + it.each([ + EncryptionMethod.SJCL1a, + EncryptionMethod.StringV1, + ])('notes and folders should get encrypted when encryption is enabled', (async (encryptionMethod) => { setEncryptionEnabled(true); + encryptionService().defaultEncryptionMethod_ = encryptionMethod; const masterKey = await loadEncryptionMasterKey(); const folder1 = await Folder.save({ title: 'folder1' }); let note1 = await Note.save({ title: 'un', body: 'to be encrypted', parent_id: folder1.id }); @@ -255,8 +260,13 @@ describe('Synchronizer.e2ee', () => { expect(allEncrypted).toBe(false); })); - it('should set the resource file size after decryption', (async () => { + it.each([ + [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1a], + [EncryptionMethod.StringV1, EncryptionMethod.FileV1], + ])('should set the resource file size after decryption', (async (stringEncryptionMethod, fileEncryptionMethod) => { setEncryptionEnabled(true); + encryptionService().defaultEncryptionMethod_ = stringEncryptionMethod; + encryptionService().defaultFileEncryptionMethod_ = fileEncryptionMethod; const masterKey = await loadEncryptionMasterKey(); const folder1 = await Folder.save({ title: 'folder1' }); @@ -282,9 +292,15 @@ describe('Synchronizer.e2ee', () => { expect(resource1_2.size).toBe(2720); })); - it('should encrypt remote resources after encryption has been enabled', (async () => { + it.each([ + [EncryptionMethod.SJCL1a, EncryptionMethod.SJCL1a], + [EncryptionMethod.StringV1, EncryptionMethod.FileV1], + ])('should encrypt remote resources after encryption has been enabled', (async (stringEncryptionMethod, fileEncryptionMethod) => { while (insideBeforeEach) await time.msleep(100); + encryptionService().defaultEncryptionMethod_ = stringEncryptionMethod; + encryptionService().defaultFileEncryptionMethod_ = fileEncryptionMethod; + const folder1 = await Folder.save({ title: 'folder1' }); const note1 = await Note.save({ title: 'ma note', parent_id: folder1.id }); await shim.attachFileToNote(note1, `${supportDir}/photo.jpg`); @@ -350,9 +366,13 @@ describe('Synchronizer.e2ee', () => { expect(!!resource.encryption_blob_encrypted).toBe(false); })); - it('should stop trying to decrypt item after a few attempts', (async () => { + it.each([ + EncryptionMethod.SJCL1a, + EncryptionMethod.StringV1, + ])('should stop trying to decrypt item after a few attempts', (async (encryptionMethod) => { let hasThrown; + encryptionService().defaultEncryptionMethod_ = encryptionMethod; const note = await Note.save({ title: 'ma note' }); const masterKey = await loadEncryptionMasterKey(); await setupAndEnableEncryption(encryptionService(), masterKey, '123456');