summaryrefslogtreecommitdiff
path: root/src/lib/pgp/create.ts
diff options
context:
space:
mode:
authorJoão Augusto Costa Branco Marado Torres <torres.dev@disroot.org>2025-06-24 12:08:41 -0300
committerJoão Augusto Costa Branco Marado Torres <torres.dev@disroot.org>2025-06-24 12:50:43 -0300
commitf9a77c5c27aede4e5978eb55d9b7af781b680a1d (patch)
treed545e325ba1ae756fc2eac66fac1001b6753c40d /src/lib/pgp/create.ts
feat!: initial commit
Signed-off-by: João Augusto Costa Branco Marado Torres <torres.dev@disroot.org>
Diffstat (limited to 'src/lib/pgp/create.ts')
-rw-r--r--src/lib/pgp/create.ts183
1 files changed, 183 insertions, 0 deletions
diff --git a/src/lib/pgp/create.ts b/src/lib/pgp/create.ts
new file mode 100644
index 0000000..fb45954
--- /dev/null
+++ b/src/lib/pgp/create.ts
@@ -0,0 +1,183 @@
+import { readKey } from "openpgp";
+
+export const armored: unique symbol = Symbol();
+export const binary: unique symbol = Symbol();
+export type KeyFileFormat = typeof armored | typeof binary;
+
+export interface KeyDiscoveryRules {
+ formats?: Partial<Record<KeyFileFormat, Set<string> | undefined>>;
+ recursive?: boolean | number;
+}
+export const DEFAULT_KEY_DISCOVERY_RULES = {
+ formats: {
+ [armored]: new Set(["asc"]),
+ [binary]: new Set(["gpg"]),
+ },
+} satisfies KeyDiscoveryRules;
+
+export async function* createKeysFromFs(
+ key: string | URL,
+ rules: KeyDiscoveryRules = DEFAULT_KEY_DISCOVERY_RULES,
+ coders: { decoder?: TextDecoder; encoder?: TextEncoder } = {},
+): AsyncGenerator<Awaited<ReturnType<typeof readKey>>, void, void> {
+ key = new URL(key);
+
+ validateKeyDiscoveryRules(rules);
+
+ const stat = await Deno.stat(key);
+
+ if (stat.isDirectory) {
+ const generator = createKeysFromDir(key, rules, coders);
+ yield* generator;
+ } else if (stat.isFile) {
+ const period = key.pathname.lastIndexOf(".");
+ const ext = period === -1 ? "" : key.pathname.slice(period + 1);
+ if (
+ rules.formats?.[armored] !== undefined && rules.formats[armored].has(ext)
+ ) {
+ yield createKeyFromFile(
+ key,
+ armored,
+ coders?.decoder,
+ );
+ } else if (
+ rules.formats?.[binary] !== undefined && rules.formats[binary].has(ext)
+ ) {
+ yield createKeyFromFile(
+ key,
+ binary,
+ coders?.encoder,
+ );
+ }
+ }
+}
+
+export async function* createKeysFromDir(
+ key: string | URL,
+ rules: KeyDiscoveryRules = DEFAULT_KEY_DISCOVERY_RULES,
+ coders: { decoder?: TextDecoder; encoder?: TextEncoder } = {},
+): AsyncGenerator<Awaited<ReturnType<typeof readKey>>, void, void> {
+ key = new URL(key);
+
+ validateKeyDiscoveryRules(rules);
+
+ for await (const dirEntry of Deno.readDir(key)) {
+ const filePath = new URL(dirEntry.name, key);
+ if (dirEntry.isFile) {
+ const period = filePath.pathname.lastIndexOf(".");
+ const ext = period === -1 ? "" : filePath.pathname.slice(period + 1);
+ if (
+ rules.formats?.[armored] !== undefined &&
+ rules.formats[armored].has(ext)
+ ) {
+ yield createKeyFromFile(
+ filePath,
+ armored,
+ coders?.decoder,
+ );
+ } else if (
+ rules.formats?.[binary] !== undefined && rules.formats[binary].has(ext)
+ ) {
+ yield createKeyFromFile(
+ filePath,
+ binary,
+ coders?.encoder,
+ );
+ }
+ } else if (dirEntry.isDirectory) {
+ const depth = typeof rules.recursive === "number"
+ ? rules.recursive
+ : rules.recursive
+ ? Infinity
+ : 0;
+ if (depth > 0) {
+ yield* createKeysFromDir(filePath, {
+ ...rules,
+ recursive: depth - 1,
+ }, coders);
+ }
+ }
+ }
+}
+
+export async function createKeyFromFile(
+ key: string | URL,
+ type: typeof armored,
+ coder?: TextDecoder,
+): ReturnType<typeof readKey>;
+export async function createKeyFromFile(
+ key: string | URL,
+ type: typeof binary,
+ coder?: TextEncoder,
+): ReturnType<typeof readKey>;
+export async function createKeyFromFile(
+ key: string | URL,
+ type: typeof armored | typeof binary,
+ coder?: TextDecoder | TextEncoder,
+): ReturnType<typeof readKey> {
+ switch (type) {
+ case armored:
+ return await Deno.readTextFile(key).then((key) =>
+ createKeyFromArmor(key, coder as TextDecoder)
+ );
+ case binary:
+ return await Deno.readFile(key).then((key) =>
+ createKeyFromBinary(key, coder as TextEncoder)
+ );
+ }
+}
+
+export function createKeyFromArmor(
+ key: string | Uint8Array,
+ decoder?: TextDecoder,
+): ReturnType<typeof readKey> {
+ return readKey({
+ armoredKey: typeof key === "string"
+ ? key
+ : (decoder ?? new TextDecoder()).decode(key),
+ });
+}
+export function createKeyFromBinary(
+ key: string | Uint8Array,
+ encoder?: TextEncoder,
+): ReturnType<typeof readKey> {
+ return readKey({
+ binaryKey: typeof key === "string"
+ ? (encoder ?? new TextEncoder()).encode(key)
+ : key,
+ });
+}
+
+function validateKeyDiscoveryRules(rules: KeyDiscoveryRules) {
+ let disjoint = true;
+ let union: Set<string> | undefined = undefined;
+ const keys = rules.formats !== undefined
+ ? Object.getOwnPropertySymbols(rules.formats) as KeyFileFormat[]
+ : [];
+
+ for (const i of keys) {
+ const set = rules.formats?.[i];
+
+ if (union === undefined) {
+ union = set;
+ continue;
+ }
+
+ if (set === undefined) {
+ continue;
+ }
+
+ disjoint &&= union.isDisjointFrom(set);
+ union = union.union(set);
+
+ if (!disjoint) {
+ break;
+ }
+ }
+
+ if (!disjoint) {
+ throw new Error(
+ `\`Set\`s from \`rules.formats\` aren't disjoint`,
+ );
+ }
+}