k9crypt
Version:
A special encryption algorithm created for K9Crypt.
266 lines (213 loc) • 9.19 kB
JavaScript
const crypto = require('crypto');
const { compress, decompress } = require('./compression');
const { deriveKey } = require('./keyDerivation');
const { encrypt, decrypt } = require('./encryption');
const { hash, verifyHash } = require('./hashing');
const { SALT_SIZE, IV_SIZE, TAG_SIZE, ARGON2_SALT_SIZE, ARGON2_HASH_LENGTH } = require('../constants');
exports.encryptMany = async (dataArray, secretKey, options = {}) => {
try {
if (!Array.isArray(dataArray)) {
throw new Error('Data must be an array');
}
if (dataArray.length === 0) {
return [];
}
const compressionLevel = options.compressionLevel || 3;
const onProgress = options.onProgress || null;
const results = [];
const totalItems = dataArray.length;
for (let i = 0; i < dataArray.length; i++) {
const item = dataArray[i];
if (item === null || item === undefined) {
results.push(null);
continue;
}
const compressed = await compress(item, compressionLevel);
const salt = crypto.randomBytes(SALT_SIZE);
const key = await deriveKey(secretKey, salt);
const { iv1, iv2, iv3, iv4, iv5, encrypted, tag1 } = await encrypt(compressed, key);
const dataToHash = Buffer.concat([salt, iv1, iv2, iv3, iv4, iv5, encrypted, tag1]);
const argon2Salt = crypto.randomBytes(ARGON2_SALT_SIZE);
const dataHash = await hash(dataToHash, argon2Salt);
const result = Buffer.concat([salt, iv1, iv2, iv3, iv4, iv5, encrypted, tag1, argon2Salt, dataHash]);
results.push(result.toString('base64'));
if (onProgress) {
onProgress({
current: i + 1,
total: totalItems,
percentage: Math.round(((i + 1) / totalItems) * 100)
});
}
}
return results;
} catch (error) {
throw new Error(`Batch encryption failed: ${error.message}`);
}
};
exports.decryptMany = async (ciphertextArray, secretKey, options = {}) => {
try {
if (!Array.isArray(ciphertextArray)) {
throw new Error('Data must be an array');
}
if (ciphertextArray.length === 0) {
return [];
}
const onProgress = options.onProgress || null;
const skipInvalid = options.skipInvalid || false;
const results = [];
const totalItems = ciphertextArray.length;
for (let i = 0; i < ciphertextArray.length; i++) {
const item = ciphertextArray[i];
if (item === null || item === undefined) {
results.push(null);
continue;
}
try {
const data = Buffer.from(item, 'base64');
const salt = data.slice(0, SALT_SIZE);
const iv1 = data.slice(SALT_SIZE, SALT_SIZE + IV_SIZE);
const iv2 = data.slice(SALT_SIZE + IV_SIZE, SALT_SIZE + 2 * IV_SIZE);
const iv3 = data.slice(SALT_SIZE + 2 * IV_SIZE, SALT_SIZE + 3 * IV_SIZE);
const iv4 = data.slice(SALT_SIZE + 3 * IV_SIZE, SALT_SIZE + 4 * IV_SIZE);
const iv5 = data.slice(SALT_SIZE + 4 * IV_SIZE, SALT_SIZE + 5 * IV_SIZE);
const dataHash = data.slice(-ARGON2_HASH_LENGTH);
const argon2Salt = data.slice(-ARGON2_HASH_LENGTH - ARGON2_SALT_SIZE, -ARGON2_HASH_LENGTH);
const tag1 = data.slice(-ARGON2_HASH_LENGTH - ARGON2_SALT_SIZE - TAG_SIZE, -ARGON2_HASH_LENGTH - ARGON2_SALT_SIZE);
const encrypted = data.slice(SALT_SIZE + 5 * IV_SIZE, -ARGON2_HASH_LENGTH - ARGON2_SALT_SIZE - TAG_SIZE);
const dataToVerify = data.slice(0, -ARGON2_HASH_LENGTH - ARGON2_SALT_SIZE);
if (!(await verifyHash(dataToVerify, dataHash, argon2Salt))) {
if (skipInvalid) {
results.push(null);
continue;
}
throw new Error(`Data integrity check failed for item ${i}`);
}
const key = await deriveKey(secretKey, salt);
const decrypted = await decrypt(encrypted, key, iv1, iv2, iv3, iv4, iv5, tag1);
const decompressed = await decompress(decrypted);
results.push(decompressed.toString('utf8'));
} catch (error) {
if (skipInvalid) {
results.push(null);
}
if (!skipInvalid) {
throw error;
}
}
if (onProgress) {
onProgress({
current: i + 1,
total: totalItems,
percentage: Math.round(((i + 1) / totalItems) * 100)
});
}
}
return results;
} catch (error) {
throw new Error(`Batch decryption failed: ${error.message}`);
}
};
exports.encryptManyParallel = async (dataArray, secretKey, options = {}) => {
try {
if (!Array.isArray(dataArray)) {
throw new Error('Data must be an array');
}
if (dataArray.length === 0) {
return [];
}
const compressionLevel = options.compressionLevel || 3;
const batchSize = options.batchSize || 10;
const results = new Array(dataArray.length);
const batches = [];
for (let i = 0; i < dataArray.length; i += batchSize) {
const batch = dataArray.slice(i, Math.min(i + batchSize, dataArray.length));
const batchPromises = batch.map(async (item, index) => {
const actualIndex = i + index;
if (item === null || item === undefined) {
return { index: actualIndex, result: null };
}
const compressed = await compress(item, compressionLevel);
const salt = crypto.randomBytes(SALT_SIZE);
const key = await deriveKey(secretKey, salt);
const { iv1, iv2, iv3, iv4, iv5, encrypted, tag1 } = await encrypt(compressed, key);
const dataToHash = Buffer.concat([salt, iv1, iv2, iv3, iv4, iv5, encrypted, tag1]);
const argon2Salt = crypto.randomBytes(ARGON2_SALT_SIZE);
const dataHash = await hash(dataToHash, argon2Salt);
const result = Buffer.concat([salt, iv1, iv2, iv3, iv4, iv5, encrypted, tag1, argon2Salt, dataHash]);
return { index: actualIndex, result: result.toString('base64') };
});
batches.push(Promise.all(batchPromises));
}
const batchResults = await Promise.all(batches);
for (const batch of batchResults) {
for (const item of batch) {
results[item.index] = item.result;
}
}
return results;
} catch (error) {
throw new Error(`Parallel batch encryption failed: ${error.message}`);
}
};
exports.decryptManyParallel = async (ciphertextArray, secretKey, options = {}) => {
try {
if (!Array.isArray(ciphertextArray)) {
throw new Error('Data must be an array');
}
if (ciphertextArray.length === 0) {
return [];
}
const batchSize = options.batchSize || 10;
const skipInvalid = options.skipInvalid || false;
const results = new Array(ciphertextArray.length);
const batches = [];
for (let i = 0; i < ciphertextArray.length; i += batchSize) {
const batch = ciphertextArray.slice(i, Math.min(i + batchSize, ciphertextArray.length));
const batchPromises = batch.map(async (item, index) => {
const actualIndex = i + index;
if (item === null || item === undefined) {
return { index: actualIndex, result: null };
}
try {
const data = Buffer.from(item, 'base64');
const salt = data.slice(0, SALT_SIZE);
const iv1 = data.slice(SALT_SIZE, SALT_SIZE + IV_SIZE);
const iv2 = data.slice(SALT_SIZE + IV_SIZE, SALT_SIZE + 2 * IV_SIZE);
const iv3 = data.slice(SALT_SIZE + 2 * IV_SIZE, SALT_SIZE + 3 * IV_SIZE);
const iv4 = data.slice(SALT_SIZE + 3 * IV_SIZE, SALT_SIZE + 4 * IV_SIZE);
const iv5 = data.slice(SALT_SIZE + 4 * IV_SIZE, SALT_SIZE + 5 * IV_SIZE);
const dataHash = data.slice(-ARGON2_HASH_LENGTH);
const argon2Salt = data.slice(-ARGON2_HASH_LENGTH - ARGON2_SALT_SIZE, -ARGON2_HASH_LENGTH);
const tag1 = data.slice(-ARGON2_HASH_LENGTH - ARGON2_SALT_SIZE - TAG_SIZE, -ARGON2_HASH_LENGTH - ARGON2_SALT_SIZE);
const encrypted = data.slice(SALT_SIZE + 5 * IV_SIZE, -ARGON2_HASH_LENGTH - ARGON2_SALT_SIZE - TAG_SIZE);
const dataToVerify = data.slice(0, -ARGON2_HASH_LENGTH - ARGON2_SALT_SIZE);
if (!(await verifyHash(dataToVerify, dataHash, argon2Salt))) {
if (skipInvalid) {
return { index: actualIndex, result: null };
}
throw new Error(`Data integrity check failed for item ${actualIndex}`);
}
const key = await deriveKey(secretKey, salt);
const decrypted = await decrypt(encrypted, key, iv1, iv2, iv3, iv4, iv5, tag1);
const decompressed = await decompress(decrypted);
return { index: actualIndex, result: decompressed.toString('utf8') };
} catch (error) {
if (skipInvalid) {
return { index: actualIndex, result: null };
}
throw error;
}
});
batches.push(Promise.all(batchPromises));
}
const batchResults = await Promise.all(batches);
for (const batch of batchResults) {
for (const item of batch) {
results[item.index] = item.result;
}
}
return results;
} catch (error) {
throw new Error(`Parallel batch decryption failed: ${error.message}`);
}
};