'use strict'; /* Utilities for scrambling strings to obscure puzzle answers. Not * intended to be secure. */ // From https://stackoverflow.com/a/34310051 function toHexString(buffer) { let byteArray = new Uint8Array(buffer); return Array.from(byteArray, function(byte) { return ('0' + (byte & 0xFF).toString(16)).slice(-2); }).join(''); } function fromHexString(str) { let buffer = new ArrayBuffer(str.length / 2); let byteArray = new Uint8Array(buffer); let i = 0; for (let idx = 0; idx < str.length; idx += 2) { let hex = str.substring(idx, idx+2); byteArray[i++] = parseInt(hex, 16); } return buffer; } // "Encrypt" a string and include the key just as a way of scrambling it // to make it hard to read for a human. async function getDefaultKeyAndIv() { let key = await window.crypto.subtle.importKey('raw', new ArrayBuffer(32), { name: "AES-GCM" }, false, ['encrypt', 'decrypt']); let iv = new Uint8Array(12); return [key, iv]; } async function encryptStringNoKey(plaintext) { let encoded = new TextEncoder().encode(plaintext); let [key, iv] = await getDefaultKeyAndIv(); let ciphertext = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, key, encoded ); return toHexString(ciphertext); } async function decryptStringNoKey(ciphertextStr) { let [key, iv] = await getDefaultKeyAndIv(); let ciphertext = fromHexString(ciphertextStr); let decrypted = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, key, ciphertext ); let plaintext = new TextDecoder().decode(decrypted); return plaintext; } async function encryptString(plaintext) { let encoded = new TextEncoder().encode(plaintext); let key = await window.crypto.subtle.generateKey( { name: "AES-GCM", length: 256, }, true, ["encrypt", "decrypt"] ); let iv = await window.crypto.getRandomValues(new Uint8Array(12)); let ciphertext = await window.crypto.subtle.encrypt( { name: "AES-GCM", iv: iv }, key, encoded ); let keyBuffer = await window.crypto.subtle.exportKey('raw', key); return toHexString(keyBuffer) + ',' + toHexString(iv) + ',' + toHexString(ciphertext); } async function decryptString(str) { let [keyStr, ivStr, ciphertextStr] = str.split(','); let keyBuffer = fromHexString(keyStr); let iv = fromHexString(ivStr); let ciphertext = fromHexString(ciphertextStr); let key = await window.crypto.subtle.importKey('raw', keyBuffer, { name: "AES-GCM" }, false, ['decrypt']); let decrypted = await window.crypto.subtle.decrypt( { name: "AES-GCM", iv: iv }, key, ciphertext ); let plaintext = new TextDecoder().decode(decrypted); return plaintext; } async function _scrambleString(str, kind) { if (kind[0] == 'B') { return 'B' + btoa(await _scrambleString(str, kind.substring(1))); } else if (kind[0] == 'E') { return 'E' + await encryptString( await _scrambleString(str, kind.substring(1))); } else if (kind[0] == 'A') { return 'A' + await encryptStringNoKey( await _scrambleString(str, kind.substring(1))); } else if(!kind) { return str; } else { throw new Exception("Unknown scramble kind: " + kind); } } function scrambleString(str, scrambleKinds) { let res = null; for (let i in scrambleKinds) { let kind = scrambleKinds[i]; if (res === null) { res = _scrambleString(str, kind); } else { res = res.catch(_ => _scrambleString(str, kind)); } } return res; } async function unscrambleString(str) { try { if (str[0] == 'B') { return await unscrambleString(atob(str.substring(1))); } else if (str[0] == 'E') { return await unscrambleString( await decryptString(str.substring(1))); } else if (str[0] == 'A') { return await unscrambleString( await decryptStringNoKey(str.substring(1))); } else { return str; } } catch(e) { return null; } }