release: WRNexusJS 0.4.0
This commit is contained in:
@@ -138,3 +138,5 @@ export async function deriveKey(password: string, salt: string): Promise<string>
|
||||
);
|
||||
return toB64(new Uint8Array(bits));
|
||||
}
|
||||
export { createKeyring, seal, open, sealedKeyId, needsRotation } from "./keyring.ts";
|
||||
export type { EncryptionKey, EncryptionKeyring } from "./keyring.ts";
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { decrypt, encrypt, generateKey } from "./index.ts";
|
||||
|
||||
export interface EncryptionKey {
|
||||
id: string;
|
||||
secret: string;
|
||||
active?: boolean;
|
||||
createdAt?: number;
|
||||
}
|
||||
|
||||
export interface EncryptionKeyring {
|
||||
active(): EncryptionKey;
|
||||
get(id: string): EncryptionKey | undefined;
|
||||
keys(): EncryptionKey[];
|
||||
rotate(key?: EncryptionKey): Promise<EncryptionKey>;
|
||||
remove(id: string): boolean;
|
||||
}
|
||||
|
||||
const KEY_ID = /^[A-Za-z0-9._-]{1,64}$/;
|
||||
|
||||
function validateKey(key: EncryptionKey): void {
|
||||
if (!KEY_ID.test(key.id)) {
|
||||
throw new TypeError(`Invalid encryption key id: ${key.id}`);
|
||||
}
|
||||
if (!key.secret.trim()) {
|
||||
throw new TypeError(`Encryption key '${key.id}' has an empty secret`);
|
||||
}
|
||||
}
|
||||
|
||||
export function createKeyring(initial: EncryptionKey[]): EncryptionKeyring {
|
||||
const values = new Map<string, EncryptionKey>();
|
||||
let activeCount = 0;
|
||||
for (const key of initial) {
|
||||
validateKey(key);
|
||||
if (values.has(key.id)) {
|
||||
throw new Error(`WRN-ENCRYPTION-KEYRING-DUPLICATE: ${key.id}`);
|
||||
}
|
||||
if (key.active) activeCount++;
|
||||
values.set(key.id, { ...key });
|
||||
}
|
||||
if (!values.size) throw new Error("WRN-ENCRYPTION-KEYRING-EMPTY");
|
||||
if (activeCount > 1) throw new Error("WRN-ENCRYPTION-KEYRING-MULTIPLE-ACTIVE");
|
||||
|
||||
const activeInternal = (): EncryptionKey => {
|
||||
const key = [...values.values()].find((entry) => entry.active) ?? [...values.values()].at(-1);
|
||||
if (!key) throw new Error("WRN-ENCRYPTION-ACTIVE-KEY-MISSING");
|
||||
return key;
|
||||
};
|
||||
|
||||
return {
|
||||
active: () => ({ ...activeInternal() }),
|
||||
get(id) {
|
||||
const key = values.get(id);
|
||||
return key ? { ...key } : undefined;
|
||||
},
|
||||
keys: () => [...values.values()].map((key) => ({ ...key })),
|
||||
async rotate(key) {
|
||||
const next = key ?? {
|
||||
id: `key-${Date.now().toString(36)}`,
|
||||
secret: await generateKey(),
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
validateKey(next);
|
||||
for (const current of values.values()) current.active = false;
|
||||
const stored = { ...next, active: true };
|
||||
values.set(stored.id, stored);
|
||||
return { ...stored };
|
||||
},
|
||||
remove(id) {
|
||||
if (!values.has(id)) return false;
|
||||
if (values.size <= 1) {
|
||||
throw new Error("WRN-ENCRYPTION-KEYRING-LAST-KEY");
|
||||
}
|
||||
if (activeInternal().id === id) {
|
||||
throw new Error("WRN-ENCRYPTION-KEYRING-ACTIVE-REMOVE");
|
||||
}
|
||||
return values.delete(id);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/** Versioned payload: `wrn1.<key-id>.<aes-gcm-payload>`. */
|
||||
export async function seal(plaintext: string, keyring: EncryptionKeyring): Promise<string> {
|
||||
const key = keyring.active();
|
||||
return `wrn1.${key.id}.${await encrypt(plaintext, key.secret)}`;
|
||||
}
|
||||
|
||||
export async function open(sealed: string, keyring: EncryptionKeyring): Promise<string> {
|
||||
const match = /^wrn1\.([A-Za-z0-9._-]{1,64})\.(.+)$/.exec(sealed);
|
||||
if (!match) throw new Error("WRN-ENCRYPTION-PAYLOAD-VERSION");
|
||||
const key = keyring.get(match[1]!);
|
||||
if (!key) throw new Error(`WRN-ENCRYPTION-KEY-NOT-FOUND: ${match[1]}`);
|
||||
return decrypt(match[2]!, key.secret);
|
||||
}
|
||||
|
||||
export function sealedKeyId(sealed: string): string | null {
|
||||
return /^wrn1\.([A-Za-z0-9._-]{1,64})\./.exec(sealed)?.[1] ?? null;
|
||||
}
|
||||
|
||||
export function needsRotation(sealed: string, keyring: EncryptionKeyring): boolean {
|
||||
return sealedKeyId(sealed) !== keyring.active().id;
|
||||
}
|
||||
Reference in New Issue
Block a user