@istvan.xyz/phc-format
Version:
An implementation of the PHC format.
1 lines • 15.7 kB
Source Map (JSON)
{"version":3,"sources":["../src/base64.ts","../src/patterns.ts","../src/serialize.ts","../src/deserialize.ts"],"sourcesContent":["// Lightweight base64 helpers that work in Node, Deno, and browsers without requiring Buffer types.\n// Encoding omits padding (=) to match PHC formatting behavior used in this project.\n\nfunction bytesToBinaryString(bytes: Uint8Array): string {\n let result = '';\n for (const b of bytes) {\n result += String.fromCharCode(b);\n }\n return result;\n}\n\nfunction binaryStringToBytes(binary: string): Uint8Array {\n const out = new Uint8Array(binary.length);\n for (let i = 0; i < binary.length; i++) {\n out[i] = binary.charCodeAt(i);\n }\n return out;\n}\n\nexport function bytesToBase64(bytes: Uint8Array): string {\n // Prefer global btoa when available (Deno, browsers)\n let b64: string;\n const g = globalThis as unknown as {\n btoa?: (data: string) => string;\n atob?: (data: string) => string;\n // Intentionally loose shape to avoid Node type dependency\n Buffer?: unknown;\n };\n if (typeof g.btoa === 'function') {\n b64 = g.btoa(bytesToBinaryString(bytes));\n } else if (typeof g.Buffer !== 'undefined') {\n // Node fallback\n // Use node's Buffer without importing types\n const BufferLike = g.Buffer as unknown as {\n from: (input: unknown) => { toString: (enc: string) => string };\n };\n const nodeBuf = BufferLike.from(bytes);\n b64 = nodeBuf.toString('base64');\n } else {\n // Very old environments; last resort using TextEncoder/Decoder\n // Note: This will not work without a polyfill for btoa/atob\n throw new Error('No base64 encoder available in this environment');\n }\n // Strip trailing padding as PHC examples avoid '=' chars\n return b64.replace(/=+$/u, '');\n}\n\nexport function base64ToBytes(b64: string): Uint8Array {\n // Re-add padding to make length a multiple of 4\n const padLength = (4 - (b64.length % 4)) % 4;\n const padded = b64 + '='.repeat(padLength);\n\n const g = globalThis as unknown as {\n atob?: (data: string) => string;\n Buffer?: unknown;\n };\n if (typeof g.atob === 'function') {\n const binary = g.atob(padded);\n return binaryStringToBytes(binary);\n }\n if (typeof g.Buffer !== 'undefined') {\n const BufferLike = g.Buffer as unknown as {\n from: (input: unknown, enc: string) => Uint8Array;\n };\n const buf = BufferLike.from(padded, 'base64');\n return new Uint8Array(buf);\n }\n throw new Error('No base64 decoder available in this environment');\n}\n\n// In Node, prefer returning Buffer instances for compatibility with existing code/tests.\nexport function asRuntimeBytes(bytes: Uint8Array): Uint8Array {\n const g = globalThis as unknown as { Buffer?: unknown };\n if (typeof g.Buffer !== 'undefined') {\n const BufferLike = g.Buffer as unknown as {\n from: (input: unknown) => Uint8Array;\n };\n return BufferLike.from(bytes);\n }\n return bytes;\n}\n","export const idRegex = /^[a-z0-9-]{1,32}$/;\nexport const nameRegex = /^[a-z0-9-]{1,32}$/;\nexport const valueRegex = /^[a-zA-Z0-9/+.-]+$/;\n","// cspell:words phcobj, phcstr, Valto, strpar, pchstr, maxf, parstr\nimport { bytesToBase64 } from './base64';\nimport { idRegex, nameRegex, valueRegex } from './patterns';\n\nfunction objectToKeyValueString(object: { [key: string]: unknown }) {\n return Object.entries(object)\n .map(([key, value]) => [key, value].join('='))\n .join(',');\n}\n\n/**\n * Generates a PHC string using the data provided.\n * @param {Object} opts Object that holds the data needed to generate the PHC\n * string.\n * @param {string} opts.id Symbolic name for the function.\n * @param {Number} [opts.version] The version of the function.\n * @param {Object} [opts.params] Parameters of the function.\n * @param {Buffer} [opts.salt] The salt as a binary buffer.\n * @param {Buffer} [opts.hash] The hash as a binary buffer.\n * @return {string} The hash string adhering to the PHC format.\n */\nexport default function serialize(opts: {\n id: string;\n version?: number;\n params?: { [key: string]: string | Uint8Array | number };\n salt?: Uint8Array;\n hash?: Uint8Array;\n}) {\n const fields = [''];\n\n if (!idRegex.test(opts.id)) {\n throw new TypeError(`id must satisfy ${idRegex}`);\n }\n\n fields.push(opts.id);\n\n if (typeof opts.version !== 'undefined') {\n if (opts.version < 0 || !Number.isInteger(opts.version)) {\n throw new TypeError('version must be a positive integer number');\n }\n\n fields.push(`v=${opts.version}`);\n }\n\n // Parameter Validation\n const { params } = opts;\n if (typeof params !== 'undefined') {\n const safeParams: { [key: string]: string | Uint8Array | number } = { ...params };\n const pk = Object.keys(safeParams);\n if (!pk.every(p => nameRegex.test(p))) {\n throw new TypeError(`params names must satisfy ${nameRegex}`);\n }\n\n // Convert Numbers into Numeric Strings and Uint8Array into B64 encoded strings.\n pk.forEach(k => {\n if (typeof safeParams[k] === 'number') {\n safeParams[k] = String(safeParams[k] as number);\n } else if (safeParams[k] instanceof Uint8Array) {\n safeParams[k] = bytesToBase64(safeParams[k] as Uint8Array);\n }\n });\n const pv = Object.values(safeParams);\n if (!pv.every(v => typeof v === 'string')) {\n throw new TypeError('params values must be strings');\n }\n\n if (!pv.every(v => valueRegex.test(v as string))) {\n throw new TypeError(`params values must satisfy ${valueRegex}`);\n }\n\n const strpar = objectToKeyValueString(safeParams);\n fields.push(strpar);\n }\n\n if (typeof opts.salt !== 'undefined') {\n fields.push(bytesToBase64(opts.salt));\n\n if (typeof opts.hash !== 'undefined') {\n // Hash Validation\n if (!(opts.hash instanceof Uint8Array)) {\n throw new TypeError('hash must be a Uint8Array');\n }\n\n fields.push(bytesToBase64(opts.hash));\n }\n }\n\n // Create the PHC formatted string\n const phcString = fields.join('$');\n\n return phcString;\n}\n","import { asRuntimeBytes, base64ToBytes } from './base64';\nimport { idRegex, nameRegex, valueRegex } from './patterns';\n\nconst b64Regex = /^([a-zA-Z0-9/+.-]+|)$/;\nconst decimalRegex = /^((-)?[1-9]\\d*|0)$/;\nconst versionRegex = /^v=(\\d+)$/;\n\nconst keyValueStringToObject = (string: string): { [key: string]: unknown } => {\n const object: { [key: string]: string } = {};\n\n string.split(',').forEach(ps => {\n const tokens = ps.split('=');\n if (tokens.length < 2) {\n throw new TypeError('params must be in the format name=value');\n }\n\n object[tokens.shift() as string] = tokens.join('=');\n });\n\n return object;\n};\n\n/**\n * Parses data from a PHC string.\n * @param {string} phcString A PHC string to parse.\n * @return {Object} The object containing the data parsed from the PHC string.\n */\nexport default function deserialize(phcString: string) {\n if (phcString === '') {\n throw new TypeError('phcString must be a non-empty string');\n }\n\n if (phcString[0] !== '$') {\n throw new TypeError('phcString must contain a $ as first char');\n }\n\n const fields = phcString.split('$');\n\n // Remove first empty $\n fields.shift();\n\n // Parse Fields\n let maxFields = 5;\n\n if (!versionRegex.test(fields[1])) maxFields--;\n if (fields.length > maxFields) {\n throw new TypeError(`phcString contains too many fields: ${fields.length}/${maxFields}`);\n }\n\n // Parse Identifier\n const id = fields.shift();\n\n if (!id) {\n throw new Error('id cannot be undefined at this point.');\n }\n\n if (!idRegex.test(id)) {\n throw new TypeError(`id must satisfy ${idRegex}`);\n }\n\n let version;\n\n // Parse Version\n if (versionRegex.test(fields[0])) {\n const versionString = fields.shift();\n\n if (!versionString) {\n throw new Error('paramString cannot be undefined at this point.');\n }\n\n version = parseInt((versionString.match(versionRegex) as RegExpMatchArray)[1], 10);\n }\n\n let hash: Uint8Array | undefined;\n let salt: Uint8Array | undefined;\n if (b64Regex.test(fields[fields.length - 1])) {\n if (fields.length > 1 && b64Regex.test(fields[fields.length - 2])) {\n // Parse Hash\n hash = asRuntimeBytes(base64ToBytes(fields.pop() as string));\n // Parse Salt\n salt = asRuntimeBytes(base64ToBytes(fields.pop() as string));\n } else {\n // Parse Salt\n salt = asRuntimeBytes(base64ToBytes(fields.pop() as string));\n }\n }\n\n // Parse Parameters\n let params: { [key: string]: unknown } | undefined;\n\n if (fields.length > 0) {\n const paramString = fields.pop();\n\n if (!paramString) {\n throw new Error('paramString cannot be undefined at this point.');\n }\n\n const currentParams = keyValueStringToObject(paramString);\n\n if (!Object.keys(currentParams).every(p => nameRegex.test(p))) {\n throw new TypeError(`params names must satisfy ${nameRegex}`);\n }\n\n const pv = Object.values(currentParams);\n if (!pv.every(v => valueRegex.test(v as string))) {\n throw new TypeError(`params values must satisfy ${valueRegex}`);\n }\n\n const pk = Object.keys(currentParams);\n // Convert Decimal Strings into Numbers\n pk.forEach(k => {\n currentParams[k] = decimalRegex.test(currentParams[k] as string)\n ? parseInt(currentParams[k] as string, 10)\n : currentParams[k];\n });\n\n params = currentParams;\n }\n\n if (fields.length > 0) {\n throw new TypeError(`phcString contains unrecognized fields: ${fields}`);\n }\n\n // Build the output object\n const result: {\n id?: string;\n version?: number;\n params?: { [key: string]: unknown };\n salt?: Uint8Array;\n hash?: Uint8Array;\n } = { id };\n\n if (version) {\n result.version = version;\n }\n\n if (params) {\n result.params = params;\n }\n\n if (salt) {\n result.salt = salt;\n }\n\n if (hash) {\n result.hash = hash;\n }\n\n return result;\n}\n"],"mappings":"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","names":[]}