mirror of
https://github.com/laurent22/joplin.git
synced 2024-12-30 10:36:35 +02:00
114 lines
4.1 KiB
TypeScript
114 lines
4.1 KiB
TypeScript
|
import { Crypto, CryptoBuffer, Digest, EncryptionResult, EncryptionParameters } from './types';
|
||
|
import { webcrypto } from 'crypto';
|
||
|
import { Buffer } from 'buffer';
|
||
|
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();
|
||
|
const key = await webcrypto.subtle.importKey(
|
||
|
'raw', encoder.encode(password), { name: 'PBKDF2' }, false, ['deriveKey'],
|
||
|
);
|
||
|
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: webcrypto.CryptoKey, iv: Uint8Array, authTagLengthBytes: number, additionalData: Uint8Array) => {
|
||
|
return Buffer.from(await webcrypto.subtle.encrypt({
|
||
|
name: 'AES-GCM',
|
||
|
iv,
|
||
|
additionalData,
|
||
|
tagLength: authTagLengthBytes * 8,
|
||
|
}, key, data));
|
||
|
};
|
||
|
|
||
|
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,
|
||
|
}, key, data));
|
||
|
};
|
||
|
|
||
|
const crypto: Crypto = {
|
||
|
|
||
|
randomBytes: async (size: number) => {
|
||
|
// .getRandomValues has a maximum output size
|
||
|
const maxChunkSize = 65536;
|
||
|
const result = new Uint8Array(size);
|
||
|
|
||
|
if (size <= maxChunkSize) {
|
||
|
webcrypto.getRandomValues(result);
|
||
|
} else {
|
||
|
const fullSizeChunk = new Uint8Array(maxChunkSize);
|
||
|
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);
|
||
|
},
|
||
|
|
||
|
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
|
||
|
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 = await encryptRaw(data, 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'), 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;
|