1
0
mirror of https://github.com/laurent22/joplin.git synced 2025-10-31 00:07:48 +02:00

All: Add new encryption methods based on native crypto libraries (#10696)

Co-authored-by: Laurent Cozic <laurent22@users.noreply.github.com>
Co-authored-by: Henry Heino <personalizedrefrigerator@gmail.com>
This commit is contained in:
Self Not Found
2024-10-27 04:15:10 +08:00
committed by GitHub
parent bed5297829
commit aa6348c5c2
23 changed files with 1064 additions and 59 deletions

View File

@@ -62,6 +62,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.8",
"react-native-securerandom": "1.0.1",

View File

@@ -110,6 +110,7 @@ import { loadMasterKeysFromSettings, migrateMasterPassword } from '@joplin/lib/s
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';
@@ -818,8 +819,9 @@ async function initialize(dispatch: Dispatch) {
if (Platform.OS !== 'web') {
await runRsaIntegrationTests();
} else {
logger.info('Skipping RSA tests -- not supported on mobile.');
logger.info('Skipping encryption tests -- not supported on web.');
}
await runCryptoIntegrationTests();
await runOnDeviceFsDriverTests();
}

View File

@@ -0,0 +1,123 @@
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';
import {
generateNonce as generateNonceShared,
increaseNonce as increaseNonceShared,
setRandomBytesImplementation,
} from '@joplin/lib/services/e2ee/cryptoShared';
type DigestNameMap = Record<Digest, string>;
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<CryptoBuffer> => {
return new Promise((resolve, reject) => {
QuickCrypto.pbkdf2(password, salt, iterations, keylen, digest as HashAlgorithm, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
};
const encryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => {
const cipher = QuickCrypto.createCipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as CipherGCM;
cipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) });
const encryptedData = [cipher.update(data), cipher.final()];
const authTag = cipher.getAuthTag();
return Buffer.concat([encryptedData[0], encryptedData[1], authTag]);
};
const decryptRaw = (data: CryptoBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: CryptoBuffer, authTagLength: number, associatedData: CryptoBuffer) => {
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);
decipher.setAuthTag(authTag);
decipher.setAAD(associatedData, { plaintextLength: Buffer.byteLength(data) });
try {
return Buffer.concat([decipher.update(encryptedData), decipher.final()]);
} catch (error) {
throw new Error(`Authentication failed! ${error}`);
}
};
const crypto: Crypto = {
randomBytes: async (size: number) => {
return new Promise((resolve, reject) => {
QuickCrypto.randomBytes(size, (error, result) => {
if (error) {
reject(error);
} else {
resolve(result);
}
});
});
},
digest: async (algorithm: Digest, data: Uint8Array) => {
const hash = QuickCrypto.createHash(digestNameMap[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
const result: EncryptionResult = {
salt: salt.toString('base64'),
iv: '',
ct: '', // cipherText
};
// 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, 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');
return result;
},
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, 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, encryptionParameters: EncryptionParameters) => {
return crypto.encrypt(password, salt, Buffer.from(data, encoding), encryptionParameters);
},
generateNonce: generateNonceShared,
increaseNonce: increaseNonceShared,
};
setRandomBytesImplementation(crypto.randomBytes);
export default crypto;

View File

@@ -6,6 +6,7 @@ import RNFetchBlob from 'rn-fetch-blob';
import { generateSecureRandom } from 'react-native-securerandom';
import FsDriverRN from '../fs-driver/fs-driver-rn';
import { Linking, Platform } from 'react-native';
import crypto from '../../services/e2ee/crypto';
const RNExitApp = require('react-native-exit-app').default;
export default function shimInit() {
@@ -18,6 +19,8 @@ export default function shimInit() {
return shim.fsDriver_;
};
shim.crypto = crypto;
shim.randomBytes = async (count: number) => {
const randomBytes = await generateSecureRandom(count);
const temp = [];

View File

@@ -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);

View File

@@ -0,0 +1,2 @@
exports.webcrypto = crypto;

View File

@@ -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,