adding opgenpgp key

This commit is contained in:
2026-05-15 15:35:31 +02:00
parent 17cb23aa40
commit f0463229a1
15 changed files with 442 additions and 68 deletions
+180
View File
@@ -0,0 +1,180 @@
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();
+10 -51
View File
@@ -1,5 +1,5 @@
import { createReadStream } from 'node:fs';
import { access, readdir, stat } from 'node:fs/promises';
import { access, stat } from 'node:fs/promises';
import { createServer } from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -9,15 +9,13 @@ const __dirname = path.dirname(__filename);
const appRoot = path.resolve(__dirname, '..');
const distRoot = path.join(appRoot, 'dist');
const port = Number.parseInt(process.env.PORT ?? '3000', 10);
const browserRoot = path.join(distRoot, 'browser');
const port = 3000;
const mimeTypes = new Map([
['.css', 'text/css; charset=utf-8'],
['.gif', 'image/gif'],
['.html', 'text/html; charset=utf-8'],
['.ico', 'image/x-icon'],
['.jpg', 'image/jpeg'],
['.jpeg', 'image/jpeg'],
['.js', 'text/javascript; charset=utf-8'],
['.json', 'application/json; charset=utf-8'],
['.map', 'application/json; charset=utf-8'],
@@ -25,8 +23,6 @@ const mimeTypes = new Map([
['.svg', 'image/svg+xml'],
['.txt', 'text/plain; charset=utf-8'],
['.webp', 'image/webp'],
['.woff', 'font/woff'],
['.woff2', 'font/woff2'],
['.xml', 'application/xml; charset=utf-8'],
]);
@@ -48,23 +44,9 @@ function getCacheControl(filePath) {
}
if (
[
'.css',
'.gif',
'.ico',
'.jpg',
'.jpeg',
'.js',
'.json',
'.map',
'.png',
'.svg',
'.txt',
'.webp',
'.woff',
'.woff2',
'.xml',
].includes(extension)
['.css', '.ico', '.js', '.json', '.map', '.png', '.svg', '.txt', '.webp', '.xml'].includes(
extension,
)
) {
return normalizedPath.includes('/browser/')
? 'public, max-age=604800, stale-while-revalidate=86400'
@@ -83,36 +65,13 @@ async function pathExists(targetPath) {
}
}
async function resolveBrowserRoot() {
const directBrowserRoot = path.join(distRoot, 'browser');
if (await pathExists(path.join(directBrowserRoot, 'index.html'))) {
return directBrowserRoot;
}
const distEntries = await readdir(distRoot, { withFileTypes: true });
for (const entry of distEntries) {
if (!entry.isDirectory()) {
continue;
}
const nestedBrowserRoot = path.join(distRoot, entry.name, 'browser');
if (await pathExists(path.join(nestedBrowserRoot, 'index.html'))) {
return nestedBrowserRoot;
}
}
throw new Error(`Could not locate a browser build under ${distRoot}`);
}
const browserRoot = await resolveBrowserRoot();
async function resolveFile(urlPath) {
const normalized = path.posix.normalize(decodeURIComponent(urlPath));
const relativePath = normalized.replace(/^(\.\.(\/|\\|$))+/, '').replace(/^\/+/, '');
const distCandidate = path.join(distRoot, relativePath);
if ((await pathExists(distCandidate)) && (await stat(distCandidate)).isFile()) {
return distCandidate;
const rootCandidate = path.join(distRoot, relativePath);
if ((await pathExists(rootCandidate)) && (await stat(rootCandidate)).isFile()) {
return rootCandidate;
}
const browserCandidate = path.join(browserRoot, relativePath);
@@ -151,5 +110,5 @@ const server = createServer(async (request, response) => {
});
server.listen(port, '0.0.0.0', () => {
console.log(`Serving static app from ${browserRoot} on port ${port}`);
console.log(`Serving static app on port ${port}`);
});