UNPKG

@nightnetwork/obscura

Version:
337 lines (320 loc) 14.7 kB
// src/cipher.ts function shuffle(seedBytes) { let letters = "abcdefghijklmnopqrstuvwxyz".split(""), seed = (seedBytes[0] << 24 | seedBytes[1] << 16 | seedBytes[2] << 8 | seedBytes[3]) >>> 0; function rand() { return seed = seed * 1831565813 + 1 >>> 0, seed; } for (let x = letters.length - 1; x > 0; x--) { let i = Math.floor(rand() / 4294967295 * (x + 1)); [letters[x], letters[i]] = [letters[i], letters[x]]; } return letters.join(""); } function apply(txt, map) { return txt.toLowerCase().split("").map((ch) => { let idx = ch.charCodeAt(0) - 97; return idx >= 0 && idx < 26 ? map[idx] : ch; }).join(""); } // src/hash.ts async function sha256(bytes) { let hashbuffer = await crypto.subtle.digest("SHA-256", bytes); return new Uint8Array(hashbuffer); } async function deriveKey(input) { let data = new TextEncoder().encode(input); return sha256(data); } // src/helpers.ts function uuidToBytes(uuid) { let hex = uuid.replace(/-/g, ""), bytes = new Uint8Array(16); for (let i = 0; i < 16; i++) bytes[i] = parseInt(hex.slice(i * 2, i * 2 + 2), 16); return bytes; } // node_modules/uuid/dist/esm-browser/regex.js var regex_default = /^(?:[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$/i; // node_modules/uuid/dist/esm-browser/validate.js function validate(uuid) { return typeof uuid == "string" && regex_default.test(uuid); } var validate_default = validate; // node_modules/uuid/dist/esm-browser/parse.js function parse(uuid) { if (!validate_default(uuid)) throw TypeError("Invalid UUID"); let v; return Uint8Array.of((v = parseInt(uuid.slice(0, 8), 16)) >>> 24, v >>> 16 & 255, v >>> 8 & 255, v & 255, (v = parseInt(uuid.slice(9, 13), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(14, 18), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(19, 23), 16)) >>> 8, v & 255, (v = parseInt(uuid.slice(24, 36), 16)) / 1099511627776 & 255, v / 4294967296 & 255, v >>> 24 & 255, v >>> 16 & 255, v >>> 8 & 255, v & 255); } var parse_default = parse; // node_modules/uuid/dist/esm-browser/stringify.js var byteToHex = []; for (let i = 0; i < 256; ++i) byteToHex.push((i + 256).toString(16).slice(1)); function unsafeStringify(arr, offset = 0) { return (byteToHex[arr[offset + 0]] + byteToHex[arr[offset + 1]] + byteToHex[arr[offset + 2]] + byteToHex[arr[offset + 3]] + "-" + byteToHex[arr[offset + 4]] + byteToHex[arr[offset + 5]] + "-" + byteToHex[arr[offset + 6]] + byteToHex[arr[offset + 7]] + "-" + byteToHex[arr[offset + 8]] + byteToHex[arr[offset + 9]] + "-" + byteToHex[arr[offset + 10]] + byteToHex[arr[offset + 11]] + byteToHex[arr[offset + 12]] + byteToHex[arr[offset + 13]] + byteToHex[arr[offset + 14]] + byteToHex[arr[offset + 15]]).toLowerCase(); } // node_modules/uuid/dist/esm-browser/rng.js var getRandomValues, rnds8 = new Uint8Array(16); function rng() { if (!getRandomValues) { if (typeof crypto > "u" || !crypto.getRandomValues) throw new Error("crypto.getRandomValues() not supported. See https://github.com/uuidjs/uuid#getrandomvalues-not-supported"); getRandomValues = crypto.getRandomValues.bind(crypto); } return getRandomValues(rnds8); } // node_modules/uuid/dist/esm-browser/v35.js function stringToBytes(str) { str = unescape(encodeURIComponent(str)); let bytes = new Uint8Array(str.length); for (let i = 0; i < str.length; ++i) bytes[i] = str.charCodeAt(i); return bytes; } var DNS = "6ba7b810-9dad-11d1-80b4-00c04fd430c8", URL = "6ba7b811-9dad-11d1-80b4-00c04fd430c8"; function v35(version, hash, value, namespace2, buf, offset) { let valueBytes = typeof value == "string" ? stringToBytes(value) : value, namespaceBytes = typeof namespace2 == "string" ? parse_default(namespace2) : namespace2; if (typeof namespace2 == "string" && (namespace2 = parse_default(namespace2)), namespace2?.length !== 16) throw TypeError("Namespace must be array-like (16 iterable integer values, 0-255)"); let bytes = new Uint8Array(16 + valueBytes.length); if (bytes.set(namespaceBytes), bytes.set(valueBytes, namespaceBytes.length), bytes = hash(bytes), bytes[6] = bytes[6] & 15 | version, bytes[8] = bytes[8] & 63 | 128, buf) { offset = offset || 0; for (let i = 0; i < 16; ++i) buf[offset + i] = bytes[i]; return buf; } return unsafeStringify(bytes); } // node_modules/uuid/dist/esm-browser/native.js var randomUUID = typeof crypto < "u" && crypto.randomUUID && crypto.randomUUID.bind(crypto), native_default = { randomUUID }; // node_modules/uuid/dist/esm-browser/v4.js function v4(options, buf, offset) { if (native_default.randomUUID && !buf && !options) return native_default.randomUUID(); options = options || {}; let rnds = options.random ?? options.rng?.() ?? rng(); if (rnds.length < 16) throw new Error("Random bytes length must be >= 16"); if (rnds[6] = rnds[6] & 15 | 64, rnds[8] = rnds[8] & 63 | 128, buf) { if (offset = offset || 0, offset < 0 || offset + 16 > buf.length) throw new RangeError(`UUID byte range ${offset}:${offset + 15} is out of buffer bounds`); for (let i = 0; i < 16; ++i) buf[offset + i] = rnds[i]; return buf; } return unsafeStringify(rnds); } var v4_default = v4; // node_modules/uuid/dist/esm-browser/sha1.js function f(s, x, y, z) { switch (s) { case 0: return x & y ^ ~x & z; case 1: return x ^ y ^ z; case 2: return x & y ^ x & z ^ y & z; case 3: return x ^ y ^ z; } } function ROTL(x, n) { return x << n | x >>> 32 - n; } function sha1(bytes) { let K = [1518500249, 1859775393, 2400959708, 3395469782], H = [1732584193, 4023233417, 2562383102, 271733878, 3285377520], newBytes = new Uint8Array(bytes.length + 1); newBytes.set(bytes), newBytes[bytes.length] = 128, bytes = newBytes; let l = bytes.length / 4 + 2, N = Math.ceil(l / 16), M = new Array(N); for (let i = 0; i < N; ++i) { let arr = new Uint32Array(16); for (let j = 0; j < 16; ++j) arr[j] = bytes[i * 64 + j * 4] << 24 | bytes[i * 64 + j * 4 + 1] << 16 | bytes[i * 64 + j * 4 + 2] << 8 | bytes[i * 64 + j * 4 + 3]; M[i] = arr; } M[N - 1][14] = (bytes.length - 1) * 8 / Math.pow(2, 32), M[N - 1][14] = Math.floor(M[N - 1][14]), M[N - 1][15] = (bytes.length - 1) * 8 & 4294967295; for (let i = 0; i < N; ++i) { let W = new Uint32Array(80); for (let t = 0; t < 16; ++t) W[t] = M[i][t]; for (let t = 16; t < 80; ++t) W[t] = ROTL(W[t - 3] ^ W[t - 8] ^ W[t - 14] ^ W[t - 16], 1); let a = H[0], b = H[1], c = H[2], d = H[3], e = H[4]; for (let t = 0; t < 80; ++t) { let s = Math.floor(t / 20), T = ROTL(a, 5) + f(s, b, c, d) + e + K[s] + W[t] >>> 0; e = d, d = c, c = ROTL(b, 30) >>> 0, b = a, a = T; } H[0] = H[0] + a >>> 0, H[1] = H[1] + b >>> 0, H[2] = H[2] + c >>> 0, H[3] = H[3] + d >>> 0, H[4] = H[4] + e >>> 0; } return Uint8Array.of(H[0] >> 24, H[0] >> 16, H[0] >> 8, H[0], H[1] >> 24, H[1] >> 16, H[1] >> 8, H[1], H[2] >> 24, H[2] >> 16, H[2] >> 8, H[2], H[3] >> 24, H[3] >> 16, H[3] >> 8, H[3], H[4] >> 24, H[4] >> 16, H[4] >> 8, H[4]); } var sha1_default = sha1; // node_modules/uuid/dist/esm-browser/v5.js function v5(value, namespace2, buf, offset) { return v35(80, sha1_default, value, namespace2, buf, offset); } v5.DNS = DNS; v5.URL = URL; var v5_default = v5; // src/idbStore.ts var IDBStore = class { constructor(dbName = "obscura", storeName = "obscura") { this.dbName = dbName, this.storeName = storeName, this.dbPromise = this.openDB(); } openDB() { return new Promise((resolve, reject) => { let request = indexedDB.open(this.dbName, 1); request.onupgradeneeded = () => { request.result.createObjectStore(this.storeName); }, request.onsuccess = () => resolve(request.result), request.onerror = () => reject(request.error); }); } async getItem(key) { let db = await this.dbPromise; return new Promise((resolve, reject) => { let req = db.transaction(this.storeName, "readonly").objectStore(this.storeName).get(key); req.onsuccess = () => resolve(req.result ?? null), req.onerror = () => reject(req.error); }); } async setItem(key, value) { let db = await this.dbPromise; return new Promise((resolve, reject) => { let req = db.transaction(this.storeName, "readwrite").objectStore(this.storeName).put(value, key); req.onsuccess = () => resolve(), req.onerror = () => reject(req.error); }); } }; // src/uuids.ts var idb = new IDBStore("obscura", "obscura"); async function initMaster() { let existing = await idb.getItem("masterUUID"); if (existing) return existing; let newUUID = v4_default(); return await idb.setItem("masterUUID", newUUID), newUUID; } async function namespace(initialKeyHash, master) { let masterBytes = new TextEncoder().encode(master), combined = new Uint8Array(initialKeyHash.length + masterBytes.length); return combined.set(initialKeyHash), combined.set(masterBytes, initialKeyHash.length), (await sha256(combined)).slice(0, 16); } function convertFinal(xor, NAMESPACE) { return v5_default(xor, NAMESPACE); } // src/xor.ts function xorEncode(input, key) { let data = typeof input == "string" ? new TextEncoder().encode(input) : input, keyBytes = Array.from(key).map((ch) => ch.charCodeAt(0)), output = ""; for (let i = 0; i < data.length; i++) { let xorByte = data[i] ^ keyBytes[i % keyBytes.length]; output += String.fromCharCode(xorByte); } return encodeURIComponent(output); } function xorDecode(input, key) { let rawStr = typeof input == "string" ? decodeURIComponent(input) : Array.from(input).map((b) => String.fromCharCode(b)).join(""), keyBytes = Array.from(key).map((ch) => ch.charCodeAt(0)), decoded = ""; for (let i = 0; i < rawStr.length; i++) { let origByte = rawStr.charCodeAt(i) ^ keyBytes[i % keyBytes.length]; decoded += String.fromCharCode(origByte); } return decoded; } // src/index.ts var Obscura = class { constructor(passphrase) { this.passphrase = passphrase, this.idb = new IDBStore("obscura", "obscura"), console.log("[DEBUG] constructor passphrase:", this.passphrase); } async init() { if (console.log("[DEBUG:init] deriving keyHash"), this.keyHash = await deriveKey(this.passphrase), console.log("[DEBUG:init] keyHash bytes:", this.keyHash), console.log("[DEBUG:init] generating substitution map"), this.map = shuffle(this.keyHash), console.log( "[DEBUG:init] map.length =", this.map.length, " map:", this.map ), this.map.length !== 26) throw new Error( `[DEBUG:init] invalid map length ${this.map.length}, expected 26` ); console.log("[DEBUG:init] building inverseMap"); let alphabet = "abcdefghijklmnopqrstuvwxyz"; this.inverseMap = {}; for (let i = 0; i < 26; i++) this.inverseMap[this.map[i]] = alphabet[i]; console.log("[DEBUG:init] inverseMap:", this.inverseMap), console.log("[DEBUG:init] initMaster() "), this.master = await initMaster(), console.log("[DEBUG:init] master UUID:", this.master), console.log("[DEBUG:init] init complete."); } static genPassphrase(len = 32) { let bytes = crypto.getRandomValues(new Uint8Array(len)), pass = Array.from( bytes, (b) => String.fromCharCode(48 + b % 75) ).join(""); return console.log("[DEBUG] genPassphrase:", pass), pass; } async encode(input) { console.log(` [DEBUG:encode] input:`, input); let pct = encodeURIComponent(input); console.log("[DEBUG:encode] URI-encoded:", pct); let sub = apply(pct, this.map); console.log("[DEBUG:encode] after substitution:", sub); let b64 = btoa(sub); console.log("[DEBUG:encode] base64:", b64); let hashBytes = await sha256(new TextEncoder().encode(b64)); console.log("[DEBUG:encode] hashBytes:", hashBytes); let hashStr = Array.from(hashBytes).map((b) => String.fromCharCode(b)).join(""); console.log("[DEBUG:encode] hashStr:", JSON.stringify(hashStr)); let xorBlob = xorEncode( new TextEncoder().encode(hashStr), this.passphrase ); console.log("[DEBUG:encode] xorBlob:", xorBlob); let nsBytes = await namespace(this.keyHash, this.master); console.log("[DEBUG:encode] nsBytes:", nsBytes); let finalUuid = convertFinal(xorBlob, nsBytes); console.log("[DEBUG:encode] finalUuid:", finalUuid); try { await this.idb.setItem(finalUuid, b64), console.log("[DEBUG:encode] stored b64 under key"); } catch (e) { console.warn("[DEBUG:encode] failed to store b64:", e); } return finalUuid; } async decode(finalUuid) { console.log(` [DEBUG:decode] finalUuid:`, finalUuid); let stored = await this.idb.getItem(finalUuid); if (console.log("[DEBUG:decode] idb.getItem:", stored), stored) { console.log("[DEBUG:decode] fast-path using stored base64"); let sub2 = atob(stored); console.log("[DEBUG:decode] atob ", sub2); let pct2 = sub2.split("").map((ch) => this.inverseMap[ch] ?? ch).join(""); console.log("[DEBUG:decode] after inverseMap ", pct2); let original2 = decodeURIComponent(pct2); return console.log("[DEBUG:decode] decodeURIComponent ", original2), original2; } console.log("[DEBUG:decode] no stored base64, doing UUIDXOR inversion"); let nsBytes = await namespace(this.keyHash, this.master); console.log("[DEBUG:decode] nsBytes:", nsBytes); let uuidBytes = uuidToBytes(finalUuid); console.log("[DEBUG:decode] UUID bytes:", uuidBytes); let blobBytes = new Uint8Array(uuidBytes.length); for (let i = 0; i < uuidBytes.length; i++) blobBytes[i] = uuidBytes[i] ^ nsBytes[i]; console.log("[DEBUG:decode] blobBytes:", blobBytes); let blobStr = new TextDecoder().decode(blobBytes); console.log("[DEBUG:decode] blobStr:", JSON.stringify(blobStr)); let rehashStr = xorDecode(blobStr, this.passphrase); console.log("[DEBUG:decode] rehashStr:", JSON.stringify(rehashStr)); let rehashBytes = new Uint8Array( Array.from(rehashStr).map((ch) => ch.charCodeAt(0)) ); console.log("[DEBUG:decode] rehashBytes:", rehashBytes); let b64 = new TextDecoder().decode(rehashBytes); console.log("[DEBUG:decode] recovered base64:", b64); let sub = atob(b64); console.log("[DEBUG:decode] atob ", sub); let pct = sub.split("").map((ch) => this.inverseMap[ch] ?? ch).join(""); console.log("[DEBUG:decode] after inverseMap ", pct); let original = decodeURIComponent(pct); return console.log("[DEBUG:decode] decodeURIComponent ", original), original; } }, index_default = Obscura; export { Obscura, index_default as default };