181 lines
5.1 KiB
JavaScript
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();
|