UNPKG

symmetricmorph

Version:

High-performance symmetric stream cipher with dynamic masking, cascading feedback, built-in MAC, and chunked encryption support. Lightweight and dependency-free.

311 lines (310 loc) 12.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * SymmetricMorph * * Unique stream symmetric encryption algorithm with cascading feedback, dynamic masking, * built-in protection against known plaintext attacks, stream MAC and resistance to side attacks. * Developed without the use of third-party libraries. */ class SymmetricMorph { constructor(key, salt) { this.key = key; this.salt = salt; } /** * @returns Returns the salt used for key derivation, if available. */ getSalt() { return this.salt; } /** * Creates a `SymmetricMorph` instance from a password. * @param password - The password to derive the encryption key. * @param iterations - Number of iterations for key derivation (default: 20000). * @param keyLength - Length of the derived key in bytes (default: 64). * @returns A new `SymmetricMorph` instance. */ static fromPassword(password, iterations = 20000, keyLength = 64) { const salt = this.generateSalt(24); const key = this.deriveKey(this.strToBytes(password), salt, iterations, keyLength); return new SymmetricMorph(key, salt); } /** * Creates a `SymmetricMorph` instance from a password and salt. * @param password - The password to derive the encryption key. * @param salt - The salt as an array of bytes or a number array. * @param iterations - Number of iterations for key derivation (default: 20000). * @param keyLength - Length of the derived key in bytes (default: 64). * @returns A new `SymmetricMorph` instance. */ static fromPasswordWithSalt(password, salt, iterations = 20000, keyLength = 64) { const saltArray = salt instanceof Uint8Array ? salt : Uint8Array.from(salt); const key = this.deriveKey(this.strToBytes(password), saltArray, iterations, keyLength); return new SymmetricMorph(key, saltArray); } /** * Creates a `SymmetricMorph` instance from a raw key. * @param key - The encryption key as an array of bytes. * @returns A new `SymmetricMorph` instance. */ static fromKey(key) { return new SymmetricMorph(key); } /** * Generates a random encryption key of the specified length. * @param length - Length of the key in bytes (default: 64). * @returns A randomly generated key as an array of bytes. */ static generateKey(length = 64) { const prng = this.createPrng([Date.now() & 0xFF]); return Uint8Array.from({ length }, (_, i) => prng()); } /** * Encrypts an array of plaintext bytes. * @param plainBytes - The plaintext data as an array of bytes. * @returns The encrypted data as an array of bytes. */ encrypt(plainBytes) { const nonce = SymmetricMorph.generateNonce(); const prng = SymmetricMorph.createPrng([...this.key, ...nonce]); const encrypted = new Uint8Array(plainBytes.length); const state = this.initState(); let feedback = 0xA5; let prev1 = 0x6C, prev2 = 0x3A, prev3 = 0x91; let macAcc = 157; for (let i = 0; i < plainBytes.length; i++) { const r = prng(); const pos = (i + feedback + prev1 + prev2) % state.length; const mask = (state[pos] ^ feedback ^ prev1 ^ prev2 ^ prev3) & 0xFF; const mixed = (plainBytes[i] ^ mask ^ r) & 0xFF; const rotated = ((mixed << (i % 5)) | (mixed >> (8 - (i % 5)))) & 0xFF; encrypted[i] = rotated; this.updateState(state, rotated, feedback, r, i, prev1, prev2, prev3); prev3 = prev2; prev2 = prev1; prev1 = rotated; feedback = (feedback ^ rotated ^ r ^ (i * 13)) & 0xFF; macAcc = ((macAcc + rotated + feedback + (i * 31)) ^ (r + prev1 + prev2)) & 0xFF; } const mac = SymmetricMorph.generateMac(macAcc, this.key); return Uint8Array.from([...nonce, ...mac, ...encrypted]); } /** * Decrypts an array of encrypted bytes. * @param encryptedBytes - The encrypted data as an array of bytes. * @returns The decrypted plaintext data as an array of bytes. * @throws Error if MAC verification fails. */ decrypt(encryptedBytes) { const nonce = encryptedBytes.slice(0, 8); const mac = encryptedBytes.slice(8, 8 + 32); const cipherData = encryptedBytes.slice(8 + 32); const prng = SymmetricMorph.createPrng([...this.key, ...nonce]); const expectedState = this.initState(); let feedback = 0xA5; let prev1 = 0x6C, prev2 = 0x3A, prev3 = 0x91; let macAcc = 157; const decrypted = new Uint8Array(cipherData.length); for (let i = 0; i < cipherData.length; i++) { const r = prng(); const pos = (i + feedback + prev1 + prev2) % expectedState.length; const mask = (expectedState[pos] ^ feedback ^ prev1 ^ prev2 ^ prev3) & 0xFF; const rotated = cipherData[i]; const unrotated = ((rotated >> (i % 5)) | (rotated << (8 - (i % 5)))) & 0xFF; const plain = (unrotated ^ mask ^ r) & 0xFF; decrypted[i] = plain; this.updateState(expectedState, rotated, feedback, r, i, prev1, prev2, prev3); prev3 = prev2; prev2 = prev1; prev1 = rotated; feedback = (feedback ^ rotated ^ r ^ (i * 13)) & 0xFF; macAcc = ((macAcc + rotated + feedback + (i * 31)) ^ (r + prev1 + prev2)) & 0xFF; } const expectedMac = SymmetricMorph.generateMac(macAcc, this.key); if (!SymmetricMorph.constantTimeCompare(mac, expectedMac)) { throw new Error('MAC verification failed (integrity broken)'); } return decrypted; } /** * Encrypts multiple chunks of plaintext data. * @param chunks - An array of plaintext byte arrays. * @returns An array of encrypted byte arrays. */ encryptChunks(chunks) { return chunks.map(chunk => this.encrypt(chunk)); } /** * Decrypts multiple chunks of encrypted data. * @param chunks - An array of encrypted byte arrays. * @returns An array of decrypted plaintext byte arrays. */ decryptChunks(chunks) { return chunks.map(chunk => this.decrypt(chunk)); } /** * Updates the internal state during encryption or decryption. * @param state - The current state array. * @param rotated - The rotated byte value. * @param feedback - The feedback byte. * @param r - The random byte from the PRNG. * @param iteration - The current iteration index. * @param prev1 - The previous byte value (1 step back). * @param prev2 - The previous byte value (2 steps back). * @param prev3 - The previous byte value (3 steps back). */ updateState(state, rotated, feedback, r, iteration, prev1, prev2, prev3) { const len = state.length; const inv = (~rotated) & 0xFF; const mixed = (prev1 ^ prev2 ^ prev3 ^ feedback ^ r) & 0xFF; for (let i = 0; i < len; i++) { state[i] ^= (rotated + mixed + (i * 31)) & 0xFF; state[i] = ((state[i] << (i % 5)) | (state[i] >> (8 - (i % 5)))) & 0xFF; } if (iteration % 4 === 3) { const temp = [...state]; for (let i = 0; i < len; i++) { const swap = (i * 13 + iteration) % len; state[i] = (temp[swap] ^ inv) & 0xFF; } } for (let i = len - 1; i >= 0; i--) { state[i] = (state[i] + (inv ^ (i * 17) ^ feedback)) & 0xFF; state[i] = ((state[i] >>> (i % 7)) | (state[i] << (8 - (i % 7)))) & 0xFF; } } /** * Initializes the internal state array based on the encryption key. * @returns The initialized state array. */ initState() { const state = new Array(64).fill(0); for (let i = 0; i < state.length; i++) { state[i] = (this.key[i % this.key.length] ^ (i * 67 + 19)) & 0xFF; } return state; } /** * Creates a pseudo-random number generator (PRNG) based on a seed. * @param seed - The seed array for the PRNG. * @returns A function that generates random bytes. */ static createPrng(seed) { const state = new Array(64).fill(0).map((_, i) => seed[i % seed.length] ^ ((i * 97) & 0xFF)); let counter = 0; return () => { const i = counter % 64; const j = (counter * 5 + 11) % 64; const k = (counter * 7 + 17) % 64; const val = (state[i] + (state[j] ^ (state[k] >>> 3)) + counter) & 0xFF; state[i] = (state[i] ^ (val << (counter % 7))) & 0xFF; counter++; return val; }; } /** * Derives a cryptographic key from a password and salt using iterative hashing. * @param pass - The password as an array of bytes. * @param salt - The salt as an array of bytes. * @param rounds - The number of hashing iterations. * @param length - The desired key length in bytes. * @returns The derived key as an array of bytes. */ static deriveKey(pass, salt, rounds, length) { let data = Array.from(pass).concat(Array.from(salt)); let state = 0; for (let i = 0; i < rounds; i++) { const next = data.map((b, j) => (b ^ (state + j * 7 + i)) % 256); state = (state + next.reduce((a, b) => a ^ b, 0)) % 256; data = next; } const key = new Uint8Array(length); for (let i = 0; i < length; i++) { key[i] = data[i % data.length] ^ ((i * 37 + state) % 256); } return key; } /** * Generates a Message Authentication Code (MAC) for data integrity verification. * @param data - The data to generate the MAC for. * @param key - The encryption key. * @returns The MAC as an array of bytes. */ static mac(data, key) { const result = new Uint8Array(32); let acc = 137; for (let i = 0; i < 32; i++) { const d = data[i % data.length]; const k = key[i % key.length]; acc = (acc + d + k * (i + 1)) % 256; result[i] = acc ^ ((i * 11 + d) % 256); } return result; } /** * Generates a MAC based on accumulated MAC state and the encryption key. * @param macAcc - The accumulated MAC state. * @param key - The encryption key. * @returns The generated MAC as an array of bytes. */ static generateMac(macAcc, key) { const result = new Uint8Array(32); for (let i = 0; i < 32; i++) { result[i] = (macAcc ^ key[i % key.length] ^ (i * 19)) & 0xFF; } return result; } /** * Generates a random salt for key derivation. * @param length - The length of the salt in bytes. * @returns The generated salt as an array of bytes. */ static generateSalt(length) { const t = performance.now() | 0; const seed = [t & 0xff, (t >> 8) & 0xff, (t >> 16) & 0xff]; const prng = this.createPrng(seed); return Uint8Array.from({ length }, (_, i) => prng()); } /** * Generates a random nonce for encryption. * @returns The generated nonce as an array of bytes. */ static generateNonce() { const t = Date.now(); return Uint8Array.from([ (t >> 0) & 0xff, (t >> 8) & 0xff, (t >> 16) & 0xff, (t >> 24) & 0xff, (t % 251), (t % 241), (t % 239), (t % 233), ]); } /** * Converts a string to an array of bytes. * @param str - The input string. * @returns The string as an array of bytes. */ static strToBytes(str) { return Array.from(str).map(c => c.charCodeAt(0)); } /** * Compares two byte arrays in constant time to prevent timing attacks. * @param a - The first byte array. * @param b - The second byte array. * @returns `true` if the arrays are equal, otherwise `false`. */ static constantTimeCompare(a, b) { if (a.length !== b.length) return false; let result = 0; for (let i = 0; i < a.length; i++) { result |= a[i] ^ b[i]; } return result === 0; } } exports.default = SymmetricMorph;