2025-09-08 15:56:40 -07:00
|
|
|
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';
|
2021-10-03 16:00:49 +01:00
|
|
|
const RnRSA = require('react-native-rsa-native').RSA;
|
|
|
|
|
|
2025-09-08 15:56:40 -07:00
|
|
|
interface LegacyRsaKeyPair {
|
2021-10-03 16:00:49 +01:00
|
|
|
public: string;
|
|
|
|
|
private: string;
|
2024-05-16 01:54:24 -07:00
|
|
|
keySizeBits: number;
|
2021-10-03 16:00:49 +01:00
|
|
|
}
|
|
|
|
|
|
2025-09-08 15:56:40 -07:00
|
|
|
const legacyRsa: PublicKeyCrypto = {
|
|
|
|
|
generateKeyPair: async () => {
|
|
|
|
|
const keySize = 2048;
|
|
|
|
|
const keys: LegacyRsaKeyPair = await RnRSA.generateKeys(keySize);
|
2021-10-03 16:00:49 +01:00
|
|
|
|
|
|
|
|
// Sanity check
|
|
|
|
|
if (!keys.private) throw new Error('No private key was generated');
|
|
|
|
|
if (!keys.public) throw new Error('No public key was generated');
|
|
|
|
|
|
2025-09-08 15:56:40 -07:00
|
|
|
const keyPair = await legacyRsa.loadKeys(keys.public, keys.private, keySize);
|
|
|
|
|
return {
|
|
|
|
|
keyPair,
|
|
|
|
|
keySize,
|
|
|
|
|
};
|
2021-10-03 16:00:49 +01:00
|
|
|
},
|
|
|
|
|
|
2025-09-08 15:56:40 -07:00
|
|
|
maximumPlaintextLengthBytes: 190,
|
|
|
|
|
|
|
|
|
|
loadKeys: async (publicKey: string, privateKey: string, keySizeBits: number): Promise<LegacyRsaKeyPair> => {
|
2024-05-16 01:54:24 -07:00
|
|
|
return { public: publicKey, private: privateKey, keySizeBits };
|
2021-10-03 16:00:49 +01:00
|
|
|
},
|
|
|
|
|
|
2025-09-08 15:56:40 -07:00
|
|
|
encrypt: async (plaintextUtf8: string, rsaKeyPair: LegacyRsaKeyPair) => {
|
2024-05-16 01:54:24 -07:00
|
|
|
// TODO: Support long-data encryption in a way compatible with node-rsa.
|
2025-09-08 15:56:40 -07:00
|
|
|
return Buffer.from(await RnRSA.encrypt(plaintextUtf8, rsaKeyPair.public), 'base64');
|
2021-10-03 16:00:49 +01:00
|
|
|
},
|
|
|
|
|
|
2025-09-08 15:56:40 -07:00
|
|
|
decrypt: async (ciphertextBuffer: CiphertextBuffer, rsaKeyPair: LegacyRsaKeyPair): Promise<string> => {
|
2024-05-16 01:54:24 -07:00
|
|
|
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 {
|
2025-09-08 15:56:40 -07:00
|
|
|
const plainText = await RnRSA.decrypt(ciphertextBuffer.toString('base64'), rsaKeyPair.private);
|
2024-05-16 01:54:24 -07:00
|
|
|
handleError(plainText);
|
|
|
|
|
return plainText;
|
|
|
|
|
}
|
2021-10-03 16:00:49 +01:00
|
|
|
},
|
|
|
|
|
|
2025-09-08 15:56:40 -07:00
|
|
|
publicKey: async (rsaKeyPair: LegacyRsaKeyPair) => {
|
2021-10-03 16:00:49 +01:00
|
|
|
return rsaKeyPair.public;
|
|
|
|
|
},
|
|
|
|
|
|
2025-09-08 15:56:40 -07:00
|
|
|
privateKey: async (rsaKeyPair: LegacyRsaKeyPair) => {
|
2021-10-03 16:00:49 +01:00
|
|
|
return rsaKeyPair.private;
|
|
|
|
|
},
|
2025-09-08 15:56:40 -07:00
|
|
|
};
|
2021-10-03 16:00:49 +01:00
|
|
|
|
2025-09-08 15:56:40 -07:00
|
|
|
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),
|
2021-10-03 16:00:49 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default rsa;
|