diff --git a/index.js b/index.js index 059cde1..5791231 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,6 @@ const nacl = require('tweetnacl'); const blake = require('blakejs'); -const stringify = require('json-stable-stringify') +const {safeJsonParse, safeStringify} = require('./utils/stringify') nacl.util = require('tweetnacl-util'); // Utility functions for encoding/decoding let HASH_KEY; @@ -46,7 +46,7 @@ function hashObj (obj, removeSign = false) { throw TypeError('Input must be an object.') } function performHash (obj) { - let input = stringify(obj) + let input = safeStringify(obj) let hashed = hash(input) return hashed } @@ -150,7 +150,7 @@ function signObj(obj, secretKey, publicKey) { } // Step 1: Hash the object using the hashObj function - let objStr = stringify(obj); + let objStr = safeStringify(obj); let hashed = hash(objStr); // Hash the object and return a hex string // Step 2: Sign the hashed object @@ -213,3 +213,5 @@ exports.sign = sign; exports.signObj = signObj; exports.verify = verify; exports.verifyObj = verifyObj; +exports.safeStringify = safeStringify; +exports.safeJsonParse = safeJsonParse; diff --git a/tests/stringify.test.js b/tests/stringify.test.js new file mode 100644 index 0000000..9f6f2ae --- /dev/null +++ b/tests/stringify.test.js @@ -0,0 +1,24 @@ +const stringify = require('../utils/stringify') +const { safeJsonParse, safeStringify } = stringify + +// Generate test data in Node.js +function generateNodeData() { + const data = { + // Generate a random byteArray using crypto + data: new Uint8Array([1, 2, 3]), + // Create a large BigInt (similar to what you might use for cryptography) + nonce: BigInt("0x123456789abcdef123456789abcdef"), + // Add some normal data too + timestamp: Date.now(), + message: "Hello from Node.js!" + }; + return data; +} + +const data = generateNodeData() + +const stringified = safeStringify(data) +console.log('Stringified data:', stringified) + +const parsed = safeJsonParse(stringified) +console.log('Parsed data:', parsed) \ No newline at end of file diff --git a/utils/stringify.js b/utils/stringify.js new file mode 100644 index 0000000..58721a5 --- /dev/null +++ b/utils/stringify.js @@ -0,0 +1,192 @@ +(function(root, factory) { + if (typeof define === 'function' && define.amd) { + // AMD + define([], factory); + } else if (typeof module === 'object' && module.exports) { + // CommonJS + module.exports = factory(); + } else { + // Browser globals + root.stringifyUtils = factory(); + } +}(typeof self !== 'undefined' ? self : this, function() { + + const objToString = Object.prototype.toString + + const objKeys = Object.keys || function(obj) { + const keys = [] + for (const name in obj) { + keys.push(name) + } + return keys + } + + const isObject = (val) => { + if (val === null) { + return false + } + if (Array.isArray(val)) { + return false + } + return typeof val === 'function' || typeof val === 'object' + } + + const isUint8Array = (val) => { + return val instanceof Uint8Array + } + + const uint8ArrayToBase64 = (uint8Array) => { + let binary = '' + for (let i = 0; i < uint8Array.length; i++) { + binary += String.fromCharCode(uint8Array[i]) + } + return btoa(binary) + } + + const base64ToUint8Array = (base64) => { + 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 safeStringify(val, options = { bufferEncoding: 'base64' }) { + const returnVal = stringifyHelper(val, false, options) + if (returnVal !== undefined) { + return '' + returnVal + } + } + + function safeJsonParse(value) { + return JSON.parse(value, typeReviver) + } + + function isBufferValue(toStr, val) { + return ( + toStr === '[object Object]' && + objKeys(val).length === 2 && + objKeys(val).includes('type') && + val['type'] === 'Buffer' + ) + } + + function stringifyHelper(val, isArrayProp, options = { bufferEncoding: 'base64' }) { + if (options === null) options = { bufferEncoding: 'base64' } + let i, max, str, keys, key, propVal, toStr + + if (val === true) { + return 'true' + } + if (val === false) { + return 'false' + } + + switch (typeof val) { + case 'object': + if (val === null) { + return null + } else if ('toJSON' in val && typeof val.toJSON === 'function') { + return stringifyHelper(val.toJSON(), isArrayProp, options) + } else { + toStr = objToString.call(val) + if (toStr === '[object Array]') { + str = '[' + max = val.length - 1 + for (i = 0; i < max; i++) { + str += stringifyHelper(val[i], true) + ',' + } + if (max > -1) { + str += stringifyHelper(val[i], true) + } + return str + ']' + } else if (isUint8Array(val)) { + return JSON.stringify({ + value: uint8ArrayToBase64(val), + dataType: 'u8ab' + }) + } else if ( + options.bufferEncoding !== 'none' && + isBufferValue(toStr, val) + ) { + switch (options.bufferEncoding) { + case 'base64': + return JSON.stringify({ + value: uint8ArrayToBase64(new Uint8Array(val['data'])), + dataType: 'bb' + }) + } + } else if (toStr === '[object Object]') { + keys = objKeys(val).sort() + max = keys.length + str = '' + i = 0 + while (i < max) { + key = keys[i] + propVal = stringifyHelper(val[key], false, options) + if (propVal !== undefined) { + if (str) { + str += ',' + } + str += JSON.stringify(key) + ':' + propVal + } + i++ + } + return '{' + str + '}' + } else { + return JSON.stringify(val) + } + } + case 'function': + case 'undefined': + return isArrayProp ? null : undefined + case 'string': + return JSON.stringify(val) + case 'bigint': + return JSON.stringify({ dataType: 'bi', value: val.toString(16) }) + default: + return isFinite(val) ? val : null + } + } + + function getUint8ArrayFromField(input, encoding) { + switch (encoding) { + case 'base64': + return base64ToUint8Array(input.value) + default: + return new Uint8Array(input) + } + } + + function typeReviver(key, value) { + if (key === 'sig') return value + const originalObject = value + if ( + isObject(originalObject) && + Object.prototype.hasOwnProperty.call(originalObject, 'dataType') && + originalObject.dataType + ) { + if (originalObject.dataType === 'bb' || originalObject.dataType === 'u8ab') { + if (typeof originalObject.value !== 'string') { + return value + } + return originalObject.dataType === 'bb' + ? getUint8ArrayFromField(originalObject, 'base64') + : base64ToUint8Array(originalObject.value) + } else if (originalObject.dataType === 'bi') { + return BigInt('0x' + originalObject.value) + } else { + return value + } + } else { + return value + } + } + + // Return public API + return { + safeStringify, + safeJsonParse + } +})); \ No newline at end of file