UNPKG

@istvan.xyz/phc-format

Version:

An implementation of the PHC format.

208 lines (205 loc) 6.32 kB
// src/base64.ts function bytesToBinaryString(bytes) { let result = ""; for (const b of bytes) { result += String.fromCharCode(b); } return result; } function binaryStringToBytes(binary) { const out = new Uint8Array(binary.length); for (let i = 0; i < binary.length; i++) { out[i] = binary.charCodeAt(i); } return out; } function bytesToBase64(bytes) { let b64; const g = globalThis; if (typeof g.btoa === "function") { b64 = g.btoa(bytesToBinaryString(bytes)); } else if (typeof g.Buffer !== "undefined") { const BufferLike = g.Buffer; const nodeBuf = BufferLike.from(bytes); b64 = nodeBuf.toString("base64"); } else { throw new Error("No base64 encoder available in this environment"); } return b64.replace(/=+$/u, ""); } function base64ToBytes(b64) { const padLength = (4 - b64.length % 4) % 4; const padded = b64 + "=".repeat(padLength); const g = globalThis; if (typeof g.atob === "function") { const binary = g.atob(padded); return binaryStringToBytes(binary); } if (typeof g.Buffer !== "undefined") { const BufferLike = g.Buffer; const buf = BufferLike.from(padded, "base64"); return new Uint8Array(buf); } throw new Error("No base64 decoder available in this environment"); } function asRuntimeBytes(bytes) { const g = globalThis; if (typeof g.Buffer !== "undefined") { const BufferLike = g.Buffer; return BufferLike.from(bytes); } return bytes; } // src/patterns.ts var idRegex = /^[a-z0-9-]{1,32}$/; var nameRegex = /^[a-z0-9-]{1,32}$/; var valueRegex = /^[a-zA-Z0-9/+.-]+$/; // src/serialize.ts function objectToKeyValueString(object) { return Object.entries(object).map(([key, value]) => [key, value].join("=")).join(","); } function serialize(opts) { const fields = [""]; if (!idRegex.test(opts.id)) { throw new TypeError(`id must satisfy ${idRegex}`); } fields.push(opts.id); if (typeof opts.version !== "undefined") { if (opts.version < 0 || !Number.isInteger(opts.version)) { throw new TypeError("version must be a positive integer number"); } fields.push(`v=${opts.version}`); } const { params } = opts; if (typeof params !== "undefined") { const safeParams = { ...params }; const pk = Object.keys(safeParams); if (!pk.every((p) => nameRegex.test(p))) { throw new TypeError(`params names must satisfy ${nameRegex}`); } pk.forEach((k) => { if (typeof safeParams[k] === "number") { safeParams[k] = String(safeParams[k]); } else if (safeParams[k] instanceof Uint8Array) { safeParams[k] = bytesToBase64(safeParams[k]); } }); const pv = Object.values(safeParams); if (!pv.every((v) => typeof v === "string")) { throw new TypeError("params values must be strings"); } if (!pv.every((v) => valueRegex.test(v))) { throw new TypeError(`params values must satisfy ${valueRegex}`); } const strpar = objectToKeyValueString(safeParams); fields.push(strpar); } if (typeof opts.salt !== "undefined") { fields.push(bytesToBase64(opts.salt)); if (typeof opts.hash !== "undefined") { if (!(opts.hash instanceof Uint8Array)) { throw new TypeError("hash must be a Uint8Array"); } fields.push(bytesToBase64(opts.hash)); } } const phcString = fields.join("$"); return phcString; } // src/deserialize.ts var b64Regex = /^([a-zA-Z0-9/+.-]+|)$/; var decimalRegex = /^((-)?[1-9]\d*|0)$/; var versionRegex = /^v=(\d+)$/; var keyValueStringToObject = (string) => { const object = {}; string.split(",").forEach((ps) => { const tokens = ps.split("="); if (tokens.length < 2) { throw new TypeError("params must be in the format name=value"); } object[tokens.shift()] = tokens.join("="); }); return object; }; function deserialize(phcString) { if (phcString === "") { throw new TypeError("phcString must be a non-empty string"); } if (phcString[0] !== "$") { throw new TypeError("phcString must contain a $ as first char"); } const fields = phcString.split("$"); fields.shift(); let maxFields = 5; if (!versionRegex.test(fields[1])) maxFields--; if (fields.length > maxFields) { throw new TypeError(`phcString contains too many fields: ${fields.length}/${maxFields}`); } const id = fields.shift(); if (!id) { throw new Error("id cannot be undefined at this point."); } if (!idRegex.test(id)) { throw new TypeError(`id must satisfy ${idRegex}`); } let version; if (versionRegex.test(fields[0])) { const versionString = fields.shift(); if (!versionString) { throw new Error("paramString cannot be undefined at this point."); } version = parseInt(versionString.match(versionRegex)[1], 10); } let hash; let salt; if (b64Regex.test(fields[fields.length - 1])) { if (fields.length > 1 && b64Regex.test(fields[fields.length - 2])) { hash = asRuntimeBytes(base64ToBytes(fields.pop())); salt = asRuntimeBytes(base64ToBytes(fields.pop())); } else { salt = asRuntimeBytes(base64ToBytes(fields.pop())); } } let params; if (fields.length > 0) { const paramString = fields.pop(); if (!paramString) { throw new Error("paramString cannot be undefined at this point."); } const currentParams = keyValueStringToObject(paramString); if (!Object.keys(currentParams).every((p) => nameRegex.test(p))) { throw new TypeError(`params names must satisfy ${nameRegex}`); } const pv = Object.values(currentParams); if (!pv.every((v) => valueRegex.test(v))) { throw new TypeError(`params values must satisfy ${valueRegex}`); } const pk = Object.keys(currentParams); pk.forEach((k) => { currentParams[k] = decimalRegex.test(currentParams[k]) ? parseInt(currentParams[k], 10) : currentParams[k]; }); params = currentParams; } if (fields.length > 0) { throw new TypeError(`phcString contains unrecognized fields: ${fields}`); } const result = { id }; if (version) { result.version = version; } if (params) { result.params = params; } if (salt) { result.salt = salt; } if (hash) { result.hash = hash; } return result; } export { deserialize, serialize }; //# sourceMappingURL=index.mjs.map