one
Version:
One is a React framework for web, iOS, and Android apps.
237 lines (234 loc) • 8.42 kB
JavaScript
import { base64ToBytes, bytesToBase64, bytesToHex, concatBytes, exactBytes, hexToBytes } from "./bytes.mjs";
const KEY_SIZES = [
128,
192,
256
];
const TAG_LENGTHS = [
16,
15,
14,
13,
12,
8,
4
];
const DEFAULT_IV_LENGTH = 12;
const DEFAULT_TAG_LENGTH = 16;
function assertTagLength(tagLength, verb) {
if (!TAG_LENGTHS.includes(tagLength)) {
throw new Error(`${verb}: tagLength must be one of: ${TAG_LENGTHS.join(", ")}`);
}
}
function toBytes(input, verb) {
if (typeof input === "string") return base64ToBytes(input, verb);
if (input instanceof ArrayBuffer) return new Uint8Array(input.slice(0));
if (ArrayBuffer.isView(input) && input.buffer instanceof ArrayBuffer) {
return exactBytes(new Uint8Array(input.buffer, input.byteOffset, input.byteLength));
}
throw new TypeError(`${verb}: expected a base64 string, Uint8Array, or ArrayBuffer`);
}
const sealedParts = /* @__PURE__ */ new WeakMap();
const keyBytes = /* @__PURE__ */ new WeakMap();
function encodeParts(bytes, encoding) {
return encoding === "base64" ? bytesToBase64(bytes) : exactBytes(bytes);
}
function createAesApi(engine) {
class AESEncryptionKey {
size;
constructor(bytes) {
if (!KEY_SIZES.includes(bytes.length * 8)) {
throw new Error(`Crypto.AESEncryptionKey: key must be 128, 192, or 256 bits, got ${bytes.length * 8}`);
}
this.size = bytes.length * 8;
keyBytes.set(this, exactBytes(bytes));
}
static async generate(size = 256) {
if (!KEY_SIZES.includes(size)) {
throw new Error(`Crypto.AESEncryptionKey.generate: size must be one of: ${KEY_SIZES.join(", ")}`);
}
const bytes = new Uint8Array(size / 8);
engine.random(bytes);
return new AESEncryptionKey(bytes);
}
static async import(input, encoding) {
const verb = "Crypto.AESEncryptionKey.import";
let bytes;
if (typeof input === "string") {
if (encoding === "hex") bytes = hexToBytes(input, verb);
else if (encoding === "base64") bytes = base64ToBytes(input, verb);
else throw new Error(`${verb}: encoding must be 'hex' or 'base64'`);
} else if (input instanceof Uint8Array) {
bytes = exactBytes(input);
} else {
throw new TypeError(`${verb}: expected bytes or an encoded string`);
}
if (!KEY_SIZES.includes(bytes.length * 8)) {
throw new Error(`${verb}: key must be 128, 192, or 256 bits, got ${bytes.length * 8}`);
}
return new AESEncryptionKey(bytes);
}
async bytes() {
return exactBytes(keyBytes.get(this));
}
async encoded(encoding) {
const bytes = keyBytes.get(this);
if (encoding === "hex") return bytesToHex(bytes);
if (encoding === "base64") return bytesToBase64(bytes);
throw new Error(`Crypto.AESEncryptionKey.encoded: encoding must be 'hex' or 'base64'`);
}
}
class AESSealedData {
constructor(parts) {
if (parts.iv.length === 0) throw new Error("Crypto.AESSealedData: iv must not be empty");
assertTagLength(parts.tag.length, "Crypto.AESSealedData");
sealedParts.set(this, parts);
}
static fromCombined(combined, config = {}) {
const verb = "Crypto.AESSealedData.fromCombined";
const ivLength = config.ivLength ?? DEFAULT_IV_LENGTH;
const tagLength = config.tagLength ?? DEFAULT_TAG_LENGTH;
assertTagLength(tagLength, verb);
if (!Number.isInteger(ivLength) || ivLength <= 0) {
throw new Error(`${verb}: ivLength must be a positive integer`);
}
const bytes = toBytes(combined, verb);
if (bytes.length < ivLength + tagLength) {
throw new Error(`${verb}: combined data is shorter than its iv and tag`);
}
return new AESSealedData({
iv: exactBytes(bytes.subarray(0, ivLength)),
ciphertext: exactBytes(bytes.subarray(ivLength, bytes.length - tagLength)),
tag: exactBytes(bytes.subarray(bytes.length - tagLength))
});
}
static fromParts(ivInput, ciphertextInput, tagOrLength) {
const verb = "Crypto.AESSealedData.fromParts";
const iv = toBytes(ivInput, verb);
const ciphertext = toBytes(ciphertextInput, verb);
if (tagOrLength === void 0 || typeof tagOrLength === "number") {
const tagLength = tagOrLength ?? DEFAULT_TAG_LENGTH;
assertTagLength(tagLength, verb);
if (ciphertext.length < tagLength) {
throw new Error(`${verb}: ciphertext is shorter than its tag`);
}
return new AESSealedData({
iv,
ciphertext: exactBytes(ciphertext.subarray(0, ciphertext.length - tagLength)),
tag: exactBytes(ciphertext.subarray(ciphertext.length - tagLength))
});
}
const tag = toBytes(tagOrLength, verb);
assertTagLength(tag.length, verb);
return new AESSealedData({
iv,
ciphertext,
tag
});
}
static fromSeal(iv, ciphertextWithTag, tagLength) {
return new AESSealedData({
iv,
ciphertext: exactBytes(ciphertextWithTag.subarray(0, ciphertextWithTag.length - tagLength)),
tag: exactBytes(ciphertextWithTag.subarray(ciphertextWithTag.length - tagLength))
});
}
get ivSize() {
return sealedParts.get(this).iv.length;
}
get tagSize() {
return sealedParts.get(this).tag.length;
}
get combinedSize() {
const parts = sealedParts.get(this);
return parts.iv.length + parts.ciphertext.length + parts.tag.length;
}
async iv(encoding) {
return encodeParts(sealedParts.get(this).iv, encoding);
}
async tag(encoding) {
return encodeParts(sealedParts.get(this).tag, encoding);
}
async ciphertext(options) {
const parts = sealedParts.get(this);
const bytes = options?.includeTag ? concatBytes([parts.ciphertext, parts.tag]) : parts.ciphertext;
return encodeParts(bytes, options?.encoding);
}
async combined(encoding) {
const parts = sealedParts.get(this);
return encodeParts(concatBytes([
parts.iv,
parts.ciphertext,
parts.tag
]), encoding);
}
}
async function keyMaterial(key, verb) {
const own = keyBytes.get(key);
if (own) return exactBytes(own);
if (key && typeof key.bytes === "function") return exactBytes(await key.bytes());
throw new TypeError(`${verb}: expected an AESEncryptionKey`);
}
async function partsOf(sealed, verb) {
const own = sealedParts.get(sealed);
if (own) {
return {
iv: exactBytes(own.iv),
ciphertext: exactBytes(own.ciphertext),
tag: exactBytes(own.tag)
};
}
if (sealed && typeof sealed.iv === "function" && typeof sealed.combined === "function") {
const iv = exactBytes(await sealed.iv());
const tag = exactBytes(await sealed.tag());
const ciphertext = exactBytes(await sealed.ciphertext({
encoding: "bytes",
includeTag: false
}));
return {
iv,
ciphertext,
tag
};
}
throw new TypeError(`${verb}: expected an AESSealedData`);
}
async function aesEncrypt(plaintext, key, options = {}) {
const verb = "Crypto.aesEncrypt";
const tagLength = options.tagLength ?? DEFAULT_TAG_LENGTH;
assertTagLength(tagLength, verb);
let iv;
const nonce = options.nonce;
if (nonce && "bytes" in nonce && nonce.bytes !== void 0) {
iv = toBytes(nonce.bytes, verb);
if (iv.length === 0) throw new Error(`${verb}: nonce bytes must not be empty`);
} else {
const length = nonce && "length" in nonce && nonce.length !== void 0 ? nonce.length : DEFAULT_IV_LENGTH;
if (!Number.isInteger(length) || length <= 0) {
throw new Error(`${verb}: nonce length must be a positive integer`);
}
iv = new Uint8Array(length);
engine.random(iv);
}
const additionalData = options.additionalData === void 0 ? void 0 : toBytes(options.additionalData, verb);
const plainBytes = toBytes(plaintext, verb);
const sealed = await engine.seal(await keyMaterial(key, verb), iv, plainBytes, additionalData, tagLength);
const actualTag = sealed.length - plainBytes.length;
return AESSealedData.fromSeal(iv, sealed, actualTag > 0 ? actualTag : tagLength);
}
async function aesDecrypt(sealedData, key, options = {}) {
const verb = "Crypto.aesDecrypt";
const parts = await partsOf(sealedData, verb);
const additionalData = options.additionalData === void 0 ? void 0 : toBytes(options.additionalData, verb);
const plaintext = await engine.open(await keyMaterial(key, verb), parts.iv, concatBytes([parts.ciphertext, parts.tag]), additionalData, parts.tag.length);
return options.output === "base64" ? bytesToBase64(plaintext) : plaintext;
}
return {
AESEncryptionKey,
AESSealedData,
aesEncrypt,
aesDecrypt
};
}
export { assertTagLength, createAesApi, toBytes };
//# sourceMappingURL=aes.mjs.map