187 lines
4.3 KiB
TypeScript
187 lines
4.3 KiB
TypeScript
import { base32, base64url } from "rfc4648";
|
|
import { bufferToArrayBuffer, hexStringToTypedArray } from "./misc";
|
|
|
|
const DEFAULT_ITER = 20000;
|
|
|
|
// base32.stringify(Buffer.from('Salted__'))
|
|
export const MAGIC_ENCRYPTED_PREFIX_BASE32 = "KNQWY5DFMRPV";
|
|
// base64.stringify(Buffer.from('Salted__'))
|
|
export const MAGIC_ENCRYPTED_PREFIX_BASE64URL = "U2FsdGVkX";
|
|
|
|
const getKeyIVFromPassword = async (
|
|
salt: Uint8Array,
|
|
password: string,
|
|
rounds: number = DEFAULT_ITER
|
|
) => {
|
|
const k1 = await window.crypto.subtle.importKey(
|
|
"raw",
|
|
new TextEncoder().encode(password),
|
|
{ name: "PBKDF2" },
|
|
false,
|
|
["deriveKey", "deriveBits"]
|
|
);
|
|
|
|
const k2 = await window.crypto.subtle.deriveBits(
|
|
{
|
|
name: "PBKDF2",
|
|
salt: salt,
|
|
iterations: rounds,
|
|
hash: "SHA-256",
|
|
},
|
|
k1,
|
|
256 + 128
|
|
);
|
|
|
|
return k2;
|
|
};
|
|
|
|
export const encryptArrayBuffer = async (
|
|
arrBuf: ArrayBuffer,
|
|
password: string,
|
|
rounds: number = DEFAULT_ITER,
|
|
saltHex = ""
|
|
) => {
|
|
let salt: Uint8Array;
|
|
if (saltHex !== "") {
|
|
salt = hexStringToTypedArray(saltHex);
|
|
} else {
|
|
salt = window.crypto.getRandomValues(new Uint8Array(8));
|
|
}
|
|
|
|
const derivedKey = await getKeyIVFromPassword(salt, password, rounds);
|
|
const key = derivedKey.slice(0, 32);
|
|
const iv = derivedKey.slice(32, 32 + 16);
|
|
|
|
const keyCrypt = await window.crypto.subtle.importKey(
|
|
"raw",
|
|
key,
|
|
{ name: "AES-CBC" },
|
|
false,
|
|
["encrypt", "decrypt"]
|
|
);
|
|
|
|
const enc = (await window.crypto.subtle.encrypt(
|
|
{ name: "AES-CBC", iv },
|
|
keyCrypt,
|
|
arrBuf
|
|
)) as ArrayBuffer;
|
|
|
|
const prefix = new TextEncoder().encode("Salted__");
|
|
|
|
const res = new Uint8Array([...prefix, ...salt, ...new Uint8Array(enc)]);
|
|
|
|
return bufferToArrayBuffer(res);
|
|
};
|
|
|
|
export const decryptArrayBuffer = async (
|
|
arrBuf: ArrayBuffer,
|
|
password: string,
|
|
rounds: number = DEFAULT_ITER
|
|
) => {
|
|
const prefix = arrBuf.slice(0, 8);
|
|
const salt = arrBuf.slice(8, 16);
|
|
const derivedKey = await getKeyIVFromPassword(
|
|
new Uint8Array(salt),
|
|
password,
|
|
rounds
|
|
);
|
|
const key = derivedKey.slice(0, 32);
|
|
const iv = derivedKey.slice(32, 32 + 16);
|
|
|
|
const keyCrypt = await window.crypto.subtle.importKey(
|
|
"raw",
|
|
key,
|
|
{ name: "AES-CBC" },
|
|
false,
|
|
["encrypt", "decrypt"]
|
|
);
|
|
|
|
const dec = (await window.crypto.subtle.decrypt(
|
|
{ name: "AES-CBC", iv },
|
|
keyCrypt,
|
|
arrBuf.slice(16)
|
|
)) as ArrayBuffer;
|
|
|
|
return dec;
|
|
};
|
|
|
|
export const encryptStringToBase32 = async (
|
|
text: string,
|
|
password: string,
|
|
rounds: number = DEFAULT_ITER,
|
|
saltHex = ""
|
|
) => {
|
|
const enc = await encryptArrayBuffer(
|
|
bufferToArrayBuffer(new TextEncoder().encode(text)),
|
|
password,
|
|
rounds,
|
|
saltHex
|
|
);
|
|
return base32.stringify(new Uint8Array(enc), { pad: false });
|
|
};
|
|
|
|
export const decryptBase32ToString = async (
|
|
text: string,
|
|
password: string,
|
|
rounds: number = DEFAULT_ITER
|
|
) => {
|
|
return new TextDecoder().decode(
|
|
await decryptArrayBuffer(
|
|
bufferToArrayBuffer(base32.parse(text, { loose: true })),
|
|
password,
|
|
rounds
|
|
)
|
|
);
|
|
};
|
|
|
|
export const encryptStringToBase64url = async (
|
|
text: string,
|
|
password: string,
|
|
rounds: number = DEFAULT_ITER,
|
|
saltHex = ""
|
|
) => {
|
|
const enc = await encryptArrayBuffer(
|
|
bufferToArrayBuffer(new TextEncoder().encode(text)),
|
|
password,
|
|
rounds,
|
|
saltHex
|
|
);
|
|
return base64url.stringify(new Uint8Array(enc), { pad: false });
|
|
};
|
|
|
|
export const decryptBase64urlToString = async (
|
|
text: string,
|
|
password: string,
|
|
rounds: number = DEFAULT_ITER
|
|
) => {
|
|
return new TextDecoder().decode(
|
|
await decryptArrayBuffer(
|
|
bufferToArrayBuffer(base64url.parse(text, { loose: true })),
|
|
password,
|
|
rounds
|
|
)
|
|
);
|
|
};
|
|
|
|
export const getSizeFromOrigToEnc = (x: number) => {
|
|
if (x < 0 || Number.isNaN(x) || !Number.isInteger(x)) {
|
|
throw Error(`getSizeFromOrigToEnc: x=${x} is not a valid size`);
|
|
}
|
|
return (Math.floor(x / 16) + 1) * 16 + 16;
|
|
};
|
|
|
|
export const getSizeFromEncToOrig = (x: number) => {
|
|
if (x < 32 || Number.isNaN(x) || !Number.isInteger(x)) {
|
|
throw Error(`getSizeFromEncToOrig: ${x} is not a valid size`);
|
|
}
|
|
if (x % 16 !== 0) {
|
|
throw Error(
|
|
`getSizeFromEncToOrig: ${x} is not a valid encrypted file size`
|
|
);
|
|
}
|
|
return {
|
|
minSize: ((x - 16) / 16 - 1) * 16,
|
|
maxSize: ((x - 16) / 16 - 1) * 16 + 15,
|
|
};
|
|
};
|