noVNC/core/crypto/aes.js

373 lines
13 KiB
JavaScript

export class AESECBCipher {
constructor() {
this._key = null;
this._jsCipher = null;
}
get algorithm() {
return { name: "AES-ECB" };
}
static async importKey(key, _algorithm, extractable, keyUsages) {
const cipher = new AESECBCipher;
await cipher._importKey(key, extractable, keyUsages);
return cipher;
}
async _importKey(key, extractable, keyUsages) {
if (window?.crypto?.subtle) {
try {
this._key = await window.crypto.subtle.importKey(
"raw", key, { name: "AES-CBC" }, extractable, keyUsages);
this._jsCipher = null;
return;
} catch (_e) {
// Fall through to JS fallback
}
}
this._key = null;
this._jsCipher = new JSAESECB(key);
}
async encrypt(_algorithm, plaintext) {
const input = new Uint8Array(plaintext);
if (input.length % 16 !== 0) {
return null;
}
// WebCrypto fast path
if (this._key !== null && window?.crypto?.subtle) {
try {
const blocks = input.length / 16;
const out = new Uint8Array(input.length);
for (let i = 0; i < blocks; i++) {
const block = input.slice(i * 16, i * 16 + 16);
const enc = await window.crypto.subtle.encrypt({
name: "AES-CBC",
iv: new Uint8Array(16),
}, this._key, block);
const truncated = new Uint8Array(enc).slice(0, 16);
out.set(truncated, i * 16);
}
return out;
} catch (_e) {
// Fallback handled below
}
}
// JS fallback for non-secure contexts (no SubtleCrypto)
if (this._jsCipher !== null) {
const blocks = input.length / 16;
const out = new Uint8Array(input.length);
for (let i = 0; i < blocks; i++) {
const block = input.slice(i * 16, i * 16 + 16);
const enc = this._jsCipher.encryptBlock(block);
out.set(enc, i * 16);
}
return out;
}
return null;
}
}
// Minimal AES-128 ECB implementation used as a fallback when SubtleCrypto
// is not available (e.g. non-secure contexts). Only encryption is implemented.
class JSAESECB {
constructor(keyBytes) {
if (!(keyBytes instanceof Uint8Array)) {
throw new Error("AES key must be Uint8Array");
}
if (keyBytes.length !== 16) {
// ARD uses MD5-derived 16-byte key (AES-128)
throw new Error("Only AES-128 is supported in JS fallback");
}
this._sbox = JSAESECB._SBOX;
this._rcon = JSAESECB._RCON;
this._roundKeys = this._keyExpansion(keyBytes);
}
encryptBlock(block) {
if (!(block instanceof Uint8Array) || block.length !== 16) {
throw new Error("AES block must be 16 bytes");
}
const state = new Uint8Array(block);
const rk = this._roundKeys;
this._addRoundKey(state, rk, 0);
for (let round = 1; round <= 9; round++) {
this._subBytes(state);
this._shiftRows(state);
this._mixColumns(state);
this._addRoundKey(state, rk, round);
}
this._subBytes(state);
this._shiftRows(state);
this._addRoundKey(state, rk, 10);
return state;
}
_addRoundKey(state, roundKeys, round) {
const offset = round * 16;
for (let i = 0; i < 16; i++) {
state[i] ^= roundKeys[offset + i];
}
}
_subBytes(state) {
const s = this._sbox;
for (let i = 0; i < 16; i++) {
state[i] = s[state[i]];
}
}
_shiftRows(state) {
// Row 0 stays
// Row 1: shift left by 1
let t = state[1];
state[1] = state[5];
state[5] = state[9];
state[9] = state[13];
state[13] = t;
// Row 2: shift left by 2
t = state[2];
let t2 = state[6];
state[2] = state[10];
state[6] = state[14];
state[10] = t;
state[14] = t2;
// Row 3: shift left by 3
t = state[3];
state[3] = state[15];
state[15] = state[11];
state[11] = state[7];
state[7] = t;
}
_mixColumns(state) {
for (let c = 0; c < 4; c++) {
const i = c * 4;
const a0 = state[i];
const a1 = state[i + 1];
const a2 = state[i + 2];
const a3 = state[i + 3];
const a0x = JSAESECB._xtime(a0);
const a1x = JSAESECB._xtime(a1);
const a2x = JSAESECB._xtime(a2);
const a3x = JSAESECB._xtime(a3);
state[i] = a0x ^ (a1 ^ a1x) ^ a2 ^ a3;
state[i + 1] = a0 ^ a1x ^ (a2 ^ a2x) ^ a3;
state[i + 2] = a0 ^ a1 ^ a2x ^ (a3 ^ a3x);
state[i + 3] = (a0 ^ a0x) ^ a1 ^ a2 ^ a3x;
}
}
_keyExpansion(key) {
const sbox = this._sbox;
const rcon = this._rcon;
const w = new Uint8Array(176); // 11 * 16
// first 16 bytes are the key
w.set(key);
let bytesGenerated = 16;
let rconIter = 1;
const temp = new Uint8Array(4);
while (bytesGenerated < 176) {
for (let i = 0; i < 4; i++) {
temp[i] = w[bytesGenerated - 4 + i];
}
if (bytesGenerated % 16 === 0) {
// RotWord
const t = temp[0];
temp[0] = temp[1];
temp[1] = temp[2];
temp[2] = temp[3];
temp[3] = t;
// SubWord
for (let i = 0; i < 4; i++) {
temp[i] = sbox[temp[i]];
}
temp[0] ^= rcon[rconIter++];
}
for (let i = 0; i < 4; i++) {
w[bytesGenerated] = w[bytesGenerated - 16] ^ temp[i];
bytesGenerated++;
}
}
return w;
}
static _xtime(a) {
return ((a << 1) & 0xff) ^ ((a & 0x80) ? 0x1b : 0x00);
}
// Precomputed S-box and Rcon tables
static get _SBOX() {
return new Uint8Array([
0x63,0x7c,0x77,0x7b,0xf2,0x6b,0x6f,0xc5,0x30,0x01,0x67,0x2b,0xfe,0xd7,0xab,0x76,
0xca,0x82,0xc9,0x7d,0xfa,0x59,0x47,0xf0,0xad,0xd4,0xa2,0xaf,0x9c,0xa4,0x72,0xc0,
0xb7,0xfd,0x93,0x26,0x36,0x3f,0xf7,0xcc,0x34,0xa5,0xe5,0xf1,0x71,0xd8,0x31,0x15,
0x04,0xc7,0x23,0xc3,0x18,0x96,0x05,0x9a,0x07,0x12,0x80,0xe2,0xeb,0x27,0xb2,0x75,
0x09,0x83,0x2c,0x1a,0x1b,0x6e,0x5a,0xa0,0x52,0x3b,0xd6,0xb3,0x29,0xe3,0x2f,0x84,
0x53,0xd1,0x00,0xed,0x20,0xfc,0xb1,0x5b,0x6a,0xcb,0xbe,0x39,0x4a,0x4c,0x58,0xcf,
0xd0,0xef,0xaa,0xfb,0x43,0x4d,0x33,0x85,0x45,0xf9,0x02,0x7f,0x50,0x3c,0x9f,0xa8,
0x51,0xa3,0x40,0x8f,0x92,0x9d,0x38,0xf5,0xbc,0xb6,0xda,0x21,0x10,0xff,0xf3,0xd2,
0xcd,0x0c,0x13,0xec,0x5f,0x97,0x44,0x17,0xc4,0xa7,0x7e,0x3d,0x64,0x5d,0x19,0x73,
0x60,0x81,0x4f,0xdc,0x22,0x2a,0x90,0x88,0x46,0xee,0xb8,0x14,0xde,0x5e,0x0b,0xdb,
0xe0,0x32,0x3a,0x0a,0x49,0x06,0x24,0x5c,0xc2,0xd3,0xac,0x62,0x91,0x95,0xe4,0x79,
0xe7,0xc8,0x37,0x6d,0x8d,0xd5,0x4e,0xa9,0x6c,0x56,0xf4,0xea,0x65,0x7a,0xae,0x08,
0xba,0x78,0x25,0x2e,0x1c,0xa6,0xb4,0xc6,0xe8,0xdd,0x74,0x1f,0x4b,0xbd,0x8b,0x8a,
0x70,0x3e,0xb5,0x66,0x48,0x03,0xf6,0x0e,0x61,0x35,0x57,0xb9,0x86,0xc1,0x1d,0x9e,
0xe1,0xf8,0x98,0x11,0x69,0xd9,0x8e,0x94,0x9b,0x1e,0x87,0xe9,0xce,0x55,0x28,0xdf,
0x8c,0xa1,0x89,0x0d,0xbf,0xe6,0x42,0x68,0x41,0x99,0x2d,0x0f,0xb0,0x54,0xbb,0x16
]);
}
static get _RCON() {
return new Uint8Array([
0x00,0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80,0x1b,0x36
]);
}
}
export class AESEAXCipher {
constructor() {
this._rawKey = null;
this._ctrKey = null;
this._cbcKey = null;
this._zeroBlock = new Uint8Array(16);
this._prefixBlock0 = this._zeroBlock;
this._prefixBlock1 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1]);
this._prefixBlock2 = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2]);
}
get algorithm() {
return { name: "AES-EAX" };
}
async _encryptBlock(block) {
const encrypted = await window.crypto.subtle.encrypt({
name: "AES-CBC",
iv: this._zeroBlock,
}, this._cbcKey, block);
return new Uint8Array(encrypted).slice(0, 16);
}
async _initCMAC() {
const k1 = await this._encryptBlock(this._zeroBlock);
const k2 = new Uint8Array(16);
const v = k1[0] >>> 6;
for (let i = 0; i < 15; i++) {
k2[i] = (k1[i + 1] >> 6) | (k1[i] << 2);
k1[i] = (k1[i + 1] >> 7) | (k1[i] << 1);
}
const lut = [0x0, 0x87, 0x0e, 0x89];
k2[14] ^= v >>> 1;
k2[15] = (k1[15] << 2) ^ lut[v];
k1[15] = (k1[15] << 1) ^ lut[v >> 1];
this._k1 = k1;
this._k2 = k2;
}
async _encryptCTR(data, counter) {
const encrypted = await window.crypto.subtle.encrypt({
name: "AES-CTR",
counter: counter,
length: 128
}, this._ctrKey, data);
return new Uint8Array(encrypted);
}
async _decryptCTR(data, counter) {
const decrypted = await window.crypto.subtle.decrypt({
name: "AES-CTR",
counter: counter,
length: 128
}, this._ctrKey, data);
return new Uint8Array(decrypted);
}
async _computeCMAC(data, prefixBlock) {
if (prefixBlock.length !== 16) {
return null;
}
const n = Math.floor(data.length / 16);
const m = Math.ceil(data.length / 16);
const r = data.length - n * 16;
const cbcData = new Uint8Array((m + 1) * 16);
cbcData.set(prefixBlock);
cbcData.set(data, 16);
if (r === 0) {
for (let i = 0; i < 16; i++) {
cbcData[n * 16 + i] ^= this._k1[i];
}
} else {
cbcData[(n + 1) * 16 + r] = 0x80;
for (let i = 0; i < 16; i++) {
cbcData[(n + 1) * 16 + i] ^= this._k2[i];
}
}
let cbcEncrypted = await window.crypto.subtle.encrypt({
name: "AES-CBC",
iv: this._zeroBlock,
}, this._cbcKey, cbcData);
cbcEncrypted = new Uint8Array(cbcEncrypted);
const mac = cbcEncrypted.slice(cbcEncrypted.length - 32, cbcEncrypted.length - 16);
return mac;
}
static async importKey(key, _algorithm, _extractable, _keyUsages) {
const cipher = new AESEAXCipher;
await cipher._importKey(key);
return cipher;
}
async _importKey(key) {
this._rawKey = key;
this._ctrKey = await window.crypto.subtle.importKey(
"raw", key, {name: "AES-CTR"}, false, ["encrypt", "decrypt"]);
this._cbcKey = await window.crypto.subtle.importKey(
"raw", key, {name: "AES-CBC"}, false, ["encrypt"]);
await this._initCMAC();
}
async encrypt(algorithm, message) {
const ad = algorithm.additionalData;
const nonce = algorithm.iv;
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
const encrypted = await this._encryptCTR(message, nCMAC);
const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
const mac = await this._computeCMAC(encrypted, this._prefixBlock2);
for (let i = 0; i < 16; i++) {
mac[i] ^= nCMAC[i] ^ adCMAC[i];
}
const res = new Uint8Array(16 + encrypted.length);
res.set(encrypted);
res.set(mac, encrypted.length);
return res;
}
async decrypt(algorithm, data) {
const encrypted = data.slice(0, data.length - 16);
const ad = algorithm.additionalData;
const nonce = algorithm.iv;
const mac = data.slice(data.length - 16);
const nCMAC = await this._computeCMAC(nonce, this._prefixBlock0);
const adCMAC = await this._computeCMAC(ad, this._prefixBlock1);
const computedMac = await this._computeCMAC(encrypted, this._prefixBlock2);
for (let i = 0; i < 16; i++) {
computedMac[i] ^= nCMAC[i] ^ adCMAC[i];
}
if (computedMac.length !== mac.length) {
return null;
}
for (let i = 0; i < mac.length; i++) {
if (computedMac[i] !== mac[i]) {
return null;
}
}
const res = await this._decryptCTR(encrypted, nCMAC);
return res;
}
}