paseto-ts
Version:
PASETO v4 (encrypt, decrypt, sign & verify) in TypeScript
94 lines (93 loc) • 3.48 kB
JavaScript
import { PasetoPayloadInvalid } from "./errors.js";
const decoder = new TextDecoder();
//
// These first two implementations are copied from the PASETO implementation guide:
// https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/01-Payload-Processing.md#enforcing-maximum-depth-without-parsing-the-json-string
//
/**
* Get the depth of a JSON string.
* @param {string} data JSON string
* @returns {number} Depth of the JSON string
* @see https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/01-Payload-Processing.md#enforcing-maximum-depth-without-parsing-the-json-string
*/
export function getJsonDepth(data) {
// Step 1
let stripped = data.replace(/\\"/g, '').replace(/\s+/g, '');
// Step 2
stripped = stripped.replace(/"[^"]+"([:,\}\]])/g, '$1');
// Step 3
stripped = stripped.replace(/[^\[\{\}\]]/g, '');
// Step 4
if (stripped.length === 0) {
return 1;
}
// Step 5
let previous = '';
let depth = 1;
// Step 6
while (stripped.length > 0 && stripped !== previous) {
previous = stripped;
stripped = stripped.replace(/({}|\[\])/g, '');
depth++;
}
// Step 7
if (stripped.length > 0) {
throw new Error(`Invalid JSON string`);
}
// Step 8
return depth;
}
/**
* Split the string based on the number of `":` pairs without a preceding
* backslash, then return the number of pieces it was broken into.
* @param {string} json JSON string
* @returns {number} Number of keys in the JSON string
* @see https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/01-Payload-Processing.md#enforcing-maximum-key-count-without-parsing-the-json-string
*/
export function countKeys(json) {
return json.split(/[^\\]":/).length;
}
/**
* Assert that a JSON string is within the maximum depth and key count
* @param {string} json JSON string
* @param {number} maxDepth Maximum depth of the JSON string
* @param {number} maxKeys Maximum number of keys in the JSON string
* @returns {boolean} true if the JSON string is valid, false otherwise
* @see https://github.com/paseto-standard/paseto-spec/blob/master/docs/02-Implementation-Guide/01-Payload-Processing.md#enforcing-maximum-depth-without-parsing-the-json-string
*/
export function assertJsonStringSize(json, { maxDepth = 10, maxKeys = 100 }) {
// assert json is a string
if (typeof json !== 'string') {
throw new PasetoPayloadInvalid(`JSON string must be a string (got ${typeof json}))`);
}
if (maxDepth || maxKeys) {
const depth = getJsonDepth(json);
const keys = countKeys(json);
if (maxDepth && maxDepth > 0 && depth > maxDepth) {
throw new PasetoPayloadInvalid(`JSON string exceeds maximum depth of ${maxDepth}`);
}
if (maxKeys && maxKeys > 0 && keys > maxKeys) {
throw new PasetoPayloadInvalid(`JSON string exceeds maximum number of keys of ${maxKeys}`);
}
}
return true;
}
/**
* Returns possible JSON object or string
* @param {string | Uint8Array} json Possible JSON string
* @returns {string | object} JSON object or string
*/
export function returnPossibleJson(json) {
if (!json) {
return '';
}
if (json instanceof Uint8Array) {
json = decoder.decode(json);
}
try {
return JSON.parse(json);
}
catch (e) {
return json;
}
}