feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled

这个提交包含在:
chen
2026-11-04 11:29:39 +08:00
当前提交 301194963b
共有 8663 个文件被更改,包括 1962691 次插入 和 0 次删除

查看文件

@@ -0,0 +1,97 @@
import buildRsaCryptoProvider from '@joplin/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider';
import { WebCryptoSlice } from '@joplin/lib/services/e2ee/ppk/webCrypto/WebCryptoRsa';
import { CiphertextBuffer, PublicKeyAlgorithm, PublicKeyCrypto, PublicKeyCryptoProvider } from '@joplin/lib/services/e2ee/types';
import QuickCrypto from 'react-native-quick-crypto';
const RnRSA = require('react-native-rsa-native').RSA;
interface LegacyRsaKeyPair {
public: string;
private: string;
keySizeBits: number;
}
const legacyRsa: PublicKeyCrypto = {
generateKeyPair: async () => {
const keySize = 2048;
const keys: LegacyRsaKeyPair = await RnRSA.generateKeys(keySize);
// Sanity check
if (!keys.private) throw new Error('No private key was generated');
if (!keys.public) throw new Error('No public key was generated');
const keyPair = await legacyRsa.loadKeys(keys.public, keys.private, keySize);
return {
keyPair,
keySize,
};
},
maximumPlaintextLengthBytes: 190,
loadKeys: async (publicKey: string, privateKey: string, keySizeBits: number): Promise<LegacyRsaKeyPair> => {
return { public: publicKey, private: privateKey, keySizeBits };
},
encrypt: async (plaintextUtf8: string, rsaKeyPair: LegacyRsaKeyPair) => {
// TODO: Support long-data encryption in a way compatible with node-rsa.
return Buffer.from(await RnRSA.encrypt(plaintextUtf8, rsaKeyPair.public), 'base64');
},
decrypt: async (ciphertextBuffer: CiphertextBuffer, rsaKeyPair: LegacyRsaKeyPair): Promise<string> => {
const maximumEncryptedSize = Math.floor(rsaKeyPair.keySizeBits / 8); // Usually 256
// On iOS, .decrypt fails without throwing or rejecting.
// This function throws for consistency with Android.
const handleError = (plainText: string|undefined) => {
if (plainText === undefined) {
throw new Error(`
RN RSA: Decryption failed.
cipherTextLength=${ciphertextBuffer.length},
maxEncryptedSize=${maximumEncryptedSize}
`.trim());
}
};
// Master keys are encrypted with RSA and are longer than the default modulus size of 256 bytes.
// node-rsa supports encrypting larger amounts of data using RSA.
// See their implementation for details: https://github.com/rzcoder/node-rsa/blob/e7e7f7d2942a3bac1d2e132a881e5a3aceda10a1/src/libs/rsa.js#L252
if (ciphertextBuffer.length > maximumEncryptedSize) {
// Use a numBlocks and blockSize that match node-rsa:
const numBlocks = Math.ceil(ciphertextBuffer.length / maximumEncryptedSize);
const blockSize = maximumEncryptedSize;
const result: string[] = [];
for (let i = 0; i < numBlocks; i++) {
const ciphertextBlock = ciphertextBuffer.slice(
i * blockSize, Math.min(ciphertextBuffer.length, (i + 1) * blockSize),
);
const plainText = await RnRSA.decrypt(ciphertextBlock.toString('base64'), rsaKeyPair.private);
handleError(plainText);
result.push(plainText);
}
return result.join('');
} else {
const plainText = await RnRSA.decrypt(ciphertextBuffer.toString('base64'), rsaKeyPair.private);
handleError(plainText);
return plainText;
}
},
publicKey: async (rsaKeyPair: LegacyRsaKeyPair) => {
return rsaKeyPair.public;
},
privateKey: async (rsaKeyPair: LegacyRsaKeyPair) => {
return rsaKeyPair.private;
},
};
const rsa: PublicKeyCryptoProvider = {
[PublicKeyAlgorithm.Unknown]: null,
[PublicKeyAlgorithm.RsaV1]: legacyRsa,
[PublicKeyAlgorithm.RsaV2]: buildRsaCryptoProvider(PublicKeyAlgorithm.RsaV2, QuickCrypto as WebCryptoSlice),
[PublicKeyAlgorithm.RsaV3]: buildRsaCryptoProvider(PublicKeyAlgorithm.RsaV3, QuickCrypto as WebCryptoSlice),
};
export default rsa;

查看文件

@@ -0,0 +1,11 @@
import buildRsaCryptoProvider from '@joplin/lib/services/e2ee/ppk/webCrypto/buildRsaCryptoProvider';
import { PublicKeyAlgorithm, PublicKeyCryptoProvider } from '@joplin/lib/services/e2ee/types';
const rsa: PublicKeyCryptoProvider = {
[PublicKeyAlgorithm.Unknown]: null,
[PublicKeyAlgorithm.RsaV1]: null, // Unsupported on web
[PublicKeyAlgorithm.RsaV2]: buildRsaCryptoProvider(PublicKeyAlgorithm.RsaV2, crypto),
[PublicKeyAlgorithm.RsaV3]: buildRsaCryptoProvider(PublicKeyAlgorithm.RsaV3, crypto),
};
export default rsa;

查看文件

@@ -0,0 +1,130 @@
import { Crypto, CryptoBuffer, CryptoBufferEncoding, Digest, CipherAlgorithm, EncryptionResult, EncryptionParameters } from '@joplin/lib/services/e2ee/types';
import QuickCrypto from 'react-native-quick-crypto';
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 | ArrayBuffer, iterations: number, keylen: number, digest: Digest): Promise<CryptoBuffer> => {
return new Promise((resolve, reject) => {
QuickCrypto.pbkdf2(password, salt, iterations, keylen, digest, (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 unknown as CipherGCM;
cipher.setAAD(associatedData, { plaintextLength: data.byteLength });
const encryptedData = [cipher.update(data), cipher.final()];
const authTag = cipher.getAuthTag();
return Buffer.concat([encryptedData[0], encryptedData[1], authTag]);
};
const decryptRaw = (data: ArrayBuffer, algorithm: CipherAlgorithm, key: CryptoBuffer, iv: ArrayBuffer, authTagLength: number, associatedData: CryptoBuffer) => {
const decipher = QuickCrypto.createDecipheriv(algorithm, key, iv, { authTagLength: authTagLength } as CipherGCMOptions) as unknown as DecipherGCM;
const plaintextLength = data.byteLength - authTagLength;
const authTag = new Uint8Array(data, plaintextLength, authTagLength);
const encryptedData = new Uint8Array(data, 0, plaintextLength);
decipher.setAuthTag(authTag);
decipher.setAAD(associatedData, { plaintextLength: plaintextLength });
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(
// Cast: hash.update accepts TypedArrays, despite its declared types
data as unknown as ArrayBuffer,
);
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: crypto.bufferToString(salt, '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 = crypto.bufferToString(iv, 'base64');
result.ct = crypto.bufferToString(encrypted, 'base64');
return result;
},
decrypt: async (password: string, data: EncryptionResult, encryptionParameters: EncryptionParameters) => {
const salt = QuickCrypto.binaryLikeToArrayBuffer(data.salt, 'base64');
const iv = QuickCrypto.binaryLikeToArrayBuffer(data.iv, 'base64');
const key = await pbkdf2Raw(password, salt, encryptionParameters.iterationCount, encryptionParameters.keyLength, encryptionParameters.digestAlgorithm);
const decrypted = decryptRaw(QuickCrypto.binaryLikeToArrayBuffer(data.ct, 'base64'), encryptionParameters.cipherAlgorithm, key, iv, encryptionParameters.authTagLength, encryptionParameters.associatedData);
return decrypted;
},
encryptString: async (password: string, salt: CryptoBuffer, data: string, encoding: CryptoBufferEncoding, encryptionParameters: EncryptionParameters) => {
return crypto.encrypt(password, salt, new Uint8Array(QuickCrypto.binaryLikeToArrayBuffer(data, encoding)), encryptionParameters);
},
bufferToString: (buffer: CryptoBuffer, encoding: CryptoBufferEncoding) => {
return QuickCrypto.ab2str(buffer.buffer, encoding, buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
},
generateNonce: generateNonceShared,
increaseNonce: increaseNonceShared,
};
setRandomBytesImplementation(crypto.randomBytes);
export default crypto;