269 lines
9.1 KiB
JavaScript
269 lines
9.1 KiB
JavaScript
|
class EonaCatCipher {
|
||
|
constructor(password, saltSize = 2048, ivSize = 2048, keySize = 2048, rounds = 2048, blockSize = 8192) {
|
||
|
if (!password) {
|
||
|
throw new Error("Password cannot be null or empty.");
|
||
|
}
|
||
|
|
||
|
this.ivSize = ivSize;
|
||
|
this.keySize = keySize;
|
||
|
this.rounds = rounds;
|
||
|
this.blockSize = blockSize;
|
||
|
|
||
|
// Derive encryption key and HMAC key
|
||
|
[this.derivedKey, this.hmacKey] = this.deriveKeyAndHMAC(password, saltSize);
|
||
|
}
|
||
|
|
||
|
/*
|
||
|
* EonaCatCipher - Because security is key!
|
||
|
*
|
||
|
* Copyright (c) 2024 EonaCat (Jeroen Saey)
|
||
|
*
|
||
|
* https://eonacat.com/license
|
||
|
*
|
||
|
* TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||
|
* OF SOFTWARE BY EONACAT (JEROEN SAEY)
|
||
|
*
|
||
|
* This software is provided "as is", without any express or implied warranty.
|
||
|
* In no event shall the authors or copyright holders be liable for any claim,
|
||
|
* damages or other liability, whether in an action of contract, tort or otherwise,
|
||
|
* arising from, out of or in connection with the software or the use or other
|
||
|
* dealings in the software.
|
||
|
*
|
||
|
* You may use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||
|
* copies of the Software, and permit persons to whom the Software is furnished
|
||
|
* to do so, subject to the following conditions:
|
||
|
*
|
||
|
* 1. The above copyright notice and this permission notice shall be included in
|
||
|
* all copies or substantial portions of the Software.
|
||
|
*
|
||
|
* 2. The software must not be used for any unlawful purpose.
|
||
|
*
|
||
|
* For any inquiries, please contact: eonacat@gmail.com
|
||
|
*/
|
||
|
|
||
|
static generateRandomBytes(size) {
|
||
|
const randomBytes = new Uint8Array(size);
|
||
|
crypto.getRandomValues(randomBytes);
|
||
|
return randomBytes;
|
||
|
}
|
||
|
|
||
|
deriveKeyAndHMAC(password, saltSize) {
|
||
|
const salt = EonaCatCipher.generateRandomBytes(saltSize);
|
||
|
const encryptionKey = this.pbkdf2(password, salt, this.keySize, this.rounds);
|
||
|
|
||
|
// Derive separate key for HMAC
|
||
|
const hmacKey = this.pbkdf2(password, salt, 32, this.rounds);
|
||
|
|
||
|
const keyWithSalt = new Uint8Array(saltSize + this.keySize);
|
||
|
keyWithSalt.set(salt, 0);
|
||
|
keyWithSalt.set(encryptionKey, saltSize);
|
||
|
|
||
|
return [keyWithSalt, hmacKey];
|
||
|
}
|
||
|
|
||
|
pbkdf2(password, salt, keyLength, iterations) {
|
||
|
const hmac = new Uint8Array(64); // HMAC length (SHA512)
|
||
|
const hashLength = hmac.length;
|
||
|
|
||
|
const blocksNeeded = Math.ceil(keyLength / hashLength);
|
||
|
const derivedKey = new Uint8Array(keyLength);
|
||
|
|
||
|
for (let blockIndex = 1; blockIndex <= blocksNeeded; blockIndex++) {
|
||
|
const currentBlock = new Uint8Array(salt.length + 4);
|
||
|
currentBlock.set(salt, 0);
|
||
|
currentBlock.set(new Uint8Array(new Uint32Array([blockIndex]).buffer), salt.length);
|
||
|
|
||
|
let u = this.hmacSha512(password, currentBlock);
|
||
|
const block = new Uint8Array(hashLength);
|
||
|
block.set(u);
|
||
|
|
||
|
// Step 4: Derived key starts with U1
|
||
|
derivedKey.set(u.subarray(0, Math.min(hashLength, keyLength)), (blockIndex - 1) * hashLength);
|
||
|
|
||
|
// Step 4: Iterations
|
||
|
for (let iteration = 1; iteration < iterations; iteration++) {
|
||
|
u = this.hmacSha512(password, u);
|
||
|
|
||
|
// Step 5: XOR U2 with previous result
|
||
|
for (let i = 0; i < hashLength; i++) {
|
||
|
block[i] ^= u[i];
|
||
|
}
|
||
|
|
||
|
// Step 6: Append result to derived key
|
||
|
derivedKey.set(block.subarray(0, Math.min(hashLength, keyLength)), (blockIndex - 1) * hashLength);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return derivedKey;
|
||
|
}
|
||
|
|
||
|
encrypt(plaintext) {
|
||
|
const iv = EonaCatCipher.generateRandomBytes(this.ivSize);
|
||
|
const plaintextBytes = new TextEncoder().encode(plaintext);
|
||
|
const ciphertext = new Uint8Array(plaintextBytes.length);
|
||
|
|
||
|
const cipher = new EonaCatCrypto(this.derivedKey, iv, this.blockSize, this.rounds);
|
||
|
cipher.generate(plaintextBytes, ciphertext, true);
|
||
|
|
||
|
// Combine IV and ciphertext
|
||
|
const result = new Uint8Array(this.ivSize + ciphertext.length);
|
||
|
result.set(iv, 0);
|
||
|
result.set(ciphertext, this.ivSize);
|
||
|
|
||
|
// Generate HMAC for integrity check
|
||
|
const hmac = this.generateHMAC(result);
|
||
|
|
||
|
// Combine result and HMAC
|
||
|
const finalResult = new Uint8Array(result.length + hmac.length);
|
||
|
finalResult.set(result, 0);
|
||
|
finalResult.set(hmac, result.length);
|
||
|
|
||
|
return finalResult;
|
||
|
}
|
||
|
|
||
|
decrypt(ciphertextWithHMAC) {
|
||
|
const hmacOffset = ciphertextWithHMAC.length - 32; // HMAC length (SHA256)
|
||
|
|
||
|
// Separate HMAC from the ciphertext
|
||
|
const providedHMAC = ciphertextWithHMAC.subarray(hmacOffset, ciphertextWithHMAC.length);
|
||
|
|
||
|
const ciphertext = ciphertextWithHMAC.subarray(0, hmacOffset);
|
||
|
|
||
|
// Verify HMAC before decrypting
|
||
|
const calculatedHMAC = this.generateHMAC(ciphertext);
|
||
|
if (!this.areEqual(providedHMAC, calculatedHMAC)) {
|
||
|
throw new Error("EonaCatCipher: HMAC validation failed. Data may have been tampered with.");
|
||
|
}
|
||
|
|
||
|
// Extract IV
|
||
|
const iv = ciphertext.subarray(0, this.ivSize);
|
||
|
|
||
|
// Extract encrypted data
|
||
|
const encryptedData = ciphertext.subarray(this.ivSize);
|
||
|
|
||
|
// Decrypt
|
||
|
const decryptedData = new Uint8Array(encryptedData.length);
|
||
|
const cipher = new EonaCatCrypto(this.derivedKey, iv, this.blockSize, this.rounds);
|
||
|
cipher.generate(encryptedData, decryptedData, false);
|
||
|
|
||
|
return new TextDecoder().decode(decryptedData);
|
||
|
}
|
||
|
|
||
|
generateHMAC(data) {
|
||
|
return this.hmacSha256(this.hmacKey, data);
|
||
|
}
|
||
|
|
||
|
hmacSha512(key, data) {
|
||
|
const hmac = new HMAC(key, "SHA-512");
|
||
|
return hmac.compute(data);
|
||
|
}
|
||
|
|
||
|
hmacSha256(key, data) {
|
||
|
const hmac = new HMAC(key, "SHA-256");
|
||
|
return hmac.compute(data);
|
||
|
}
|
||
|
|
||
|
areEqual(a, b) {
|
||
|
if (a.length !== b.length) return false;
|
||
|
for (let i = 0; i < a.length; i++) {
|
||
|
if (a[i] !== b[i]) return false;
|
||
|
}
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class EonaCatCrypto {
|
||
|
constructor(keyWithSalt, nonce, blockSize, rounds) {
|
||
|
this.rounds = rounds;
|
||
|
this.blockSize = blockSize > 0 ? blockSize : 128;
|
||
|
|
||
|
this.key = new Uint32Array(keyWithSalt.length / 4);
|
||
|
this.key.set(new Uint32Array(keyWithSalt.buffer));
|
||
|
|
||
|
this.nonce = new Uint32Array(nonce.length / 4);
|
||
|
this.nonce.set(new Uint32Array(nonce.buffer));
|
||
|
|
||
|
this.state = new BigUint64Array(this.blockSize / 4);
|
||
|
this.blockCounter = 0;
|
||
|
}
|
||
|
|
||
|
generateBlock(output) {
|
||
|
// Initialize state using a combined operation
|
||
|
for (let i = 0; i < this.state.length; i++) {
|
||
|
this.state[i] = (this.key[i % this.key.length] ^ this.nonce[i % this.nonce.length]) + BigInt(i) * BigInt(0x5DEECE66D);
|
||
|
}
|
||
|
|
||
|
// Mix the states according to the rounds
|
||
|
for (let round = 0; round < this.rounds; round++) {
|
||
|
for (let i = 0; i < this.state.length; i++) {
|
||
|
this.state[i] = (this.state[i] + BigInt(round)) ^ (BigInt(round) * BigInt(0x5DEECE66D)) + BigInt(i + this.blockCounter);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Output block
|
||
|
output.set(new Uint8Array(this.state.buffer));
|
||
|
this.blockCounter++;
|
||
|
}
|
||
|
|
||
|
generate(input, output, encrypt) {
|
||
|
const totalBlocks = Math.ceil(input.length / this.blockSize);
|
||
|
|
||
|
for (let blockIndex = 0; blockIndex < totalBlocks; blockIndex++) {
|
||
|
const inputOffset = blockIndex * this.blockSize;
|
||
|
const outputOffset = blockIndex * this.blockSize;
|
||
|
const block = new Uint8Array(this.blockSize);
|
||
|
|
||
|
// Generate a block based on the input
|
||
|
this.generateBlock(block);
|
||
|
|
||
|
// Perform XOR for encryption or decryption
|
||
|
for (let i = 0; i < block.length && inputOffset + i < input.length; i++) {
|
||
|
output[outputOffset + i] = input[inputOffset + i] ^ block[i];
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class HMAC {
|
||
|
constructor(key, algorithm) {
|
||
|
this.key = key;
|
||
|
this.algorithm = algorithm;
|
||
|
}
|
||
|
|
||
|
compute(data) {
|
||
|
const cryptoKey = crypto.subtle.importKey(
|
||
|
"raw",
|
||
|
this.key,
|
||
|
{ name: "HMAC", hash: { name: this.algorithm } },
|
||
|
false,
|
||
|
["sign", "verify"]
|
||
|
);
|
||
|
|
||
|
return crypto.subtle.sign("HMAC", cryptoKey, data).then(signature => new Uint8Array(signature));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Usage Example
|
||
|
(async () => {
|
||
|
const password = "securePassword123!@#$";
|
||
|
const plaintext = "Thank you for using EonaCatCipher!";
|
||
|
|
||
|
console.log(`Encrypting '${plaintext}' with password '${password}' (we do this 5 times)`);
|
||
|
console.log("================");
|
||
|
|
||
|
for (let i = 0; i < 5; i++) {
|
||
|
console.log(`Encryption round ${i + 1}: `);
|
||
|
console.log("================");
|
||
|
|
||
|
const cipher = new EonaCatCipher(password);
|
||
|
const encrypted = await cipher.encrypt(plaintext);
|
||
|
|
||
|
console.log("Encrypted (byte array): " + Array.from(encrypted).join(', '));
|
||
|
|
||
|
const decrypted = await cipher.decrypt(encrypted);
|
||
|
|
||
|
console.log("Decrypted: " + decrypted);
|
||
|
console.log("================");
|
||
|
}
|
||
|
})();
|