Init
This commit is contained in:
@@ -0,0 +1,199 @@
|
||||
(() => {
|
||||
const te = new TextEncoder();
|
||||
const td = new TextDecoder();
|
||||
|
||||
function b64encode(buf) {
|
||||
const bytes = buf instanceof ArrayBuffer ? new Uint8Array(buf) : buf;
|
||||
let s = "";
|
||||
for (let i = 0; i < bytes.length; i += 0x8000) {
|
||||
s += String.fromCharCode(...bytes.subarray(i, i + 0x8000));
|
||||
}
|
||||
return btoa(s);
|
||||
}
|
||||
|
||||
function b64decode(str) {
|
||||
const bin = atob(str);
|
||||
const out = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
function b64urlEncode(buf) {
|
||||
return b64encode(buf).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/g, "");
|
||||
}
|
||||
|
||||
function b64urlDecode(str) {
|
||||
const pad = "=".repeat((4 - (str.length % 4)) % 4);
|
||||
return b64decode(str.replace(/-/g, "+").replace(/_/g, "/") + pad);
|
||||
}
|
||||
|
||||
async function importAesKey(raw) {
|
||||
return crypto.subtle.importKey("raw", raw, { name: "AES-GCM" }, false, [
|
||||
"encrypt",
|
||||
"decrypt",
|
||||
]);
|
||||
}
|
||||
|
||||
async function derivePasswordKey(password, salt) {
|
||||
const base = await crypto.subtle.importKey(
|
||||
"raw",
|
||||
te.encode(password),
|
||||
"PBKDF2",
|
||||
false,
|
||||
["deriveKey"]
|
||||
);
|
||||
return crypto.subtle.deriveKey(
|
||||
{ name: "PBKDF2", salt, iterations: 210000, hash: "SHA-256" },
|
||||
base,
|
||||
{ name: "AES-GCM", length: 256 },
|
||||
false,
|
||||
["encrypt", "decrypt"]
|
||||
);
|
||||
}
|
||||
|
||||
async function encryptPackage(packageObj, password) {
|
||||
const dek = crypto.getRandomValues(new Uint8Array(32));
|
||||
const payload = te.encode(JSON.stringify(packageObj));
|
||||
const nonce = crypto.getRandomValues(new Uint8Array(12));
|
||||
const key = await importAesKey(dek);
|
||||
const encrypted = new Uint8Array(
|
||||
await crypto.subtle.encrypt({ name: "AES-GCM", iv: nonce }, key, payload)
|
||||
);
|
||||
|
||||
let body = encrypted;
|
||||
let bodyNonce = nonce;
|
||||
let flags = 0;
|
||||
let salt = new Uint8Array(16);
|
||||
let outerNonce = new Uint8Array(12);
|
||||
|
||||
if (password) {
|
||||
flags = 1;
|
||||
salt = crypto.getRandomValues(new Uint8Array(16));
|
||||
outerNonce = crypto.getRandomValues(new Uint8Array(12));
|
||||
const pwKey = await derivePasswordKey(password, salt);
|
||||
const inner = new Uint8Array(nonce.length + encrypted.length);
|
||||
inner.set(nonce, 0);
|
||||
inner.set(encrypted, nonce.length);
|
||||
body = new Uint8Array(
|
||||
await crypto.subtle.encrypt({ name: "AES-GCM", iv: outerNonce }, pwKey, inner)
|
||||
);
|
||||
bodyNonce = outerNonce;
|
||||
}
|
||||
|
||||
// header: magic(4) version(1) flags(1) salt(16) nonce(12) + ciphertext
|
||||
const header = new Uint8Array(4 + 1 + 1 + 16 + 12);
|
||||
header.set(te.encode("WRPD"), 0);
|
||||
header[4] = 1;
|
||||
header[5] = flags;
|
||||
header.set(salt, 6);
|
||||
header.set(bodyNonce, 22);
|
||||
const out = new Uint8Array(header.length + body.length);
|
||||
out.set(header, 0);
|
||||
out.set(body, header.length);
|
||||
|
||||
return {
|
||||
ciphertext: out,
|
||||
keyB64url: b64urlEncode(dek),
|
||||
};
|
||||
}
|
||||
|
||||
async function decryptPackage(ciphertext, keyB64url, password) {
|
||||
if (ciphertext.length < 34) throw new Error("truncated");
|
||||
const magic = td.decode(ciphertext.subarray(0, 4));
|
||||
if (magic !== "WRPD") throw new Error("bad_magic");
|
||||
const flags = ciphertext[5];
|
||||
const salt = ciphertext.subarray(6, 22);
|
||||
const nonce = ciphertext.subarray(22, 34);
|
||||
const body = ciphertext.subarray(34);
|
||||
const dek = b64urlDecode(keyB64url);
|
||||
|
||||
let plainEncrypted;
|
||||
let innerNonce;
|
||||
|
||||
if (flags & 1) {
|
||||
if (!password) throw new Error("password_required");
|
||||
const pwKey = await derivePasswordKey(password, salt);
|
||||
let inner;
|
||||
try {
|
||||
inner = new Uint8Array(
|
||||
await crypto.subtle.decrypt({ name: "AES-GCM", iv: nonce }, pwKey, body)
|
||||
);
|
||||
} catch {
|
||||
throw new Error("bad_password");
|
||||
}
|
||||
innerNonce = inner.subarray(0, 12);
|
||||
plainEncrypted = inner.subarray(12);
|
||||
} else {
|
||||
innerNonce = nonce;
|
||||
plainEncrypted = body;
|
||||
}
|
||||
|
||||
const key = await importAesKey(dek);
|
||||
let payload;
|
||||
try {
|
||||
payload = new Uint8Array(
|
||||
await crypto.subtle.decrypt({ name: "AES-GCM", iv: innerNonce }, key, plainEncrypted)
|
||||
);
|
||||
} catch {
|
||||
throw new Error("decrypt_failed");
|
||||
}
|
||||
return JSON.parse(td.decode(payload));
|
||||
}
|
||||
|
||||
function buildToken(wrapId, keyB64url) {
|
||||
return `wrapped_v1.${wrapId}.${keyB64url}`;
|
||||
}
|
||||
|
||||
function parseToken(input) {
|
||||
const raw = (input || "").trim();
|
||||
if (!raw) return null;
|
||||
if (raw.startsWith("wrapped_v1.")) {
|
||||
const parts = raw.split(".");
|
||||
if (parts.length < 3) return null;
|
||||
return { wrapId: parts[1], key: parts.slice(2).join(".") };
|
||||
}
|
||||
// bare id — key may come from location.hash
|
||||
if (/^[A-Za-z0-9]+$/.test(raw) && raw.length >= 16) {
|
||||
return { wrapId: raw, key: null };
|
||||
}
|
||||
try {
|
||||
const url = new URL(raw);
|
||||
const m = url.pathname.match(/\/w\/([A-Za-z0-9]+)/);
|
||||
if (m) {
|
||||
return { wrapId: m[1], key: url.hash ? url.hash.slice(1) : null };
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function bytesToBase64(bytes) {
|
||||
return b64encode(bytes);
|
||||
}
|
||||
|
||||
function base64ToBytes(b64) {
|
||||
return b64decode(b64);
|
||||
}
|
||||
|
||||
async function fileToItem(file) {
|
||||
const buf = new Uint8Array(await file.arrayBuffer());
|
||||
return {
|
||||
type: "file",
|
||||
name: file.name || "file",
|
||||
mime: file.type || "application/octet-stream",
|
||||
data_b64: b64encode(buf),
|
||||
};
|
||||
}
|
||||
|
||||
window.WrappedCrypto = {
|
||||
encryptPackage,
|
||||
decryptPackage,
|
||||
buildToken,
|
||||
parseToken,
|
||||
bytesToBase64,
|
||||
base64ToBytes,
|
||||
fileToItem,
|
||||
b64urlEncode,
|
||||
};
|
||||
})();
|
||||
Reference in New Issue
Block a user