Created
April 12, 2025 16:10
-
-
Save dmorosinotto/659d91f6db36418b0ff8946f90e3b1ef to your computer and use it in GitHub Desktop.
XTEA - A pure TypeScript implementation of XTEA block cipher with support for ECB and CBC modes of operation.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
//INSPIRED BY npm xtea -> https://github.com/simplito/xtea-js/blob/master/xtea.js | |
//+ MINIMAL npm Buffer implementation using Uint8Array -> https://github.com/feross/buffer/blob/master/index.js#L104 | |
type Buffer = Uint8Array; | |
function _allocUnsafe(length: number) { | |
return new Uint8Array(length | 0); | |
} | |
function _readUInt32BE(buffer: Buffer, offset: number) { | |
offset = offset >>> 0; | |
return ( | |
buffer[offset] * 0x1000000 + | |
((buffer[offset + 1] << 16) | | |
(buffer[offset + 2] << 8) | | |
buffer[offset + 3]) | |
); | |
//return buffer[offset] << 24 | buffer[offset + 1] << 16 | buffer[offset + 2] << 8 | buffer[offset + 3]; | |
} | |
function _writeUInt32BE(buffer: Buffer, value: number, offset: number) { | |
value = +value; | |
offset = offset >>> 0; | |
buffer[offset] = value >>> 24; | |
buffer[offset + 1] = value >>> 16; | |
buffer[offset + 2] = value >>> 8; | |
buffer[offset + 3] = value & 0xff; | |
return offset + 4; | |
// offset = offset >>> 0; | |
// buffer[offset] = (value >>> 24); | |
// buffer[offset + 1] = (value >>> 16) & 0xFF; | |
// buffer[offset + 2] = (value >>> 8) & 0xFF; | |
// buffer[offset + 3] = value & 0xFF; | |
} | |
function _concat(list: Buffer[]) { | |
if (list.length === 0) { | |
return new Uint8Array(0); | |
} | |
let length = 0; | |
for (let i = 0; i < list.length; ++i) { | |
length += list[i].length; | |
} | |
const out = new Uint8Array(length); | |
let offset = 0; | |
for (let i = 0; i < list.length; ++i) { | |
out.set(list[i], offset); | |
offset += list[i].length; | |
} | |
return out; | |
} | |
const ROUNDS = 32; | |
const DELTA = 0x9e3779b9; | |
/** @private */ | |
function encipher(v: Uint32Array, k: Uint32Array) { | |
let y = v[0]; | |
let z = v[1]; | |
let sum = 0; | |
const limit = (DELTA * ROUNDS) >>> 0; | |
while (sum !== limit) { | |
y += ((((z << 4) >>> 0) ^ (z >>> 5)) + z) ^ (sum + k[sum & 3]); | |
y = y >>> 0; | |
sum = (sum + DELTA) >>> 0; | |
z += ((((y << 4) >>> 0) ^ (y >>> 5)) + y) ^ (sum + k[(sum >> 11) & 3]); | |
z = z >>> 0; | |
} | |
v[0] = y; | |
v[1] = z; | |
} | |
/** @private */ | |
function decipher(v: Uint32Array, k: Uint32Array) { | |
let y = v[0]; | |
let z = v[1]; | |
let sum = (DELTA * ROUNDS) >>> 0; | |
while (sum) { | |
z -= ((((y << 4) >>> 0) ^ (y >>> 5)) + y) ^ (sum + k[(sum >> 11) & 3]); | |
z = z >>> 0; | |
sum = (sum - DELTA) >>> 0; | |
y -= ((((z << 4) >>> 0) ^ (z >>> 5)) + z) ^ (sum + k[sum & 3]); | |
y = y >>> 0; | |
} | |
v[0] = y; | |
v[1] = z; | |
} | |
/** @private */ | |
function encipher_cbc(v: Uint32Array, k: Uint32Array, iv: Uint32Array) { | |
v[0] ^= iv[0]; | |
v[1] ^= iv[1]; | |
encipher(v, k); | |
iv[0] = v[0]; | |
iv[1] = v[1]; | |
} | |
/** @private */ | |
function decipher_cbc(v: Uint32Array, k: Uint32Array, iv: Uint32Array) { | |
let tmp = new Uint32Array(v); | |
decipher(v, k); | |
v[0] ^= iv[0]; | |
v[1] ^= iv[1]; | |
iv[0] = tmp[0]; | |
iv[1] = tmp[1]; | |
} | |
/** @private */ | |
function doBlock( | |
method: (v: Uint32Array, k: Uint32Array) => void, | |
block: Buffer, | |
key: Buffer | |
) { | |
const k = new Uint32Array(4); | |
const v = new Uint32Array(2); | |
const out = _allocUnsafe(8); | |
for (let i = 0; i < 4; ++i) { | |
k[i] = _readUInt32BE(key, i * 4); | |
} | |
v[0] = _readUInt32BE(block, 0); | |
v[1] = _readUInt32BE(block, 4); | |
method(v, k); | |
_writeUInt32BE(out, v[0], 0); | |
_writeUInt32BE(out, v[1], 4); | |
return out; | |
} | |
const MODES = { | |
ecb: { encrypt: encipher, decrypt: decipher }, | |
cbc: { encrypt: encipher_cbc, decrypt: decipher_cbc }, | |
}; | |
/** @private */ | |
function doBlocks( | |
encryption: boolean, | |
msg: Buffer, | |
key: Buffer, | |
mode: 'ecb' | 'cbc', | |
ivbuf?: Buffer, | |
skippad?: boolean | |
) { | |
mode = mode || 'ecb'; | |
if (!ivbuf) { | |
ivbuf = _allocUnsafe(8); | |
ivbuf.fill(0); | |
} | |
const mode_ = MODES[mode]; | |
if (!mode_) { | |
throw new Error('Unimplemented mode: ' + mode); | |
} | |
let method; | |
if (encryption) { | |
method = mode_.encrypt; | |
} else { | |
method = mode_.decrypt; | |
} | |
const length = msg.length; | |
let pad = 8 - (length & 7); | |
if (skippad || !encryption) { | |
if (pad !== 8) { | |
throw new Error('Data not aligned to 8 bytes block boundary'); | |
} | |
pad = 0; | |
} | |
const out = _allocUnsafe(length + pad); | |
const k = new Uint32Array(4); | |
const v = new Uint32Array(2); | |
const iv = new Uint32Array(2); | |
iv[0] = _readUInt32BE(ivbuf, 0); | |
iv[1] = _readUInt32BE(ivbuf, 4); | |
for (let i = 0; i < 4; ++i) { | |
k[i] = _readUInt32BE(key, i * 4); | |
} | |
let offset = 0; | |
while (offset <= length) { | |
if (length - offset < 8) { | |
if (skippad || !encryption) { | |
break; | |
} | |
let buf = _allocUnsafe(pad); | |
buf.fill(pad); | |
buf = _concat([msg.slice(offset), buf]); | |
v[0] = _readUInt32BE(buf, 0); | |
v[1] = _readUInt32BE(buf, 4); | |
} else { | |
v[0] = _readUInt32BE(msg, offset); | |
v[1] = _readUInt32BE(msg, offset + 4); | |
} | |
method(v, k, iv); | |
_writeUInt32BE(out, v[0], offset); | |
_writeUInt32BE(out, v[1], offset + 4); | |
offset += 8; | |
} | |
if (skippad || encryption) return out; | |
pad = out[out.length - 1]; | |
return out.slice(0, out.length - pad); | |
} | |
/** | |
* Encrypts single block of data using XTEA cipher. | |
* | |
* @param {Buffer} block 64-bit (8-bytes) block of data to encrypt | |
* @param {Buffer} key 128-bit (16-bytes) encryption key | |
* @returns {Buffer} 64-bit of encrypted block | |
*/ | |
function encryptBlock(block: Buffer, key: Buffer): Buffer { | |
return doBlock(encipher, block, key); | |
} | |
/** | |
* Decrypts single block of data using XTEA cipher. | |
* | |
* @param {Buffer} block 64-bit (8-bytes) block of data to encrypt | |
* @param {Buffer} key 128-bit (16-bytes) encryption key | |
* @returns {Buffer} 64-bit of encrypted block | |
*/ | |
function decryptBlock(block: Buffer, key: Buffer) { | |
return doBlock(decipher, block, key); | |
} | |
/** | |
* Encrypts data using XTEA cipher using specified block cipher mode of operation | |
* and PKCS#7 padding. | |
* | |
* @param {Buffer} msg Message to encrypt | |
* @param {Buffer} key 128-bit encryption key (16 bytes) | |
* @param {string} [mode=ecb] Block cipher mode of operation (currently only 'ecb' or 'cbc') | |
* @param {Buffer} [iv] Optional IV | |
* @param {bool} [skippad] Skip PKCS#7 padding postprocessing | |
* @returns {Buffer} | |
*/ | |
function encrypt( | |
msg: Buffer, | |
key: Buffer, | |
mode: 'ecb' | 'cbc', | |
ivbuf?: Buffer, | |
skippad?: boolean | |
): Buffer { | |
return doBlocks(true, msg, key, mode, ivbuf, skippad); | |
} | |
/** | |
* Decrypts data using XTEA cipher using specified block cipher mode of operation | |
* and PKCS#7 padding. | |
* | |
* @param {Buffer} msg Ciphertext to decrypt | |
* @param {Buffer} key 128-bit encryption key (16 bytes) | |
* @param {string} [mode=ecb] Block cipher mode of operation (currently only 'ecb' or 'cbc') | |
* @param {Buffer} [iv] Optional IV | |
* @param {bool} [skippad] Skip PKCS#7 padding postprocessing | |
* @returns {Buffer} | |
*/ | |
function decrypt( | |
msg: Buffer, | |
key: Buffer, | |
mode: 'ecb' | 'cbc', | |
ivbuf?: Buffer, | |
skippad?: boolean | |
): Buffer { | |
return doBlocks(false, msg, key, mode, ivbuf, skippad); | |
} | |
export function generateKey() { | |
const key = _allocUnsafe(16); // 128 bits | |
key.fill(0); | |
for (let i = 0; i < key.length; ++i) { | |
key[i] = Math.floor(Math.random() * 256); | |
} | |
return key; | |
} | |
export function xteaEncrypt( | |
msg: string, | |
options?: { | |
key: Buffer; | |
mode?: 'ecb' | 'cbc'; | |
ivbuf?: Buffer; | |
} | |
): string { | |
const { key, mode, ivbuf } = options || { key: generateKey() }; | |
const utf8 = new TextEncoder(); | |
const msgBuf = utf8.encode(msg); | |
const cryptedBuf = encrypt(msgBuf, key, mode || 'ecb', ivbuf); | |
// SE MI PASSANO LA KEY ALLORA RITORNO SOLO CYPHER_TEXT ALTRIMENTI RITORNO KEY|CYPHER_TEXT | |
const base64 = options?.key | |
? btoa(cryptedBuf.toString()) | |
: btoa(key.toString() + '|' + cryptedBuf.toString()); //Buffer.toString() ritorna numeri (byte) separati da virgola! | |
return base64; | |
} | |
export function xteaDecrypt( | |
msg: string, | |
options?: { | |
key: Buffer; | |
mode?: 'ecb' | 'cbc'; | |
ivbuf?: Buffer; | |
} | |
): string { | |
const str = atob(msg); | |
let key: Buffer; | |
let buf: Buffer; | |
if (!str.includes('|')) { | |
if (!options?.key) { | |
throw new Error('Key not provided'); | |
} | |
key = options.key; | |
buf = new Uint8Array(str.split(',').map(Number)); | |
} else { | |
const [keyStr, cypherStr] = str.split('|'); | |
key = new Uint8Array(keyStr.split(',').map(Number)); | |
buf = new Uint8Array(cypherStr.split(',').map(Number)); | |
} | |
const decryptedBuf = decrypt( | |
buf, | |
key, | |
options?.mode || 'ecb', | |
options?.ivbuf | |
); | |
const utf8 = new TextDecoder(); | |
return utf8.decode(decryptedBuf); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment