120 lines
3.3 KiB
TypeScript
120 lines
3.3 KiB
TypeScript
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);
|
|
}
|