Skip to content

Instantly share code, notes, and snippets.

@dmorosinotto
Created April 12, 2025 16:10
Show Gist options
  • Save dmorosinotto/659d91f6db36418b0ff8946f90e3b1ef to your computer and use it in GitHub Desktop.
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.
//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