UNPKG

baid64

Version:

URL-safe Base64 encoding with checksums for identities

146 lines (122 loc) 3.94 kB
import { createHash } from 'crypto'; // Constants matching Rust implementation export const BAID64_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_~'; const STANDARD_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; // Helper to convert between alphabets function toStandardBase64(baid64Str) { let result = ''; for (let char of baid64Str) { const idx = BAID64_ALPHABET.indexOf(char); if (idx === -1) throw new Error(`Invalid baid64 character: ${char}`); result += STANDARD_ALPHABET[idx]; } return result; } function fromStandardBase64(base64Str) { let result = ''; for (let char of base64Str) { const idx = STANDARD_ALPHABET.indexOf(char); if (idx === -1) throw new Error(`Invalid base64 character: ${char}`); result += BAID64_ALPHABET[idx]; } return result; } // Calculate checksum matching Rust implementation export function calculateChecksum(hri, payload) { // First hash: SHA256 of HRI const key = createHash('sha256').update(hri).digest(); // Second hash: SHA256 with key as prefix, then payload const hash = createHash('sha256'); hash.update(key); hash.update(payload); const result = hash.digest(); // Return [result[0], result[1], result[1], result[2]] - note duplication! return Buffer.from([result[0], result[1], result[1], result[2]]); } // Main encoding function export function encode(payload, options = {}) { const { hri = '', chunking = false, chunkFirst = 8, chunkLen = 7, prefix = false, mnemonic = false, embedChecksum = false } = options; let data = Buffer.from(payload); // Add embedded checksum if requested if (embedChecksum && hri) { const checksum = calculateChecksum(hri, data); data = Buffer.concat([data, checksum]); } // Convert to base64 with custom alphabet let encoded = fromStandardBase64(data.toString('base64').replace(/=/g, '')); // Apply chunking if requested if (chunking) { let chunked = encoded.slice(0, chunkFirst); for (let i = chunkFirst; i < encoded.length; i += chunkLen) { chunked += '-' + encoded.slice(i, i + chunkLen); } encoded = chunked; } // Build final string let result = ''; if (prefix && hri) { result += hri + ':'; } result += encoded; // Note: mnemonic encoding would require additional library // Skipping for basic implementation return result; } // Main decoding function export function decode(baid64Str, expectedHri = null) { let str = baid64Str; let hri = ''; let mnemonic = ''; // Extract HRI if present if (str.includes(':')) { [hri, str] = str.split(':'); if (expectedHri && hri !== expectedHri) { throw new Error(`Invalid HRI: expected ${expectedHri}, got ${hri}`); } } // Extract mnemonic if present if (str.includes('#')) { [str, mnemonic] = str.split('#'); } // Remove chunking str = str.replace(/-/g, ''); // Convert to standard base64 and decode const standardBase64 = toStandardBase64(str); const paddingNeeded = (4 - (standardBase64.length % 4)) % 4; const padded = standardBase64 + '='.repeat(paddingNeeded); const decoded = Buffer.from(padded, 'base64'); // Check if last 4 bytes might be checksum let payload = decoded; let embeddedChecksum = null; if (hri && decoded.length > 4) { // Could have embedded checksum const possiblePayload = decoded.slice(0, -4); const possibleChecksum = decoded.slice(-4); const expectedChecksum = calculateChecksum(hri, possiblePayload); if (Buffer.compare(possibleChecksum, expectedChecksum) === 0) { payload = possiblePayload; embeddedChecksum = possibleChecksum; } } return { hri, payload, embeddedChecksum, mnemonic }; } // Default export with all functions export default { encode, decode, calculateChecksum, BAID64_ALPHABET };