Files
2026-05-15 15:35:31 +02:00

181 lines
5.1 KiB
JavaScript

const crypto = require('node:crypto');
const fs = require('node:fs');
const path = require('node:path');
const workspaceRoot = process.cwd();
const sourceKeyPath = path.join(workspaceRoot, 'src', 'assets', 'unoptimized', 'public.asc');
const publicKeyOutputPath = path.join(workspaceRoot, 'public', 'keys', 'public.asc');
const generatedModulePath = path.join(
workspaceRoot,
'src',
'app',
'generated',
'openpgp-key.generated.ts',
);
function ensureDirectory(directoryPath) {
fs.mkdirSync(directoryPath, { recursive: true });
}
function readAsciiArmoredKey(filePath) {
if (!fs.existsSync(filePath)) {
throw new Error(`Missing OpenPGP public key file at ${filePath}`);
}
return fs.readFileSync(filePath, 'utf8');
}
function decodeArmoredKey(armoredKey) {
const base64Lines = armoredKey
.split(/\r?\n/u)
.map((line) => line.trim())
.filter(
(line) =>
line.length > 0 &&
!line.startsWith('-----BEGIN ') &&
!line.startsWith('-----END ') &&
!line.includes(':') &&
!line.startsWith('='),
);
if (base64Lines.length === 0) {
throw new Error('OpenPGP public key armor did not contain any base64 payload.');
}
return Buffer.from(base64Lines.join(''), 'base64');
}
function parsePacketLength(buffer, offset, newFormat, lengthType) {
if (newFormat) {
const firstLengthOctet = buffer[offset];
if (firstLengthOctet < 192) {
return { headerLength: 1, packetLength: firstLengthOctet };
}
if (firstLengthOctet < 224) {
const secondLengthOctet = buffer[offset + 1];
const packetLength = ((firstLengthOctet - 192) << 8) + secondLengthOctet + 192;
return { headerLength: 2, packetLength };
}
if (firstLengthOctet === 255) {
return {
headerLength: 5,
packetLength: buffer.readUInt32BE(offset + 1),
};
}
throw new Error('OpenPGP partial body lengths are not supported by this generator.');
}
if (lengthType === 0) {
return { headerLength: 1, packetLength: buffer[offset] };
}
if (lengthType === 1) {
return { headerLength: 2, packetLength: buffer.readUInt16BE(offset) };
}
if (lengthType === 2) {
return { headerLength: 4, packetLength: buffer.readUInt32BE(offset) };
}
return { headerLength: 0, packetLength: buffer.length - offset };
}
function readPackets(buffer) {
const packets = [];
let offset = 0;
while (offset < buffer.length) {
const headerOctet = buffer[offset];
if ((headerOctet & 0x80) === 0) {
throw new Error(`Invalid OpenPGP packet header at byte offset ${offset}.`);
}
offset += 1;
const newFormat = (headerOctet & 0x40) !== 0;
const tag = newFormat ? headerOctet & 0x3f : (headerOctet >> 2) & 0x0f;
const lengthType = headerOctet & 0x03;
const { headerLength, packetLength } = parsePacketLength(buffer, offset, newFormat, lengthType);
offset += headerLength;
const body = buffer.subarray(offset, offset + packetLength);
packets.push({ tag, body });
offset += packetLength;
}
return packets;
}
function calculateFingerprintFromPacket(publicKeyPacketBody) {
const version = publicKeyPacketBody[0];
if (version === 4) {
const lengthBytes = Buffer.alloc(2);
lengthBytes.writeUInt16BE(publicKeyPacketBody.length, 0);
return crypto
.createHash('sha1')
.update(Buffer.concat([Buffer.from([0x99]), lengthBytes, publicKeyPacketBody]))
.digest('hex')
.toUpperCase();
}
if (version === 5 || version === 6) {
const lengthBytes = Buffer.alloc(4);
lengthBytes.writeUInt32BE(publicKeyPacketBody.length, 0);
return crypto
.createHash('sha256')
.update(Buffer.concat([Buffer.from([0x9a]), lengthBytes, publicKeyPacketBody]))
.digest('hex')
.toUpperCase();
}
throw new Error(`Unsupported OpenPGP public key version ${version}.`);
}
function formatFingerprint(fingerprint) {
return fingerprint.match(/.{1,4}/gu)?.join(' ') ?? fingerprint;
}
function buildGeneratedModule(assetPath, fingerprint) {
return [
'export const openPgpKey = {',
` assetPath: '${assetPath}',`,
` fingerprint: '${fingerprint}',`,
` formattedFingerprint: '${formatFingerprint(fingerprint)}',`,
'} as const;',
'',
].join('\n');
}
function run() {
const armoredKey = readAsciiArmoredKey(sourceKeyPath);
const binaryKey = decodeArmoredKey(armoredKey);
const packets = readPackets(binaryKey);
const publicKeyPacket = packets.find((packet) => packet.tag === 6);
if (!publicKeyPacket) {
throw new Error('Could not find a public-key packet in the provided OpenPGP key.');
}
const fingerprint = calculateFingerprintFromPacket(publicKeyPacket.body);
ensureDirectory(path.dirname(publicKeyOutputPath));
ensureDirectory(path.dirname(generatedModulePath));
fs.copyFileSync(sourceKeyPath, publicKeyOutputPath);
fs.writeFileSync(
generatedModulePath,
buildGeneratedModule('/keys/public.asc', fingerprint),
'utf8',
);
console.log(`Copied ${path.relative(workspaceRoot, publicKeyOutputPath)}`);
console.log(`Generated ${path.relative(workspaceRoot, generatedModulePath)}`);
console.log(`OpenPGP fingerprint: ${formatFingerprint(fingerprint)}`);
}
run();