diff options
author | João Augusto Costa Branco Marado Torres <torres.dev@disroot.org> | 2025-06-24 12:08:41 -0300 |
---|---|---|
committer | João Augusto Costa Branco Marado Torres <torres.dev@disroot.org> | 2025-06-24 12:50:43 -0300 |
commit | f9a77c5c27aede4e5978eb55d9b7af781b680a1d (patch) | |
tree | d545e325ba1ae756fc2eac66fac1001b6753c40d /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.ts | 183 |
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`, + ); + } +} |