diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c591ac..d89d460 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,7 @@ jobs: run: yarn install - name: Get initial dist checksum - id: initial-checksum + id: initial-dist-checksum run: | if [ -d "dist" ]; then echo "checksum=$(find dist -type f -exec md5sum {} \; | sort -k 2 | md5sum | cut -d' ' -f1)" >> $GITHUB_OUTPUT @@ -83,13 +83,31 @@ jobs: echo "checksum=none" >> $GITHUB_OUTPUT fi + - name: Get initial bin checksum + id: initial-bin-checksum + run: | + if [ -d "bin" ]; then + echo "checksum=$(find bin -type f -exec md5sum {} \; | sort -k 2 | md5sum | cut -d' ' -f1)" >> $GITHUB_OUTPUT + else + echo "checksum=none" >> $GITHUB_OUTPUT + fi + + - name: Get initial preview checksum + id: initial-preview-checksum + run: | + if [ -d "preview" ]; then + echo "checksum=$(find preview -type f -exec md5sum {} \; | sort -k 2 | md5sum | cut -d' ' -f1)" >> $GITHUB_OUTPUT + else + echo "checksum=none" >> $GITHUB_OUTPUT + fi + - name: Build run: yarn build - name: Verify dist checksum run: | NEW_CHECKSUM=$(find dist -type f -exec md5sum {} \; | sort -k 2 | md5sum | cut -d' ' -f1) - INITIAL_CHECKSUM=${{ steps.initial-checksum.outputs.checksum }} + INITIAL_CHECKSUM=${{ steps.initial-dist-checksum.outputs.checksum }} if [ "$INITIAL_CHECKSUM" != "none" ] && [ "$NEW_CHECKSUM" != "$INITIAL_CHECKSUM" ]; then echo "Error: dist folder content changed after build" @@ -98,3 +116,29 @@ jobs: echo "Please run 'yarn build' locally and commit the changes." exit 1 fi + + - name: Verify bin checksum + run: | + NEW_CHECKSUM=$(find bin -type f -exec md5sum {} \; | sort -k 2 | md5sum | cut -d' ' -f1) + INITIAL_CHECKSUM=${{ steps.initial-bin-checksum.outputs.checksum }} + + if [ "$INITIAL_CHECKSUM" != "none" ] && [ "$NEW_CHECKSUM" != "$INITIAL_CHECKSUM" ]; then + echo "Error: bin folder content changed after build" + echo "Initial checksum: $INITIAL_CHECKSUM" + echo "New checksum: $NEW_CHECKSUM" + echo "Please run 'yarn build:cli' locally and commit the changes." + exit 1 + fi + + - name: Verify preview checksum + run: | + NEW_CHECKSUM=$(find preview -type f -exec md5sum {} \; | sort -k 2 | md5sum | cut -d' ' -f1) + INITIAL_CHECKSUM=${{ steps.initial-preview-checksum.outputs.checksum }} + + if [ "$INITIAL_CHECKSUM" != "none" ] && [ "$NEW_CHECKSUM" != "$INITIAL_CHECKSUM" ]; then + echo "Error: preview folder content changed after build" + echo "Initial checksum: $INITIAL_CHECKSUM" + echo "New checksum: $NEW_CHECKSUM" + echo "Please run 'yarn build:preview' locally and commit the changes." + exit 1 + fi diff --git a/bin/letsdecrypt.js b/bin/letsdecrypt.js new file mode 100644 index 0000000..8945abc --- /dev/null +++ b/bin/letsdecrypt.js @@ -0,0 +1,577 @@ +#!/usr/bin/env node +import { Buffer as y } from "node:buffer"; +function oe(e) { + return e && e.__esModule && Object.prototype.hasOwnProperty.call(e, "default") ? e.default : e; +} +var j, X; +function ce() { + if (X) return j; + X = 1; + function e(n, o) { + var a = n; + o.slice(0, -1).forEach(function(s) { + a = a[s] || {}; + }); + var c = o[o.length - 1]; + return c in a; + } + function t(n) { + return typeof n == "number" || /^0x[0-9a-f]+$/i.test(n) ? !0 : /^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(n); + } + function r(n, o) { + return o === "constructor" && typeof n[o] == "function" || o === "__proto__"; + } + return j = function(n, o) { + o || (o = {}); + var a = { + bools: {}, + strings: {}, + unknownFn: null + }; + typeof o.unknown == "function" && (a.unknownFn = o.unknown), typeof o.boolean == "boolean" && o.boolean ? a.allBools = !0 : [].concat(o.boolean).filter(Boolean).forEach(function(i) { + a.bools[i] = !0; + }); + var c = {}; + function s(i) { + return c[i].some(function(d) { + return a.bools[d]; + }); + } + Object.keys(o.alias || {}).forEach(function(i) { + c[i] = [].concat(o.alias[i]), c[i].forEach(function(d) { + c[d] = [i].concat(c[i].filter(function(S) { + return d !== S; + })); + }); + }), [].concat(o.string).filter(Boolean).forEach(function(i) { + a.strings[i] = !0, c[i] && [].concat(c[i]).forEach(function(d) { + a.strings[d] = !0; + }); + }); + var m = o.default || {}, l = { _: [] }; + function C(i, d) { + return a.allBools && /^--[^=]+$/.test(d) || a.strings[i] || a.bools[i] || c[i]; + } + function R(i, d, S) { + for (var p = i, N = 0; N < d.length - 1; N++) { + var g = d[N]; + if (r(p, g)) + return; + p[g] === void 0 && (p[g] = {}), (p[g] === Object.prototype || p[g] === Number.prototype || p[g] === String.prototype) && (p[g] = {}), p[g] === Array.prototype && (p[g] = []), p = p[g]; + } + var P = d[d.length - 1]; + r(p, P) || ((p === Object.prototype || p === Number.prototype || p === String.prototype) && (p = {}), p === Array.prototype && (p = []), p[P] === void 0 || a.bools[P] || typeof p[P] == "boolean" ? p[P] = S : Array.isArray(p[P]) ? p[P].push(S) : p[P] = [p[P], S]); + } + function b(i, d, S) { + if (!(S && a.unknownFn && !C(i, S) && a.unknownFn(S) === !1)) { + var p = !a.strings[i] && t(d) ? Number(d) : d; + R(l, i.split("."), p), (c[i] || []).forEach(function(N) { + R(l, N.split("."), p); + }); + } + } + Object.keys(a.bools).forEach(function(i) { + b(i, m[i] === void 0 ? !1 : m[i]); + }); + var J = []; + n.indexOf("--") !== -1 && (J = n.slice(n.indexOf("--") + 1), n = n.slice(0, n.indexOf("--"))); + for (var w = 0; w < n.length; w++) { + var u = n[w], f, h; + if (/^--.+=/.test(u)) { + var Q = u.match(/^--([^=]+)=([\s\S]*)$/); + f = Q[1]; + var $ = Q[2]; + a.bools[f] && ($ = $ !== "false"), b(f, $, u); + } else if (/^--no-.+/.test(u)) + f = u.match(/^--no-(.+)/)[1], b(f, !1, u); + else if (/^--.+/.test(u)) + f = u.match(/^--(.+)/)[1], h = n[w + 1], h !== void 0 && !/^(-|--)[^-]/.test(h) && !a.bools[f] && !a.allBools && (!c[f] || !s(f)) ? (b(f, h, u), w += 1) : /^(true|false)$/.test(h) ? (b(f, h === "true", u), w += 1) : b(f, a.strings[f] ? "" : !0, u); + else if (/^-[^-]+/.test(u)) { + for (var v = u.slice(1, -1).split(""), G = !1, K = 0; K < v.length; K++) { + if (h = u.slice(K + 2), h === "-") { + b(v[K], h, u); + continue; + } + if (/[A-Za-z]/.test(v[K]) && h[0] === "=") { + b(v[K], h.slice(1), u), G = !0; + break; + } + if (/[A-Za-z]/.test(v[K]) && /-?\d+(\.\d*)?(e-?\d+)?$/.test(h)) { + b(v[K], h, u), G = !0; + break; + } + if (v[K + 1] && v[K + 1].match(/\W/)) { + b(v[K], u.slice(K + 2), u), G = !0; + break; + } else + b(v[K], a.strings[v[K]] ? "" : !0, u); + } + f = u.slice(-1)[0], !G && f !== "-" && (n[w + 1] && !/^(-|--)[^-]/.test(n[w + 1]) && !a.bools[f] && (!c[f] || !s(f)) ? (b(f, n[w + 1], u), w += 1) : n[w + 1] && /^(true|false)$/.test(n[w + 1]) ? (b(f, n[w + 1] === "true", u), w += 1) : b(f, a.strings[f] ? "" : !0, u)); + } else if ((!a.unknownFn || a.unknownFn(u) !== !1) && l._.push(a.strings._ || !t(u) ? u : Number(u)), o.stopEarly) { + l._.push.apply(l._, n.slice(w + 1)); + break; + } + } + return Object.keys(m).forEach(function(i) { + e(l, i.split(".")) || (R(l, i.split("."), m[i]), (c[i] || []).forEach(function(d) { + R(l, d.split("."), m[i]); + })); + }), o["--"] ? l["--"] = J.slice() : J.forEach(function(i) { + l._.push(i); + }), l; + }, j; +} +var ie = ce(); +const se = /* @__PURE__ */ oe(ie); +class V extends Error { + constructor(t, ...r) { + super(...r), this.name = "UnhandledMatchError", this.message = `Unhandled match value of type ${typeof t} - ${t}`, Error.captureStackTrace(this, V); + } +} +function ye(e) { + throw e; +} +const I = Symbol(), ue = (e) => ye(new V(e)), E = (e, t, r = ue) => { + const n = /* @__PURE__ */ new Map(), o = Array.isArray(t) ? t : Object.entries(t).map(([c, s]) => [c, s]); + for (const [...c] of o) { + const s = c.pop(); + for (const m of c.flat()) + n.has(m) || n.set(m, s); + } + n.has(I) || n.set(I, r); + const a = n.get(e) ?? n.get(I); + return typeof a == "function" ? a(e) : a; +}; +E.default = I; +const te = "AES-GCM", re = "SHA-256", U = async (e) => { + const t = new TextEncoder(), r = await crypto.subtle.importKey("raw", t.encode(e), "PBKDF2", !1, [ + "deriveBits", + "deriveKey" + ]); + return crypto.subtle.deriveKey( + { + name: "PBKDF2", + salt: t.encode("salt"), + iterations: 1e5, + hash: re + }, + r, + { + name: te, + length: 256 + }, + !0, + ["encrypt", "decrypt"] + ); +}, q = async (e, t, r, n) => { + if (e.type === "private") + throw new Error("Cannot wrap a private key as public key"); + return { + fingerprint: r, + wrappedKey: y.from(await crypto.subtle.exportKey("spki", e)).toString("base64"), + iv: y.from(crypto.getRandomValues(new Uint8Array(12))).toString("base64"), + format: "spki", + algorithm: t, + namedCurve: n + }; +}, k = async (e, t, r, n, o) => { + const a = "jwk", c = await crypto.subtle.exportKey(a, e), s = new TextEncoder().encode(JSON.stringify(c)), m = await U(t), l = crypto.getRandomValues(new Uint8Array(12)), C = await crypto.subtle.encrypt({ name: te, iv: l }, m, s); + return { + fingerprint: n, + wrappedKey: y.from(C).toString("base64"), + iv: y.from(l).toString("base64"), + algorithm: r, + format: a, + namedCurve: o, + protected: t.length > 0 ? !0 : void 0 + }; +}, D = async (e, t = "spki") => { + const r = await crypto.subtle.exportKey(t, e), n = await crypto.subtle.digest(re, r); + return y.from(n).toString("hex"); +}, T = "RSA-OAEP", x = "AES-GCM", pe = 2048, F = "SHA-256", me = (e) => ({ + name: T, + modulusLength: (e == null ? void 0 : e.rsaModulusLength) || pe, + publicExponent: new Uint8Array([1, 0, 1]), + hash: F +}), _ = { + async generateKeyPair(e) { + const t = me(e), r = await crypto.subtle.generateKey(t, !0, ["encrypt", "decrypt"]), n = await D(r.publicKey), o = await k( + r.privateKey, + (e == null ? void 0 : e.passphrase) ?? "", + t.name, + n + ); + return { + publicKey: await q(r.publicKey, t.name, n), + privateKey: o, + fingerprint: n + }; + }, + async importPublicKey(e) { + if (e instanceof CryptoKey) + return e; + const t = typeof e == "string" ? O(e) : e, { wrappedKey: r, algorithm: n, format: o } = t, a = { name: n, hash: F }, c = y.from(r, "base64"); + return await crypto.subtle.importKey(o, c, a, !0, ["encrypt"]); + }, + async importPrivateKey(e, t) { + if (e instanceof CryptoKey) + return e; + const r = typeof e == "string" ? O(e) : e, n = await U(t), o = y.from(r.wrappedKey, "base64"), a = y.from(r.iv, "base64"), c = await crypto.subtle.decrypt({ name: x, iv: a }, n, o), s = r.format || "pkcs8", m = s === "jwk" ? JSON.parse(new TextDecoder().decode(c)) : c; + return crypto.subtle.importKey( + s, + m, + { + name: T, + hash: F + }, + !0, + ["decrypt"] + ); + }, + async derivePublicKey(e) { + const t = await crypto.subtle.exportKey("jwk", e), r = { + kty: t.kty, + n: t.n, + e: t.e, + alg: t.alg, + ext: !0 + }; + return crypto.subtle.importKey( + "jwk", + r, + { + name: T, + hash: F + }, + !0, + ["encrypt"] + ); + }, + async encrypt(e, t) { + t = await this.importPublicKey(t); + const r = await crypto.subtle.generateKey( + { + name: x, + length: 256 + }, + !0, + ["encrypt", "decrypt"] + ), n = crypto.getRandomValues(new Uint8Array(12)), o = new TextEncoder().encode(e), a = await crypto.subtle.encrypt( + { + name: x, + iv: n + }, + r, + o + ), c = await crypto.subtle.exportKey("raw", r), s = await crypto.subtle.encrypt( + { + name: T + }, + t, + c + ), m = { + algorithm: T, + keyFingerprint: await D(t), + iv: y.from(n).toString("base64"), + symmetricKey: y.from(s).toString("base64") + }; + return { + encryptedData: y.from(a).toString("base64"), + metadata: m + }; + }, + async decrypt(e, t, r) { + const n = typeof e == "string" ? Z(e) : e; + t = await this.importPrivateKey(t, r ?? ""); + const o = n.metadata, a = y.from(o.symmetricKey, "base64"), c = await crypto.subtle.decrypt( + { + name: T + }, + t, + a + ), s = await crypto.subtle.importKey( + "raw", + c, + { + name: x, + length: 256 + }, + !1, + ["decrypt"] + ), m = y.from(n.encryptedData, "base64"), l = y.from(o.iv, "base64"), C = await crypto.subtle.decrypt( + { + name: x, + iv: l + }, + s, + m + ); + return new TextDecoder().decode(C); + } +}, A = "ECDH", ne = "P-256", B = "AES-GCM", le = (e) => ({ + name: A, + namedCurve: (e == null ? void 0 : e.eccCurve) || ne +}), M = { + async generateKeyPair(e) { + const t = le(e), r = await crypto.subtle.generateKey(t, !0, ["deriveKey", "deriveBits"]), n = await D(r.publicKey), o = await k( + r.privateKey, + (e == null ? void 0 : e.passphrase) ?? "", + t.name, + n, + t.namedCurve + ); + return { + publicKey: await q(r.publicKey, t.name, n, t.namedCurve), + privateKey: o, + fingerprint: n + }; + }, + async importPrivateKey(e, t) { + if (e instanceof CryptoKey) + return e; + const r = typeof e == "string" ? O(e) : e, n = await U(t), o = y.from(r.wrappedKey, "base64"), a = y.from(r.iv, "base64"), c = await crypto.subtle.decrypt({ name: B, iv: a }, n, o), s = r.format || (r.algorithm === A ? "jwk" : "pkcs8"), m = s === "jwk" ? JSON.parse(new TextDecoder().decode(c)) : c, l = { name: A, namedCurve: r.namedCurve }; + return crypto.subtle.importKey(s, m, l, !0, ["deriveKey", "deriveBits"]); + }, + async derivePublicKey(e) { + const t = await crypto.subtle.exportKey("jwk", e), r = { + kty: t.kty, + crv: t.crv, + x: t.x, + y: t.y, + ext: !0 + }; + return crypto.subtle.importKey( + "jwk", + r, + { + name: A, + namedCurve: e.algorithm.namedCurve + }, + !0, + [] + ); + }, + async importPublicKey(e) { + if (e instanceof CryptoKey) + return e; + const t = typeof e == "string" ? O(e) : e, { wrappedKey: r, algorithm: n, format: o, namedCurve: a } = t, c = { name: n, namedCurve: a }, s = y.from(r, "base64"); + return await crypto.subtle.importKey(o, s, c, !0, []); + }, + async encrypt(e, t) { + t = await this.importPublicKey(t); + const r = t.algorithm, n = await crypto.subtle.generateKey( + { + name: A, + namedCurve: r.namedCurve + }, + !0, + ["deriveKey", "deriveBits"] + ), o = await crypto.subtle.deriveKey( + { + name: A, + public: t + }, + n.privateKey, + { + name: B, + length: 256 + }, + !1, + ["encrypt"] + ), a = crypto.getRandomValues(new Uint8Array(12)), c = new TextEncoder().encode(e), s = await crypto.subtle.encrypt( + { + name: B, + iv: a + }, + o, + c + ), m = await crypto.subtle.exportKey("spki", n.publicKey), l = { + algorithm: A, + keyFingerprint: await D(t), + iv: y.from(a).toString("base64"), + symmetricKey: "", + // Not needed for ECC + publicKey: y.from(m).toString("base64"), + namedCurve: r.namedCurve + }; + return { + encryptedData: y.from(s).toString("base64"), + metadata: l + }; + }, + async decrypt(e, t, r) { + const n = typeof e == "string" ? Z(e) : e; + t = await this.importPrivateKey(t, r ?? ""); + const o = await crypto.subtle.importKey( + "spki", + y.from(n.metadata.publicKey, "base64"), + { + name: A, + namedCurve: n.metadata.namedCurve ?? ne + }, + !0, + [] + ), a = await crypto.subtle.deriveKey( + { + name: A, + public: o + }, + t, + { + name: B, + length: 256 + }, + !1, + ["decrypt"] + ), c = y.from(n.encryptedData, "base64"), s = y.from(n.metadata.iv, "base64"), m = await crypto.subtle.decrypt( + { + name: B, + iv: s + }, + a, + c + ); + return new TextDecoder().decode(m); + } +}, L = "AES-CTR", fe = "AES-GCM", ee = { name: L, length: 256 }, H = { + async generateKeyPair(e) { + const t = await crypto.subtle.generateKey(ee, !0, ["encrypt", "decrypt"]), r = await D(t, "raw"), n = (e == null ? void 0 : e.passphrase) || "", o = { + fingerprint: r, + wrappedKey: y.from(JSON.stringify(await crypto.subtle.exportKey("jwk", t))).toString("base64"), + algorithm: L, + format: "jwk" + }, a = n.length > 0 ? await k(t, n, L, r) : o; + return { + publicKey: o, + privateKey: a, + fingerprint: r + }; + }, + derivePublicKey() { + throw Error("Not implemented"); + }, + async importPublicKey(e) { + return this.importPrivateKey(e, ""); + }, + async importPrivateKey(e, t) { + if (e instanceof CryptoKey) + return e; + const r = typeof e == "string" ? O(e) : e, { wrappedKey: n, format: o, iv: a, protected: c } = r, s = ee; + if (c) { + const C = await U(t), R = await crypto.subtle.decrypt( + { name: fe, iv: y.from(a, "base64") }, + C, + y.from(n, "base64") + ), b = JSON.parse(new TextDecoder().decode(R)); + return await crypto.subtle.importKey(o, b, s, !0, ["encrypt", "decrypt"]); + } + const m = y.from(n, "base64").toString(), l = JSON.parse(m); + return await crypto.subtle.importKey(o, l, s, !0, ["encrypt", "decrypt"]); + }, + async encrypt(e, t) { + t = await this.importPublicKey(t); + const r = new TextEncoder().encode(e), n = { name: "AES-CTR", counter: new Uint8Array(16), length: 16 * 8 }, o = await crypto.subtle.encrypt(n, t, r), a = { + algorithm: L, + keyFingerprint: await D(t, "raw") + }; + return { + encryptedData: y.from(o).toString("base64"), + metadata: a + }; + }, + async decrypt(e, t, r) { + const n = typeof e == "string" ? Z(e) : e; + t = await this.importPrivateKey(t, r ?? ""); + const o = { name: "AES-CTR", counter: new Uint8Array(16), length: 16 * 8 }; + return new TextDecoder("utf-8").decode( + await crypto.subtle.decrypt(o, t, y.from(n.encryptedData, "base64")) + ); + } +}; +function ae(e) { + return btoa(encodeURIComponent(e)); +} +function Y(e) { + return decodeURIComponent(atob(e)); +} +const de = async (e) => { + let t; + if (typeof e == "string") + t = O(e); + else if (typeof e == "object") + t = e; + else + return e; + return E(t.algorithm, [ + ["RSA-OAEP", () => _.importPublicKey(t)], + ["ECDH", () => M.importPublicKey(t)], + ["AES-CTR", () => H.importPublicKey(t)] + ]); +}, be = async (e) => E((e == null ? void 0 : e.algorithm) ?? "RSA", [ + ["RSA", () => _.generateKeyPair(e)], + ["ECC", () => M.generateKeyPair(e)], + ["AES", () => H.generateKeyPair(e)] +]), z = (e) => ae(JSON.stringify(e)), O = (e) => JSON.parse(Y(e)), we = async (e) => ({ + publicKey: z(e.publicKey), + privateKey: z(e.privateKey), + fingerprint: e.fingerprint +}), Ke = async (e, t) => { + const r = await de(t); + return E(r.algorithm.name, [ + ["RSA-OAEP", async () => _.encrypt(e, r)], + ["ECDH", async () => M.encrypt(e, r)], + ["AES-CTR", async () => H.encrypt(e, r)] + ]); +}, he = (e) => ae(JSON.stringify(e)), Z = (e) => JSON.parse(Y(e)), ve = async (e, t, r) => (typeof e == "string" && (e = JSON.parse(Y(e))), E(e.metadata.algorithm, [ + ["RSA-OAEP", async () => _.decrypt(e, t, r)], + ["ECDH", async () => M.decrypt(e, t, r)], + ["AES-CTR", async () => H.decrypt(e, t, r)] +])), ge = async (e, t = "") => { + const r = typeof e == "string" ? O(e) : e, n = await E(r.algorithm, [ + ["RSA-OAEP", () => _.importPrivateKey(r, t)], + ["ECDH", () => M.importPrivateKey(r, t)], + ["AES-CTR", () => H.importPrivateKey(r, t)] + ]); + if (r.algorithm === "AES-CTR") + return r; + const o = await E(r.algorithm, [ + ["RSA-OAEP", () => _.derivePublicKey(n)], + ["ECDH", () => M.derivePublicKey(n)], + ["AES-CTR", () => H.derivePublicKey(n)] + ]); + return q(o, r.algorithm, r.fingerprint, r.namedCurve); +}, Se = async (e, t) => { + const { privateKey: r } = await we(await be(t)); + return r; +}, W = () => { + throw new Error("A required value was not provided"); +}; +async function Pe(e, t) { + const r = t["private-key"] ?? W(); + return z(await ge(r, t.passphrase ?? "")); +} +async function Ae([e], t) { + const r = t["public-key"] ?? W(); + return he(await Ke(e, r)); +} +async function Ee([e], t) { + const r = t["private-key"] ?? W(); + return await ve(e, r, t.passphrase ?? ""); +} +(async () => { + const e = se(process.argv.slice(2)), t = e._[0] ?? void 0; + e._.shift(); + const r = e._, n = e; + delete n._; + const o = () => E(t, [ + ["private-key:generate", () => Se(r, n)], + ["public-key:generate", () => Pe(r, n)], + ["encrypt", () => Ae(r, n)], + ["decrypt", () => Ee(r, n)] + ]); + try { + console.log(await o()), process.exit(0); + } catch (a) { + console.error(a), process.exit(1); + } +})(); diff --git a/bin/letsdecrypt.umd.cjs b/bin/letsdecrypt.umd.cjs new file mode 100644 index 0000000..ad3cbb0 --- /dev/null +++ b/bin/letsdecrypt.umd.cjs @@ -0,0 +1 @@ +(function(s,N){typeof exports=="object"&&typeof module<"u"?N(require("node:buffer")):typeof define=="function"&&define.amd?define(["node:buffer"],N):(s=typeof globalThis<"u"?globalThis:s||self,N(s.node_buffer))})(this,function(s){"use strict";function N(e){return e&&e.__esModule&&Object.prototype.hasOwnProperty.call(e,"default")?e.default:e}var j,X;function ce(){if(X)return j;X=1;function e(n,o){var a=n;o.slice(0,-1).forEach(function(y){a=a[y]||{}});var c=o[o.length-1];return c in a}function t(n){return typeof n=="number"||/^0x[0-9a-f]+$/i.test(n)?!0:/^[-+]?(?:\d+(?:\.\d*)?|\.\d+)(e[-+]?\d+)?$/.test(n)}function r(n,o){return o==="constructor"&&typeof n[o]=="function"||o==="__proto__"}return j=function(n,o){o||(o={});var a={bools:{},strings:{},unknownFn:null};typeof o.unknown=="function"&&(a.unknownFn=o.unknown),typeof o.boolean=="boolean"&&o.boolean?a.allBools=!0:[].concat(o.boolean).filter(Boolean).forEach(function(i){a.bools[i]=!0});var c={};function y(i){return c[i].some(function(d){return a.bools[d]})}Object.keys(o.alias||{}).forEach(function(i){c[i]=[].concat(o.alias[i]),c[i].forEach(function(d){c[d]=[i].concat(c[i].filter(function(P){return d!==P}))})}),[].concat(o.string).filter(Boolean).forEach(function(i){a.strings[i]=!0,c[i]&&[].concat(c[i]).forEach(function(d){a.strings[d]=!0})});var f=o.default||{},m={_:[]};function O(i,d){return a.allBools&&/^--[^=]+$/.test(d)||a.strings[i]||a.bools[i]||c[i]}function H(i,d,P){for(var p=i,I=0;Iye(new z(e)),E=(e,t,r=ue)=>{const n=new Map,o=Array.isArray(t)?t:Object.entries(t).map(([c,y])=>[c,y]);for(const[...c]of o){const y=c.pop();for(const f of c.flat())n.has(f)||n.set(f,y)}n.has(F)||n.set(F,r);const a=n.get(e)??n.get(F);return typeof a=="function"?a(e):a};E.default=F;const ee="AES-GCM",te="SHA-256",L=async e=>{const t=new TextEncoder,r=await crypto.subtle.importKey("raw",t.encode(e),"PBKDF2",!1,["deriveBits","deriveKey"]);return crypto.subtle.deriveKey({name:"PBKDF2",salt:t.encode("salt"),iterations:1e5,hash:te},r,{name:ee,length:256},!0,["encrypt","decrypt"])},q=async(e,t,r,n)=>{if(e.type==="private")throw new Error("Cannot wrap a private key as public key");return{fingerprint:r,wrappedKey:s.Buffer.from(await crypto.subtle.exportKey("spki",e)).toString("base64"),iv:s.Buffer.from(crypto.getRandomValues(new Uint8Array(12))).toString("base64"),format:"spki",algorithm:t,namedCurve:n}},V=async(e,t,r,n,o)=>{const a="jwk",c=await crypto.subtle.exportKey(a,e),y=new TextEncoder().encode(JSON.stringify(c)),f=await L(t),m=crypto.getRandomValues(new Uint8Array(12)),O=await crypto.subtle.encrypt({name:ee,iv:m},f,y);return{fingerprint:n,wrappedKey:s.Buffer.from(O).toString("base64"),iv:s.Buffer.from(m).toString("base64"),algorithm:r,format:a,namedCurve:o,protected:t.length>0?!0:void 0}},B=async(e,t="spki")=>{const r=await crypto.subtle.exportKey(t,e),n=await crypto.subtle.digest(te,r);return s.Buffer.from(n).toString("hex")},T="RSA-OAEP",x="AES-GCM",pe=2048,U="SHA-256",fe=e=>({name:T,modulusLength:(e==null?void 0:e.rsaModulusLength)||pe,publicExponent:new Uint8Array([1,0,1]),hash:U}),R={async generateKeyPair(e){const t=fe(e),r=await crypto.subtle.generateKey(t,!0,["encrypt","decrypt"]),n=await B(r.publicKey),o=await V(r.privateKey,(e==null?void 0:e.passphrase)??"",t.name,n);return{publicKey:await q(r.publicKey,t.name,n),privateKey:o,fingerprint:n}},async importPublicKey(e){if(e instanceof CryptoKey)return e;const t=typeof e=="string"?C(e):e,{wrappedKey:r,algorithm:n,format:o}=t,a={name:n,hash:U},c=s.Buffer.from(r,"base64");return await crypto.subtle.importKey(o,c,a,!0,["encrypt"])},async importPrivateKey(e,t){if(e instanceof CryptoKey)return e;const r=typeof e=="string"?C(e):e,n=await L(t),o=s.Buffer.from(r.wrappedKey,"base64"),a=s.Buffer.from(r.iv,"base64"),c=await crypto.subtle.decrypt({name:x,iv:a},n,o),y=r.format||"pkcs8",f=y==="jwk"?JSON.parse(new TextDecoder().decode(c)):c;return crypto.subtle.importKey(y,f,{name:T,hash:U},!0,["decrypt"])},async derivePublicKey(e){const t=await crypto.subtle.exportKey("jwk",e),r={kty:t.kty,n:t.n,e:t.e,alg:t.alg,ext:!0};return crypto.subtle.importKey("jwk",r,{name:T,hash:U},!0,["encrypt"])},async encrypt(e,t){t=await this.importPublicKey(t);const r=await crypto.subtle.generateKey({name:x,length:256},!0,["encrypt","decrypt"]),n=crypto.getRandomValues(new Uint8Array(12)),o=new TextEncoder().encode(e),a=await crypto.subtle.encrypt({name:x,iv:n},r,o),c=await crypto.subtle.exportKey("raw",r),y=await crypto.subtle.encrypt({name:T},t,c),f={algorithm:T,keyFingerprint:await B(t),iv:s.Buffer.from(n).toString("base64"),symmetricKey:s.Buffer.from(y).toString("base64")};return{encryptedData:s.Buffer.from(a).toString("base64"),metadata:f}},async decrypt(e,t,r){const n=typeof e=="string"?Z(e):e;t=await this.importPrivateKey(t,r??"");const o=n.metadata,a=s.Buffer.from(o.symmetricKey,"base64"),c=await crypto.subtle.decrypt({name:T},t,a),y=await crypto.subtle.importKey("raw",c,{name:x,length:256},!1,["decrypt"]),f=s.Buffer.from(n.encryptedData,"base64"),m=s.Buffer.from(o.iv,"base64"),O=await crypto.subtle.decrypt({name:x,iv:m},y,f);return new TextDecoder().decode(O)}},S="ECDH",re="P-256",G="AES-GCM",me=e=>({name:S,namedCurve:(e==null?void 0:e.eccCurve)||re}),D={async generateKeyPair(e){const t=me(e),r=await crypto.subtle.generateKey(t,!0,["deriveKey","deriveBits"]),n=await B(r.publicKey),o=await V(r.privateKey,(e==null?void 0:e.passphrase)??"",t.name,n,t.namedCurve);return{publicKey:await q(r.publicKey,t.name,n,t.namedCurve),privateKey:o,fingerprint:n}},async importPrivateKey(e,t){if(e instanceof CryptoKey)return e;const r=typeof e=="string"?C(e):e,n=await L(t),o=s.Buffer.from(r.wrappedKey,"base64"),a=s.Buffer.from(r.iv,"base64"),c=await crypto.subtle.decrypt({name:G,iv:a},n,o),y=r.format||(r.algorithm===S?"jwk":"pkcs8"),f=y==="jwk"?JSON.parse(new TextDecoder().decode(c)):c,m={name:S,namedCurve:r.namedCurve};return crypto.subtle.importKey(y,f,m,!0,["deriveKey","deriveBits"])},async derivePublicKey(e){const t=await crypto.subtle.exportKey("jwk",e),r={kty:t.kty,crv:t.crv,x:t.x,y:t.y,ext:!0};return crypto.subtle.importKey("jwk",r,{name:S,namedCurve:e.algorithm.namedCurve},!0,[])},async importPublicKey(e){if(e instanceof CryptoKey)return e;const t=typeof e=="string"?C(e):e,{wrappedKey:r,algorithm:n,format:o,namedCurve:a}=t,c={name:n,namedCurve:a},y=s.Buffer.from(r,"base64");return await crypto.subtle.importKey(o,y,c,!0,[])},async encrypt(e,t){t=await this.importPublicKey(t);const r=t.algorithm,n=await crypto.subtle.generateKey({name:S,namedCurve:r.namedCurve},!0,["deriveKey","deriveBits"]),o=await crypto.subtle.deriveKey({name:S,public:t},n.privateKey,{name:G,length:256},!1,["encrypt"]),a=crypto.getRandomValues(new Uint8Array(12)),c=new TextEncoder().encode(e),y=await crypto.subtle.encrypt({name:G,iv:a},o,c),f=await crypto.subtle.exportKey("spki",n.publicKey),m={algorithm:S,keyFingerprint:await B(t),iv:s.Buffer.from(a).toString("base64"),symmetricKey:"",publicKey:s.Buffer.from(f).toString("base64"),namedCurve:r.namedCurve};return{encryptedData:s.Buffer.from(y).toString("base64"),metadata:m}},async decrypt(e,t,r){const n=typeof e=="string"?Z(e):e;t=await this.importPrivateKey(t,r??"");const o=await crypto.subtle.importKey("spki",s.Buffer.from(n.metadata.publicKey,"base64"),{name:S,namedCurve:n.metadata.namedCurve??re},!0,[]),a=await crypto.subtle.deriveKey({name:S,public:o},t,{name:G,length:256},!1,["decrypt"]),c=s.Buffer.from(n.encryptedData,"base64"),y=s.Buffer.from(n.metadata.iv,"base64"),f=await crypto.subtle.decrypt({name:G,iv:y},a,c);return new TextDecoder().decode(f)}},J="AES-CTR",le="AES-GCM",ne={name:J,length:256},M={async generateKeyPair(e){const t=await crypto.subtle.generateKey(ne,!0,["encrypt","decrypt"]),r=await B(t,"raw"),n=(e==null?void 0:e.passphrase)||"",o={fingerprint:r,wrappedKey:s.Buffer.from(JSON.stringify(await crypto.subtle.exportKey("jwk",t))).toString("base64"),algorithm:J,format:"jwk"},a=n.length>0?await V(t,n,J,r):o;return{publicKey:o,privateKey:a,fingerprint:r}},derivePublicKey(){throw Error("Not implemented")},async importPublicKey(e){return this.importPrivateKey(e,"")},async importPrivateKey(e,t){if(e instanceof CryptoKey)return e;const r=typeof e=="string"?C(e):e,{wrappedKey:n,format:o,iv:a,protected:c}=r,y=ne;if(c){const O=await L(t),H=await crypto.subtle.decrypt({name:le,iv:s.Buffer.from(a,"base64")},O,s.Buffer.from(n,"base64")),w=JSON.parse(new TextDecoder().decode(H));return await crypto.subtle.importKey(o,w,y,!0,["encrypt","decrypt"])}const f=s.Buffer.from(n,"base64").toString(),m=JSON.parse(f);return await crypto.subtle.importKey(o,m,y,!0,["encrypt","decrypt"])},async encrypt(e,t){t=await this.importPublicKey(t);const r=new TextEncoder().encode(e),n={name:"AES-CTR",counter:new Uint8Array(16),length:16*8},o=await crypto.subtle.encrypt(n,t,r),a={algorithm:J,keyFingerprint:await B(t,"raw")};return{encryptedData:s.Buffer.from(o).toString("base64"),metadata:a}},async decrypt(e,t,r){const n=typeof e=="string"?Z(e):e;t=await this.importPrivateKey(t,r??"");const o={name:"AES-CTR",counter:new Uint8Array(16),length:16*8};return new TextDecoder("utf-8").decode(await crypto.subtle.decrypt(o,t,s.Buffer.from(n.encryptedData,"base64")))}};function ae(e){return btoa(encodeURIComponent(e))}function k(e){return decodeURIComponent(atob(e))}const de=async e=>{let t;if(typeof e=="string")t=C(e);else if(typeof e=="object")t=e;else return e;return E(t.algorithm,[["RSA-OAEP",()=>R.importPublicKey(t)],["ECDH",()=>D.importPublicKey(t)],["AES-CTR",()=>M.importPublicKey(t)]])},we=async e=>E((e==null?void 0:e.algorithm)??"RSA",[["RSA",()=>R.generateKeyPair(e)],["ECC",()=>D.generateKeyPair(e)],["AES",()=>M.generateKeyPair(e)]]),Y=e=>ae(JSON.stringify(e)),C=e=>JSON.parse(k(e)),Ke=async e=>({publicKey:Y(e.publicKey),privateKey:Y(e.privateKey),fingerprint:e.fingerprint}),be=async(e,t)=>{const r=await de(t);return E(r.algorithm.name,[["RSA-OAEP",async()=>R.encrypt(e,r)],["ECDH",async()=>D.encrypt(e,r)],["AES-CTR",async()=>M.encrypt(e,r)]])},he=e=>ae(JSON.stringify(e)),Z=e=>JSON.parse(k(e)),ve=async(e,t,r)=>(typeof e=="string"&&(e=JSON.parse(k(e))),E(e.metadata.algorithm,[["RSA-OAEP",async()=>R.decrypt(e,t,r)],["ECDH",async()=>D.decrypt(e,t,r)],["AES-CTR",async()=>M.decrypt(e,t,r)]])),ge=async(e,t="")=>{const r=typeof e=="string"?C(e):e,n=await E(r.algorithm,[["RSA-OAEP",()=>R.importPrivateKey(r,t)],["ECDH",()=>D.importPrivateKey(r,t)],["AES-CTR",()=>M.importPrivateKey(r,t)]]);if(r.algorithm==="AES-CTR")return r;const o=await E(r.algorithm,[["RSA-OAEP",()=>R.derivePublicKey(n)],["ECDH",()=>D.derivePublicKey(n)],["AES-CTR",()=>M.derivePublicKey(n)]]);return q(o,r.algorithm,r.fingerprint,r.namedCurve)},Se=async(e,t)=>{const{privateKey:r}=await Ke(await we(t));return r},W=()=>{throw new Error("A required value was not provided")};async function Pe(e,t){const r=t["private-key"]??W();return Y(await ge(r,t.passphrase??""))}async function Ae([e],t){const r=t["public-key"]??W();return he(await be(e,r))}async function Ee([e],t){const r=t["private-key"]??W();return await ve(e,r,t.passphrase??"")}(async()=>{const e=se(process.argv.slice(2)),t=e._[0]??void 0;e._.shift();const r=e._,n=e;delete n._;const o=()=>E(t,[["private-key:generate",()=>Se(r,n)],["public-key:generate",()=>Pe(r,n)],["encrypt",()=>Ae(r,n)],["decrypt",()=>Ee(r,n)]]);try{console.log(await o()),process.exit(0)}catch(a){console.error(a),process.exit(1)}})()}); diff --git a/cli/decrypt-command.ts b/cli/decrypt-command.ts new file mode 100755 index 0000000..c5e989e --- /dev/null +++ b/cli/decrypt-command.ts @@ -0,0 +1,13 @@ +import {Arguments, Options} from './input' +import {required} from './required' +import {decrypt} from '../src' + +type DecryptCommandOptions = Options & { + 'private-key': string + passphrase?: string +} + +export async function decryptCommand([value]: Arguments, options: DecryptCommandOptions) { + const privateKey = options['private-key'] ?? required() + return await decrypt(value, privateKey, options.passphrase ?? '') +} diff --git a/cli/encrypt-command.ts b/cli/encrypt-command.ts new file mode 100755 index 0000000..4eb5511 --- /dev/null +++ b/cli/encrypt-command.ts @@ -0,0 +1,12 @@ +import {Arguments, Options} from './input' +import {required} from './required' +import {encrypt, serializeSecret} from '../src' + +type EncryptCommandOptions = Options & { + 'public-key': string +} + +export async function encryptCommand([value]: Arguments, options: EncryptCommandOptions) { + const publicKey = options['public-key'] ?? required() + return serializeSecret(await encrypt(value, publicKey)) +} diff --git a/cli/generate-private-key-command.ts b/cli/generate-private-key-command.ts new file mode 100644 index 0000000..b255997 --- /dev/null +++ b/cli/generate-private-key-command.ts @@ -0,0 +1,7 @@ +import {exportKeyPair, generateKeyPair, KeyPairOptions} from '../src' +import {Arguments} from './input' + +export const generatePrivateKeyCommand = async (args: Arguments, options: KeyPairOptions) => { + const {privateKey} = await exportKeyPair(await generateKeyPair(options)) + return privateKey +} diff --git a/cli/generate-public-key-command.ts b/cli/generate-public-key-command.ts new file mode 100755 index 0000000..b8ed625 --- /dev/null +++ b/cli/generate-public-key-command.ts @@ -0,0 +1,13 @@ +import {Arguments, Options} from './input' +import {required} from './required' +import {derivePublicKey, serializeKey} from '../src' + +type GeneratePublicKeyCommandOptions = Options & { + 'private-key': string + passphrase?: string +} + +export async function generatePublicKeyCommand(args: Arguments, options: GeneratePublicKeyCommandOptions) { + const privateKey = options['private-key'] ?? required() + return serializeKey(await derivePublicKey(privateKey, options.passphrase ?? '')) +} diff --git a/cli/index.ts b/cli/index.ts new file mode 100755 index 0000000..9581e4b --- /dev/null +++ b/cli/index.ts @@ -0,0 +1,33 @@ +#!/usr/bin/env node + +import minimist from 'minimist' +import match from 'match-operator' +import {generatePrivateKeyCommand} from './generate-private-key-command' +import {Argv} from './input' +import {generatePublicKeyCommand} from './generate-public-key-command' +import {encryptCommand} from './encrypt-command' +import {decryptCommand} from './decrypt-command' +;(async () => { + const argv: Argv = minimist(process.argv.slice(2)) + const commandName = argv['_'][0] ?? undefined + argv['_'].shift() + const args = argv['_'] + const options = argv + // @ts-ignore + delete options['_'] + + const commandToRun = () => + match(commandName, [ + ['private-key:generate', () => generatePrivateKeyCommand(args, options as any)], + ['public-key:generate', () => generatePublicKeyCommand(args, options as any)], + ['encrypt', () => encryptCommand(args, options as any)], + ['decrypt', () => decryptCommand(args, options as any)], + ]) + try { + console.log(await commandToRun()) + process.exit(0) + } catch (e) { + console.error(e) + process.exit(1) + } +})() diff --git a/cli/input.ts b/cli/input.ts new file mode 100644 index 0000000..5e8c524 --- /dev/null +++ b/cli/input.ts @@ -0,0 +1,8 @@ +export type Arguments = string[] +export type Options = { + [key: string]: unknown +} + +export type Argv = Options & { + _: Arguments +} diff --git a/cli/required.ts b/cli/required.ts new file mode 100644 index 0000000..38f25ff --- /dev/null +++ b/cli/required.ts @@ -0,0 +1,3 @@ +export const required = (): never => { + throw new Error('A required value was not provided') +} diff --git a/package.json b/package.json index f262c4d..1997e61 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,9 @@ "files": [ "dist" ], + "bin": { + "letsdecrypt": "./bin/letsdecrypt.js" + }, "main": "./dist/letsdecrypt.umd.js", "module": "./dist/letsdecrypt.es.js", "types": "./dist/index.d.ts", @@ -18,23 +21,29 @@ }, "scripts": { "dev": "vite", - "build": "vite build", + "build": "npm-run-all build:lib build:preview build:cli", + "build:lib": "vite build", "build:preview": "vite build -c vite.config.preview.js --outDir .output", + "build:cli": "tsc && vite build -c vite.config.cli.js --outDir bin", "preview": "vite preview --outDir .output", "test": "vitest", - "lint": "prettier --check \"(src|tests|examples)/**\" --ignore-unknown", - "format": "prettier -w \"(src|tests|examples)/**\" --ignore-unknown" + "lint": "prettier --check \"(src|tests|examples|cli)/**\" --ignore-unknown", + "format": "prettier -w \"(src|tests|examples|cli)/**\" --ignore-unknown" }, "dependencies": { - "match-operator": "^0.3.0" + "match-operator": "^0.3.0", + "minimist": "^1.2.8" }, "devDependencies": { - "@types/node": "^20.4.2", + "@types/minimist": "^1.2.5", + "@types/node": "^22.10.7", "@vitejs/plugin-vue": "^5.2.1", "@vueuse/core": "^12.0.0", "buffer": "^6.0.3", + "npm-run-all": "^4.1.5", "path": "^0.12.7", "prettier": "^3.0.0", + "rollup-plugin-node-externals": "^8.0.0", "shiki": "^1.24.3", "typescript": "^5.0.2", "vite": "^6.0.3", diff --git a/vite.config.cli.ts b/vite.config.cli.ts new file mode 100644 index 0000000..dcbb4c2 --- /dev/null +++ b/vite.config.cli.ts @@ -0,0 +1,36 @@ +import { defineConfig, Plugin } from 'vite'; +import { resolve } from 'path'; +import { nodeExternals } from 'rollup-plugin-node-externals'; + +function externals(): Plugin { + return { + ...nodeExternals({ + deps: false, + devDeps: false, + peerDeps: false, + optDeps: false, + }), + name: 'node-externals', + enforce: 'pre', + apply: 'build', + } +} + + +export default defineConfig({ + build: { + lib: { + entry: resolve(__dirname, 'cli/index.ts'), + name: 'letsdecrypt', + fileName: 'letsdecrypt', + }, + rollupOptions: { + output: { + globals: { + //'node:fs': 'fs' + } + } + } + }, + plugins: [externals()] +})