@nightnetwork/obscura
Version:
The next-level encoding tool.
337 lines (320 loc) • 14.7 kB
JavaScript
// 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
};