import { createPacketReader, createSignedPacketReader } from "https://esm.town/v/vladimyr/libarmor";
import { bufferEquals } from "https://esm.town/v/vladimyr/libbuffer";
import { verifyAsync } from "npm:@noble/ed25519";
import { base64 } from "npm:multiformats/bases/base64";
const KEY_ALG_LENGTH = 2;
const KEY_ID_LENGTH = 8;
const REOP_PUBLIC_KEY_START = "-----BEGIN REOP PUBLIC KEY-----";
const REOP_PUBLIC_KEY_END = "-----END REOP PUBLIC KEY-----";
const parseArmoredKey = createPacketReader({
packetStartMarker: REOP_PUBLIC_KEY_START,
packetEndMarker: REOP_PUBLIC_KEY_END,
});
const REOP_MESSAGE_START = "-----BEGIN REOP SIGNED MESSAGE-----";
const REOP_SIGNATURE_START = "-----BEGIN REOP SIGNATURE-----";
const REOP_MESSAGE_END = "-----END REOP SIGNED MESSAGE-----";
const parseAmoredMessage = createSignedPacketReader({
packetStartMarker: REOP_MESSAGE_START,
packetEndMarker: REOP_MESSAGE_END,
signatureStartMarker: REOP_SIGNATURE_START,
});
type PublicKey = ReturnType<typeof parsePublicKey>;
export type SignedMessage = ReturnType<typeof parseSignedMessage>;
export type SigningKey = PublicKey["signingKey"];
export function parsePublicKey(input: string) {
const publicKey = parseArmoredKey(input);
const publicKeyEncoded = publicKey
.split(/\r?\n/g)
.filter(line => !line.startsWith("ident:"))
.join("")
.trim();
return decodePublicKey(publicKeyEncoded);
}
export function decodePublicKey(input: string) {
input = input.replace(/^reop/, "");
return readPublicKeyPacket(base64.baseDecode(input));
}
export function parseSignedMessage(input: string) {
const { content, signature } = parseAmoredMessage(input);
const signatureEncoded = signature
.split(/\r?\n/g)
.filter(line => !line.startsWith("ident:"))
.join("")
.trim();
return {
signature: readSignaturePacket(base64.baseDecode(signatureEncoded)),
message: {
text: content,
bytes: new TextEncoder().encode(content),
},
};
}
export async function verifyMessage(
signedMessage: string,
getKey: (message: string, signature: SignedMessage["signature"]) => SigningKey,
) {
const { message, signature } = parseSignedMessage(signedMessage);
const signingKey = getKey(message.text, signature);
if (signature.algorithm !== signingKey.algorithm) throw Error("error: algorithm mismatch");
if (signature.algorithm !== "Ed") throw Error("error: unsupported alogrithm");
if (!bufferEquals(signature.keyId, signingKey.id)) throw Error("error: key id mismatch");
const result = await verifyAsync(signature.bytes, message.bytes, signingKey.bytes);
if (!result) throw Error("error: verification failed");
return message.text;
}
function readPublicKeyPacket(publicKeyPacket: Uint8Array) {
const signingAlgorithm = new TextDecoder().decode(
new Uint8Array(publicKeyPacket.buffer, 0, KEY_ALG_LENGTH),
);
const encryptionAlgorithm = new TextDecoder().decode(
new Uint8Array(publicKeyPacket.buffer, KEY_ALG_LENGTH, KEY_ALG_LENGTH),
);
const keyId = new Uint8Array(publicKeyPacket.buffer, 2 * KEY_ALG_LENGTH, KEY_ID_LENGTH);
const keysOffset = 2 * KEY_ALG_LENGTH + KEY_ID_LENGTH;
const keyLength = new Uint8Array(publicKeyPacket.buffer, keysOffset).byteLength / 2;
const signingKeyBytes = new Uint8Array(
publicKeyPacket.buffer,
keysOffset,
keyLength,
);
const encryptionKeyBytes = new Uint8Array(
publicKeyPacket.buffer,
keysOffset + keyLength,
keyLength,
);
return {
keyId,
signingKey: {
id: keyId,