import * as ed from '@noble/ed25519';
import { sha512 } from '@noble/hashes/sha2.js';
// Configure ed25519 to use sha512 (required for noble/ed25519 v2+)
ed.etc.sha512Sync = (...m) => sha512(ed.etc.concatBytes(...m));
/**
* Sign a message with an Ed25519 private key.
*
* @param message - The message to sign (string or bytes)
* @param privateKeyBase64 - The private key in base64 format
* @returns The signature in base64 format
*/
export async function sign(message: string | Uint8Array, privateKeyBase64: string): Promise<string> {
const privateKey = base64ToBytes(privateKeyBase64);
// Handle both 32-byte seed and 64-byte full private key
const seed = privateKey.length === 64 ? privateKey.slice(0, 32) : privateKey;
const messageBytes = typeof message === 'string' ? new TextEncoder().encode(message) : message;
const signature = await ed.signAsync(messageBytes, seed);
return bytesToBase64(signature);
}
/**
* Synchronous version of sign for contexts where async is not available.
*/
export function signSync(message: string | Uint8Array, privateKeyBase64: string): string {
const privateKey = base64ToBytes(privateKeyBase64);
const seed = privateKey.length === 64 ? privateKey.slice(0, 32) : privateKey;
const messageBytes = typeof message === 'string' ? new TextEncoder().encode(message) : message;
const signature = ed.sign(messageBytes, seed);
return bytesToBase64(signature);
}
/**
* Verify an Ed25519 signature.
*
* @param message - The original message (string or bytes)
* @param signatureBase64 - The signature in base64 format
* @param publicKeyBase64 - The public key in base64 format
* @returns True if the signature is valid
*/
export async function verify(
message: string | Uint8Array,
signatureBase64: string,
publicKeyBase64: string
): Promise<boolean> {
try {
const signature = base64ToBytes(signatureBase64);
const publicKey = base64ToBytes(publicKeyBase64);
const messageBytes = typeof message === 'string' ? new TextEncoder().encode(message) : message;
return await ed.verifyAsync(signature, messageBytes, publicKey);
} catch {
return false;
}
}
/**
* Synchronous version of verify.
*/
export function verifySync(
message: string | Uint8Array,
signatureBase64: string,
publicKeyBase64: string
): boolean {
try {
const signature = base64ToBytes(signatureBase64);
const publicKey = base64ToBytes(publicKeyBase64);
const messageBytes = typeof message === 'string' ? new TextEncoder().encode(message) : message;
return ed.verify(signature, messageBytes, publicKey);
} catch {
return false;
}
}
/**
* Derive the public key from a private key.
*
* @param privateKeyBase64 - The private key in base64 format
* @returns The public key in base64 format
*/
export async function getPublicKey(privateKeyBase64: string): Promise<string> {
const privateKey = base64ToBytes(privateKeyBase64);
const seed = privateKey.length === 64 ? privateKey.slice(0, 32) : privateKey;
const publicKey = await ed.getPublicKeyAsync(seed);
return bytesToBase64(publicKey);
}
/**
* Synchronous version of getPublicKey.
*/
export function getPublicKeySync(privateKeyBase64: string): string {
const privateKey = base64ToBytes(privateKeyBase64);
const seed = privateKey.length === 64 ? privateKey.slice(0, 32) : privateKey;
const publicKey = ed.getPublicKey(seed);
return bytesToBase64(publicKey);
}
/**
* Generate a new Ed25519 keypair.
*
* @returns Object with privateKey and publicKey in base64 format
*/
export function generateKeyPair(): { privateKey: string; publicKey: string } {
const privateKey = ed.utils.randomPrivateKey();
const publicKey = ed.getPublicKey(privateKey);
return {
privateKey: bytesToBase64(privateKey),
publicKey: bytesToBase64(publicKey),
};
}
/**
* Create a signature payload for API requests.
* The payload includes timestamp to prevent replay attacks.
*
* @param method - HTTP method
* @param path - Request path
* @param body - Optional request body (will be JSON stringified if object)
* @returns The canonical string to sign
*/
export function createSignaturePayload(
method: string,
path: string,
body?: unknown,
timestamp?: number
): string {
const ts = timestamp ?? Date.now();
const bodyStr = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : '';
// Canonical format: METHOD\nPATH\nTIMESTAMP\nBODY_HASH
const bodyHash = bodyStr ? hashBody(bodyStr) : '';
return `${method.toUpperCase()}\n${path}\n${ts}\n${bodyHash}`;
}
/**
* Simple hash of body content for signature payload.
* Uses a basic checksum approach for determinism.
*/
function hashBody(body: string): string {
// Use sha512 for body hashing
const bytes = new TextEncoder().encode(body);
const hash = sha512(bytes);
return bytesToBase64(hash.slice(0, 32)); // Use first 32 bytes
}
// Utility functions for base64 encoding/decoding
function base64ToBytes(base64: string): Uint8Array {
const binary = atob(base64);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
function bytesToBase64(bytes: Uint8Array): string {
let binary = '';
for (let i = 0; i < bytes.length; i++) {
binary += String.fromCharCode(bytes[i]!);
}
return btoa(binary);
}
export { base64ToBytes, bytesToBase64 };