Skip to content

Commit

Permalink
chore: release v0.10.0
Browse files Browse the repository at this point in the history
  • Loading branch information
brc-dd committed Sep 1, 2023
1 parent 8511ec4 commit 7de5807
Show file tree
Hide file tree
Showing 7 changed files with 1,065 additions and 1 deletion.
2 changes: 2 additions & 0 deletions CHANGELOG.md
@@ -1,5 +1,7 @@


## [0.10.0](https://github.com/brc-dd/iron-webcrypto/compare/v0.9.0...v0.10.0) (2023-09-01)

## [0.9.0](https://github.com/brc-dd/iron-webcrypto/compare/v0.8.2...v0.9.0) (2023-09-01)


Expand Down
19 changes: 19 additions & 0 deletions dist/example.js
@@ -0,0 +1,19 @@
const { webcrypto } = require('node:crypto')
const Iron = require('iron-webcrypto')

const obj = {
a: 1,
b: 2,
c: [3, 4, 5],
d: {
e: 'f'
}
}

const password = 'a_password_having_at_least_32_chars'

const sealed = await Iron.seal(webcrypto, obj, password, Iron.defaults)
console.log({ sealed })

const unsealed = await Iron.unseal(webcrypto, sealed, password, Iron.defaults)
console.log({ unsealed })
320 changes: 320 additions & 0 deletions dist/index.cjs
@@ -0,0 +1,320 @@
'use strict';

// node_modules/.pnpm/@[email protected]/node_modules/@smithy/util-base64/dist-es/constants.browser.js
var alphabetByEncoding = {};
var alphabetByValue = new Array(64);
for (let i = 0, start = "A".charCodeAt(0), limit = "Z".charCodeAt(0); i + start <= limit; i++) {
const char = String.fromCharCode(i + start);
alphabetByEncoding[char] = i;
alphabetByValue[i] = char;
}
for (let i = 0, start = "a".charCodeAt(0), limit = "z".charCodeAt(0); i + start <= limit; i++) {
const char = String.fromCharCode(i + start);
const index = i + 26;
alphabetByEncoding[char] = index;
alphabetByValue[index] = char;
}
for (let i = 0; i < 10; i++) {
alphabetByEncoding[i.toString(10)] = i + 52;
const char = i.toString(10);
const index = i + 52;
alphabetByEncoding[char] = index;
alphabetByValue[index] = char;
}
alphabetByEncoding["+"] = 62;
alphabetByValue[62] = "+";
alphabetByEncoding["/"] = 63;
alphabetByValue[63] = "/";
var bitsPerLetter = 6;
var bitsPerByte = 8;
var maxLetterValue = 63;

// node_modules/.pnpm/@[email protected]/node_modules/@smithy/util-base64/dist-es/fromBase64.browser.js
var fromBase64 = (input) => {
let totalByteLength = input.length / 4 * 3;
if (input.slice(-2) === "==") {
totalByteLength -= 2;
} else if (input.slice(-1) === "=") {
totalByteLength--;
}
const out = new ArrayBuffer(totalByteLength);
const dataView = new DataView(out);
for (let i = 0; i < input.length; i += 4) {
let bits = 0;
let bitLength = 0;
for (let j = i, limit = i + 3; j <= limit; j++) {
if (input[j] !== "=") {
if (!(input[j] in alphabetByEncoding)) {
throw new TypeError(`Invalid character ${input[j]} in base64 string.`);
}
bits |= alphabetByEncoding[input[j]] << (limit - j) * bitsPerLetter;
bitLength += bitsPerLetter;
} else {
bits >>= bitsPerLetter;
}
}
const chunkOffset = i / 4 * 3;
bits >>= bitLength % bitsPerByte;
const byteLength = Math.floor(bitLength / bitsPerByte);
for (let k = 0; k < byteLength; k++) {
const offset = (byteLength - k - 1) * bitsPerByte;
dataView.setUint8(chunkOffset + k, (bits & 255 << offset) >> offset);
}
}
return new Uint8Array(out);
};

// node_modules/.pnpm/@[email protected]/node_modules/@smithy/util-base64/dist-es/toBase64.browser.js
function toBase64(input) {
let str = "";
for (let i = 0; i < input.length; i += 3) {
let bits = 0;
let bitLength = 0;
for (let j = i, limit = Math.min(i + 3, input.length); j < limit; j++) {
bits |= input[j] << (limit - j - 1) * bitsPerByte;
bitLength += bitsPerByte;
}
const bitClusterCount = Math.ceil(bitLength / bitsPerLetter);
bits <<= bitClusterCount * bitsPerLetter - bitLength;
for (let k = 1; k <= bitClusterCount; k++) {
const offset = (bitClusterCount - k) * bitsPerLetter;
str += alphabetByValue[(bits & maxLetterValue << offset) >> offset];
}
str += "==".slice(0, 4 - bitClusterCount);
}
return str;
}

// src/index.ts
var stringToBuffer = (value) => {
return new TextEncoder().encode(value);
};
var bufferToString = (value) => {
return new TextDecoder().decode(value);
};
var base64urlEncode = (value) => toBase64(typeof value === "string" ? stringToBuffer(value) : value).replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "");
var base64urlDecode = (value) => fromBase64(
value.replace(/-/g, "+").replace(/_/g, "/") + Array((4 - value.length % 4) % 4 + 1).join("=")
);
var defaults = {
encryption: { saltBits: 256, algorithm: "aes-256-cbc", iterations: 1, minPasswordlength: 32 },
integrity: { saltBits: 256, algorithm: "sha256", iterations: 1, minPasswordlength: 32 },
ttl: 0,
timestampSkewSec: 60,
localtimeOffsetMsec: 0
};
var clone = (options) => ({
...options,
encryption: { ...options.encryption },
integrity: { ...options.integrity }
});
var algorithms = {
"aes-128-ctr": { keyBits: 128, ivBits: 128, name: "AES-CTR" },
"aes-256-cbc": { keyBits: 256, ivBits: 128, name: "AES-CBC" },
sha256: { keyBits: 256, name: "SHA-256" }
};
var macFormatVersion = "2";
var macPrefix = `Fe26.${macFormatVersion}`;
var randomBytes = (_crypto, size) => {
const bytes = new Uint8Array(size);
_crypto.getRandomValues(bytes);
return bytes;
};
var randomBits = (_crypto, bits) => {
if (bits < 1)
throw Error("Invalid random bits count");
const bytes = Math.ceil(bits / 8);
return randomBytes(_crypto, bytes);
};
var pbkdf2 = async (_crypto, password, salt, iterations, keyLength, hash) => {
const passwordBuffer = stringToBuffer(password);
const importedKey = await _crypto.subtle.importKey("raw", passwordBuffer, "PBKDF2", false, [
"deriveBits"
]);
const saltBuffer = stringToBuffer(salt);
const params = { name: "PBKDF2", hash, salt: saltBuffer, iterations };
const derivation = await _crypto.subtle.deriveBits(params, importedKey, keyLength * 8);
return derivation;
};
var generateKey = async (_crypto, password, options) => {
if (!password?.length)
throw new Error("Empty password");
if (options == null || typeof options !== "object")
throw new Error("Bad options");
if (!(options.algorithm in algorithms))
throw new Error(`Unknown algorithm: ${options.algorithm}`);
const algorithm = algorithms[options.algorithm];
const result = {};
const hmac = options.hmac ?? false;
const id = hmac ? { name: "HMAC", hash: algorithm.name } : { name: algorithm.name };
const usage = hmac ? ["sign", "verify"] : ["encrypt", "decrypt"];
if (typeof password === "string") {
if (password.length < options.minPasswordlength)
throw new Error(
`Password string too short (min ${options.minPasswordlength} characters required)`
);
let { salt = "" } = options;
if (!salt) {
const { saltBits = 0 } = options;
if (!saltBits)
throw new Error("Missing salt and saltBits options");
const randomSalt = randomBits(_crypto, saltBits);
salt = [...new Uint8Array(randomSalt)].map((x) => x.toString(16).padStart(2, "0")).join("");
}
const derivedKey = await pbkdf2(
_crypto,
password,
salt,
options.iterations,
algorithm.keyBits / 8,
"SHA-1"
);
const importedEncryptionKey = await _crypto.subtle.importKey(
"raw",
derivedKey,
id,
false,
usage
);
result.key = importedEncryptionKey;
result.salt = salt;
} else {
if (password.length < algorithm.keyBits / 8)
throw new Error("Key buffer (password) too small");
result.key = await _crypto.subtle.importKey("raw", password, id, false, usage);
result.salt = "";
}
if (options.iv)
result.iv = options.iv;
else if ("ivBits" in algorithm)
result.iv = randomBits(_crypto, algorithm.ivBits);
return result;
};
var encrypt = async (_crypto, password, options, data) => {
const key = await generateKey(_crypto, password, options);
const textBuffer = stringToBuffer(data);
const encrypted = await _crypto.subtle.encrypt(
{ name: algorithms[options.algorithm].name, iv: key.iv },
key.key,
textBuffer
);
return { encrypted: new Uint8Array(encrypted), key };
};
var decrypt = async (_crypto, password, options, data) => {
const key = await generateKey(_crypto, password, options);
const decrypted = await _crypto.subtle.decrypt(
{ name: algorithms[options.algorithm].name, iv: key.iv },
key.key,
typeof data === "string" ? stringToBuffer(data) : data
);
return bufferToString(new Uint8Array(decrypted));
};
var hmacWithPassword = async (_crypto, password, options, data) => {
const key = await generateKey(_crypto, password, { ...options, hmac: true });
const textBuffer = stringToBuffer(data);
const signed = await _crypto.subtle.sign({ name: "HMAC" }, key.key, textBuffer);
const digest = base64urlEncode(new Uint8Array(signed));
return { digest, salt: key.salt };
};
var normalizePassword = (password) => {
if (typeof password === "string" || password instanceof Uint8Array)
return { encryption: password, integrity: password };
if ("secret" in password)
return { id: password.id, encryption: password.secret, integrity: password.secret };
return { id: password.id, encryption: password.encryption, integrity: password.integrity };
};
var seal = async (_crypto, object, password, options) => {
if (!password)
throw Error("Empty password");
const opts = clone(options);
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
const objectString = JSON.stringify(object);
const pass = normalizePassword(password);
const { id = "" } = pass;
if (id && !/^\w+$/.test(id))
throw new Error("Invalid password id");
const { encrypted, key } = await encrypt(_crypto, pass.encryption, opts.encryption, objectString);
const encryptedB64 = base64urlEncode(new Uint8Array(encrypted));
const iv = base64urlEncode(key.iv);
const expiration = opts.ttl ? now + opts.ttl : "";
const macBaseString = `${macPrefix}*${id}*${key.salt}*${iv}*${encryptedB64}*${expiration}`;
const mac = await hmacWithPassword(_crypto, pass.integrity, opts.integrity, macBaseString);
const sealed = `${macBaseString}*${mac.salt}*${mac.digest}`;
return sealed;
};
var fixedTimeComparison = (a, b) => {
let mismatch = a.length === b.length ? 0 : 1;
if (mismatch)
b = a;
for (let i = 0; i < a.length; i += 1)
mismatch |= a.charCodeAt(i) ^ b.charCodeAt(i);
return mismatch === 0;
};
var unseal = async (_crypto, sealed, password, options) => {
if (!password)
throw Error("Empty password");
const opts = clone(options);
const now = Date.now() + (opts.localtimeOffsetMsec || 0);
const parts = sealed.split("*");
if (parts.length !== 8)
throw new Error("Incorrect number of sealed components");
const prefix = parts[0];
let passwordId = parts[1];
const encryptionSalt = parts[2];
const encryptionIv = parts[3];
const encryptedB64 = parts[4];
const expiration = parts[5];
const hmacSalt = parts[6];
const hmac = parts[7];
const macBaseString = `${prefix}*${passwordId}*${encryptionSalt}*${encryptionIv}*${encryptedB64}*${expiration}`;
if (macPrefix !== prefix)
throw new Error("Wrong mac prefix");
if (expiration) {
if (!/^\d+$/.exec(expiration))
throw new Error("Invalid expiration");
const exp = parseInt(expiration, 10);
if (exp <= now - opts.timestampSkewSec * 1e3)
throw new Error("Expired seal");
}
if (typeof password === "undefined" || typeof password === "string" && password.length === 0)
throw new Error("Empty password");
let pass = "";
passwordId = passwordId || "default";
if (typeof password === "string" || password instanceof Uint8Array)
pass = password;
else if (!(passwordId in password))
throw new Error(`Cannot find password: ${passwordId}`);
else
pass = password[passwordId];
pass = normalizePassword(pass);
const macOptions = opts.integrity;
macOptions.salt = hmacSalt;
const mac = await hmacWithPassword(_crypto, pass.integrity, macOptions, macBaseString);
if (!fixedTimeComparison(mac.digest, hmac))
throw new Error("Bad hmac value");
const encrypted = base64urlDecode(encryptedB64);
const decryptOptions = opts.encryption;
decryptOptions.salt = encryptionSalt;
decryptOptions.iv = base64urlDecode(encryptionIv);
const decrypted = await decrypt(_crypto, pass.encryption, decryptOptions, encrypted);
if (decrypted)
return JSON.parse(decrypted);
return null;
};

exports.algorithms = algorithms;
exports.base64urlDecode = base64urlDecode;
exports.base64urlEncode = base64urlEncode;
exports.bufferToString = bufferToString;
exports.clone = clone;
exports.decrypt = decrypt;
exports.defaults = defaults;
exports.encrypt = encrypt;
exports.generateKey = generateKey;
exports.hmacWithPassword = hmacWithPassword;
exports.macFormatVersion = macFormatVersion;
exports.macPrefix = macPrefix;
exports.randomBits = randomBits;
exports.seal = seal;
exports.stringToBuffer = stringToBuffer;
exports.unseal = unseal;

0 comments on commit 7de5807

Please sign in to comment.