feat(client): add keypair generation

This commit is contained in:
nav
2026-04-08 21:34:38 +00:00
parent fb39a17dbb
commit fc226b1f18
3 changed files with 155 additions and 4 deletions

119
plugin/crypto/keypair.ts Normal file
View File

@@ -0,0 +1,119 @@
import { randomBytes, sign as signMessageRaw, verify as verifySignatureRaw } from "node:crypto";
/**
* Key pair for Yonexus client authentication
* Uses Ed25519 for digital signatures
*/
export interface KeyPair {
/** Base64-encoded Ed25519 private key */
readonly privateKey: string;
/** Base64-encoded Ed25519 public key */
readonly publicKey: string;
/** Algorithm identifier for compatibility */
readonly algorithm: "Ed25519";
}
/**
* Generate a new Ed25519 key pair for client authentication.
*
* In v1, we use Node.js crypto.generateKeyPairSync for Ed25519.
* The keys are encoded as base64 for JSON serialization.
*/
export async function generateKeyPair(): Promise<KeyPair> {
const { generateKeyPair } = await import("node:crypto");
const { publicKey, privateKey } = await new Promise<{
publicKey: string;
privateKey: string;
}>((resolve, reject) => {
generateKeyPair(
"ed25519",
{
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" }
},
(err, pubKey, privKey) => {
if (err) {
reject(err);
return;
}
resolve({ publicKey: pubKey, privateKey: privKey });
}
);
});
return {
privateKey,
publicKey,
algorithm: "Ed25519"
};
}
/**
* Sign a message using the client's private key.
*
* @param privateKeyPem - PEM-encoded private key
* @param message - Message to sign (Buffer or string)
* @returns Base64-encoded signature
*/
export async function signMessage(
privateKeyPem: string,
message: Buffer | string
): Promise<string> {
const signature = signMessageRaw(null, typeof message === "string" ? Buffer.from(message) : message, privateKeyPem);
return signature.toString("base64");
}
/**
* Verify a signature using the client's public key.
*
* @param publicKeyPem - PEM-encoded public key
* @param message - Original message (Buffer or string)
* @param signature - Base64-encoded signature
* @returns Whether the signature is valid
*/
export async function verifySignature(
publicKeyPem: string,
message: Buffer | string,
signature: string
): Promise<boolean> {
try {
const sigBuffer = Buffer.from(signature, "base64");
return verifySignatureRaw(null, typeof message === "string" ? Buffer.from(message) : message, publicKeyPem, sigBuffer);
} catch {
return false;
}
}
/**
* Generate a cryptographically secure random pairing code.
* Format: XXXX-XXXX-XXXX (12 alphanumeric characters in groups of 4)
*/
export function generatePairingCode(): string {
const bytes = randomBytes(8);
const chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // Excludes confusing chars (0, O, 1, I)
let code = "";
for (let i = 0; i < 12; i++) {
code += chars[bytes[i % bytes.length] % chars.length];
}
// Format as XXXX-XXXX-XXXX
return `${code.slice(0, 4)}-${code.slice(4, 8)}-${code.slice(8, 12)}`;
}
/**
* Generate a shared secret for client authentication.
* This is issued by the server after successful pairing.
*/
export function generateSecret(): string {
return randomBytes(32).toString("base64url");
}
/**
* Generate a 24-character nonce for authentication.
*/
export function generateNonce(): string {
const bytes = randomBytes(18);
return bytes.toString("base64url").slice(0, 24);
}