/** * Common Utils Module based on the native common_utils_bridge C library. * * This module provides a collection of common utilities organized into namespaces. * It is designed as a stateless, singleton-like utility library. * * Features: * - `crypto`: Hashing (MD5, HMAC-MD5), symmetric encryption (AES), * and asymmetric encryption (RSA). * - `fs`: File system operations, such as converting files to/from Base64. * - `codec`: Data encoding and decoding functions (Hex, Base64, UTF-8, etc.). * - `random`: Generation of cryptographically secure random bytes and simple * random strings. * * Usage: * import dxCommonUtils from './dxCommonUtils.js'; * const md5Hash = dxCommonUtils.crypto.md5('hello'); * * Doc/Demo: https://github.com/DejaOS/DejaOS */ import { commonUtilsClass } from './libvbar-m-dxcommonutils.so' const dxCommonUtils = {}; // ----------- Constants & Enums ------------------- dxCommonUtils.AES_MODE = { ECB: 'ECB', CBC: 'CBC', CFB: 'CFB', OFB: 'OFB', CTR: 'CTR' }; dxCommonUtils.AES_KEY_SIZE = { BITS_128: 128, BITS_192: 192, BITS_256: 256 }; dxCommonUtils.AES_PADDING = { PKCS7: 'PKCS7', NONE: 'NoPadding' }; dxCommonUtils.RSA_KEY_SIZE = { BITS_1024: 1024, BITS_2048: 2048, BITS_4096: 4096 }; const utils = new commonUtilsClass(); // ----------- Crypto Namespace ------------------- dxCommonUtils.crypto = {}; dxCommonUtils.crypto.aes = {}; dxCommonUtils.crypto.rsa = {}; /** * Calculates MD5 hash of the input string. * @param {string|ArrayBuffer|Uint8Array} data - The data to hash. If a string is provided, it will be treated as UTF-8. * @returns {string} MD5 hash in hexadecimal format. */ dxCommonUtils.crypto.md5 = function (data) { const buffer = _normalizeDataToBuffer(data, false, 'Data'); return utils.md5(buffer); } /** * Calculates HMAC-MD5 hash using the provided key. * @param {string|ArrayBuffer|Uint8Array} data - The data to hash. If a string is provided, it will be treated as UTF-8. * @param {string|ArrayBuffer|Uint8Array} key - The secret key for HMAC. If a string is provided, it will be treated as UTF-8. * @returns {string} HMAC-MD5 hash in hexadecimal format. */ dxCommonUtils.crypto.hmacMd5 = function (data, key) { const dataBuffer = _normalizeDataToBuffer(data, false, 'Data'); const keyBuffer = _normalizeDataToBuffer(key, false, 'Key'); return utils.hmacMd5(dataBuffer, keyBuffer); } /** * Calculates a hash of the input data using the specified algorithm. * @param {string|ArrayBuffer|Uint8Array} data - The data to hash. If a string is provided, it will be treated as UTF-8. * @param {string} [hashAlgorithm='SHA-256'] - The hash algorithm to use (e.g., 'SHA-256', 'MD5', 'SHA1', 'SHA-384', 'SHA-512'). * @returns {string} The hash in hexadecimal format. */ dxCommonUtils.crypto.hash = function (data, hashAlgorithm = 'SHA-256') { const buffer = _normalizeDataToBuffer(data, false, 'Data'); return utils.hash(buffer, hashAlgorithm); } /** * Encrypts data using AES encryption. * @param {string|ArrayBuffer|Uint8Array} data - The data to encrypt. If a string is provided, it will be treated as UTF-8. * @param {string|ArrayBuffer|Uint8Array} key - The encryption key. Can be a hex string, an ArrayBuffer, or a Uint8Array. * @param {object} options - Encryption options. * @param {string} [options.mode='CBC'] - The AES mode, from dxCommonUtils.AES_MODE. * @param {number} [options.keySize=256] - The key size in bits (128, 192, or 256), from dxCommonUtils.AES_KEY_SIZE. * @param {string|ArrayBuffer|Uint8Array} [options.iv] - The initialization vector (required for non-ECB modes). This is ignored if useSalt is true. * @param {string} [options.padding='PKCS7'] - The padding scheme, from dxCommonUtils.AES_PADDING. Defaults to PKCS7. * @param {boolean} [options.useSalt=false] - If true, generates an OpenSSL-compatible salted ciphertext. The provided 'key' is used as a password (UTF-8 string) to derive the actual key and IV. * @returns {string} The encrypted data as a Base64 string. */ dxCommonUtils.crypto.aes.encrypt = function (data, key, options = {}) { // Set default options, including PKCS7 padding by default const finalOptions = Object.assign({ mode: dxCommonUtils.AES_MODE.CBC, keySize: 256, padding: dxCommonUtils.AES_PADDING.PKCS7, useSalt: false // Default to false for backward compatibility }, options); let keyBuffer; if (finalOptions.useSalt) { // In salted mode, a string key is treated as a password (UTF-8). keyBuffer = _normalizeDataToBuffer(key, false, 'Key'); } else { // In non-salted mode, a string key is treated as a raw key in hex format. keyBuffer = _normalizeHexInput(key, 'Key'); // Key length must be validated for non-salted keys. const expectedKeyLength = finalOptions.keySize / 8; if (keyBuffer.byteLength !== expectedKeyLength) { throw new Error(`Key length must be ${expectedKeyLength} bytes for ${finalOptions.keySize}-bit AES`); } } // Validate mode and keySize if (!Object.values(dxCommonUtils.AES_MODE).includes(finalOptions.mode)) { throw new Error("options.mode must be one of dxCommonUtils.AES_MODE values"); } if (!Object.values(dxCommonUtils.AES_KEY_SIZE).includes(finalOptions.keySize)) { throw new Error("options.keySize must be one of dxCommonUtils.AES_KEY_SIZE values"); } // Validate padding if (finalOptions.padding !== undefined) { if (typeof finalOptions.padding !== 'string' || !Object.values(dxCommonUtils.AES_PADDING).includes(finalOptions.padding)) { throw new Error("options.padding must be one of dxCommonUtils.AES_PADDING values"); } if (finalOptions.padding === dxCommonUtils.AES_PADDING.NONE) { const dataBuffer = _normalizeDataToBuffer(data, false, 'Data'); if (dataBuffer.byteLength % 16 !== 0) { throw new Error("Data length must be a multiple of 16 bytes when using NoPadding"); } } } const ivBuffer = (finalOptions.iv && !finalOptions.useSalt) ? _normalizeHexInput(finalOptions.iv) : null; const dataBuffer = _normalizeDataToBuffer(data, false, 'Data'); const encrypted = utils.aesEncrypt(dataBuffer, keyBuffer, { ...finalOptions, iv: ivBuffer }); if (encrypted === null) { throw new Error("AES encryption failed. Check parameters."); } return dxCommonUtils.codec.arrayBufferToBase64(encrypted); }; /** * Decrypts data using AES encryption. * Note: This function automatically handles OpenSSL's "Salted__" format if present in the encrypted data. * @param {string|ArrayBuffer|Uint8Array} encryptedData - The encrypted data. If a string, it is assumed to be Base64. * @param {string|ArrayBuffer|Uint8Array} key - The decryption key. If a string, it will be treated as a password (UTF-8) for salted data, or as a hex string for non-salted data. * @param {object} options - Decryption options. * @param {string} [options.mode='CBC'] - The AES mode, from dxCommonUtils.AES_MODE. * @param {number} [options.keySize=256] - The key size in bits (128, 192, or 256), from dxCommonUtils.AES_KEY_SIZE. This is also used for key derivation in "Salted__" format. * @param {string|ArrayBuffer|Uint8Array} [options.iv] - The initialization vector (required for non-ECB modes and non-salted data). If a string, it must be Hex. * @param {string} [options.padding='PKCS7'] - The padding scheme, from dxCommonUtils.AES_PADDING. Defaults to PKCS7. * @returns {ArrayBuffer|null} The decrypted data as an ArrayBuffer. Returns null on decryption failure (e.g., bad key or padding). */ dxCommonUtils.crypto.aes.decrypt = function (encryptedData, key, options = {}) { // Set default options, including PKCS7 padding by default const finalOptions = Object.assign({ mode: dxCommonUtils.AES_MODE.CBC, keySize: 256, padding: dxCommonUtils.AES_PADDING.PKCS7 }, options); // Validate mode and keySize if (!Object.values(dxCommonUtils.AES_MODE).includes(finalOptions.mode)) { throw new Error("options.mode must be one of dxCommonUtils.AES_MODE values"); } if (!Object.values(dxCommonUtils.AES_KEY_SIZE).includes(finalOptions.keySize)) { throw new Error("options.keySize must be one of dxCommonUtils.AES_KEY_SIZE values"); } // Validate padding if (finalOptions.padding !== undefined) { if (typeof finalOptions.padding !== 'string' || !Object.values(dxCommonUtils.AES_PADDING).includes(finalOptions.padding)) { throw new Error("options.padding must be one of dxCommonUtils.AES_PADDING values"); } } const encryptedDataBuffer = _normalizeDataToBuffer(encryptedData, true); // true for base64 // Auto-detect if the data is in OpenSSL's "Salted__" format let isSalted = false; if (encryptedDataBuffer.byteLength >= 16) { const header = new Uint8Array(encryptedDataBuffer, 0, 8); // "Salted__" in ASCII: 83 97 108 116 101 100 95 95 const saltedHeader = new Uint8Array([83, 97, 108, 116, 101, 100, 95, 95]); isSalted = header.every((value, index) => value === saltedHeader[index]); } let keyBuffer; if (isSalted) { // If data is salted, a string key is treated as a password (UTF-8). keyBuffer = _normalizeDataToBuffer(key, false, 'Key'); } else { // If data is not salted, a string key is treated as a raw key in hex format. keyBuffer = _normalizeHexInput(key, 'Key'); // Key length must be validated for non-salted keys. const expectedKeyLength = finalOptions.keySize / 8; if (keyBuffer.byteLength !== expectedKeyLength) { throw new Error(`Key length must be ${expectedKeyLength} bytes for ${finalOptions.keySize}-bit AES`); } } const ivBuffer = finalOptions.iv ? _normalizeHexInput(finalOptions.iv) : null; return utils.aesDecrypt(encryptedDataBuffer, keyBuffer, { ...finalOptions, iv: ivBuffer }); }; /** * Convenience method for AES-256-CBC encryption with automatic IV generation. * The key is treated as a Hex string. The plaintext data is treated as a UTF-8 string. * @param {string} data - The UTF-8 data to encrypt. * @param {string|ArrayBuffer|Uint8Array} key - The encryption key (32 bytes). If a string, it must be Hex. * @returns {{encrypted: string, iv: string}} Object containing Base64 encrypted data and the generated IV as a hex string. */ dxCommonUtils.crypto.aes.encryptWithRandomIV = function (data, key) { if (typeof data !== 'string') { throw new Error('Data must be a UTF-8 string for this convenience function.'); } const keyBuffer = _normalizeHexInput(key, 'Key'); if (keyBuffer.byteLength !== 32) { throw new Error('Key must be 32 bytes for AES-256'); } // 1. Generate 16 random bytes, returned directly as a hex string. const ivHex = dxCommonUtils.random.getBytes(16); // 2. Encrypt using the main AES function. const encrypted = dxCommonUtils.crypto.aes.encrypt(data, keyBuffer, { mode: 'CBC', keySize: 256, iv: ivHex }); // 3. Return the encrypted data and the hex-encoded IV. return { encrypted: encrypted, iv: ivHex }; } /** * Generates a new RSA key pair. * @param {number} [bits=2048] - Key size in bits: 1024, 2048, or 4096. * @returns {object} Object containing privateKey and publicKey in PEM format. */ dxCommonUtils.crypto.rsa.generateKeyPair = function (bits = 2048) { if (![1024, 2048, 4096].includes(bits)) { throw new Error('RSA key size must be 1024, 2048, or 4096 bits'); } return utils.generateRsaKeyPair(bits); } /** * Encrypts data using RSA public key. * @param {string|ArrayBuffer|Uint8Array} data - The data to encrypt. If a string is provided, it will be treated as UTF-8. * @param {string} publicKey - PEM formatted RSA public key. * @returns {string} Base64 encoded encrypted data. */ dxCommonUtils.crypto.rsa.encrypt = function (data, publicKey) { const dataBuffer = _normalizeDataToBuffer(data, false, 'Data'); if (typeof publicKey !== 'string') { throw new Error('Public key must be a PEM string'); } if (!publicKey.includes('-----BEGIN PUBLIC KEY-----')) { throw new Error('Public key must be in PEM format'); } return utils.rsaEncrypt(dataBuffer, publicKey); } /** * Decrypts RSA encrypted data using private key. * @param {string} encryptedData - Base64 encoded encrypted data. * @param {string} privateKey - PEM formatted RSA private key. * @returns {ArrayBuffer|null} The decrypted data as an ArrayBuffer. Returns null on decryption failure. */ dxCommonUtils.crypto.rsa.decrypt = function (encryptedData, privateKey) { if (typeof encryptedData !== 'string') { throw new Error('Encrypted data must be a Base64 string'); } if (typeof privateKey !== 'string') { throw new Error('Private key must be a PEM string'); } if (!privateKey.includes('-----BEGIN PRIVATE KEY-----')) { throw new Error('Private key must be in PEM format'); } // Decode the Base64 input to an ArrayBuffer before passing to native code. const encryptedBuffer = dxCommonUtils.codec.base64ToArrayBuffer(encryptedData); // Pass the privateKey string directly, as the C layer expects a PEM string. const decryptedBuffer = utils.rsaDecrypt(encryptedBuffer, privateKey); if (!decryptedBuffer) { // Return null instead of throwing to match AES behavior return null; } return decryptedBuffer; // Return the raw ArrayBuffer }; /** * Creates a digital signature for data using an RSA private key. * This is the core function needed for standards like JWT (RS256/RS384/RS512). * @param {string|ArrayBuffer|Uint8Array} data - The data to sign. If a string, it will be treated as UTF-8. * @param {string} privateKey - The PEM formatted RSA private key. * @param {string} [hashAlgorithm='SHA-256'] - The hash algorithm to use (e.g., 'SHA-256', 'SHA-384', 'SHA-512'). * @returns {string} The signature as a Base64 encoded string. */ dxCommonUtils.crypto.rsa.sign = function (data, privateKey, hashAlgorithm = 'SHA-256') { const dataBuffer = _normalizeDataToBuffer(data, false, 'Data'); if (typeof privateKey !== 'string' || !privateKey.includes('-----BEGIN PRIVATE KEY-----')) { throw new Error('Private key must be a PEM string'); } return utils.rsaSign(dataBuffer, privateKey, hashAlgorithm); }; /** * Verifies a digital signature using an RSA public key. * This is the counterpart to `rsa.sign` and is used to validate signatures like those in JWT. * @param {string|ArrayBuffer|Uint8Array} data - The original, unsigned data. * @param {string|ArrayBuffer|Uint8Array} signature - The signature to verify. If a string, it must be Base64 encoded. * @param {string} publicKey - The PEM formatted RSA public key. * @param {string} [hashAlgorithm='SHA-256'] - The hash algorithm used for signing (e.g., 'SHA-256', 'SHA-384', 'SHA-512'). * @returns {boolean} True if the signature is valid, otherwise false. */ dxCommonUtils.crypto.rsa.verify = function (data, signature, publicKey, hashAlgorithm = 'SHA-256') { const dataBuffer = _normalizeDataToBuffer(data, false, 'Data'); // The C layer expects a raw ArrayBuffer, so we decode the Base64 signature here in JS. const signatureBuffer = _normalizeDataToBuffer(signature, true, 'Signature'); if (typeof publicKey !== 'string' || !publicKey.includes('-----BEGIN PUBLIC KEY-----')) { throw new Error('Public key must be a PEM string'); } return utils.rsaVerify(dataBuffer, signatureBuffer, publicKey, hashAlgorithm); }; /** * Parses a PEM formatted X.509 certificate and returns its details. * @param {string} pemString - The certificate content in PEM format. * @returns {object} An object containing certificate details: * - serialNumber (string) * - issuer (string) * - subject (string) * - validFrom (string) * - validTo (string) * - publicKey (string, in PEM format) */ dxCommonUtils.crypto.parsePEM = function (pemString) { if (typeof pemString !== 'string' || !pemString.includes('-----BEGIN CERTIFICATE-----')) { throw new Error('Input must be a PEM formatted certificate string'); } return utils.parsePEMCertificate(pemString); }; /** * Verifies if a certificate was signed by a given Certificate Authority (CA). * @param {string} certPEM - The certificate to verify, in PEM format. * @param {string} caCertPEM - The CA's certificate, in PEM format. * @returns {boolean} True if the certificate is signed by the CA. * @throws {Error} If the native verification fails due to parsing errors or signature mismatch. */ dxCommonUtils.crypto.verifyCertificate = function (certPEM, caCertPEM) { if (typeof certPEM !== 'string' || !certPEM.includes('-----BEGIN CERTIFICATE-----')) { throw new Error('Input certPEM must be a PEM formatted certificate string'); } if (typeof caCertPEM !== 'string' || !caCertPEM.includes('-----BEGIN CERTIFICATE-----')) { throw new Error('Input caCertPEM must be a PEM formatted certificate string'); } return utils.verifyCertificate(certPEM, caCertPEM); }; // ===================================================================================== // == FS (File System) Namespace ===================================================== // ===================================================================================== dxCommonUtils.fs = {}; /** * Reads the entire content of a file and returns it as a Base64 encoded string. * @memberof dxCommonUtils.fs * @param {string} filePath - The path to the file. * @returns {string} The Base64 encoded content of the file. */ dxCommonUtils.fs.fileToBase64 = function (filePath) { if (typeof filePath !== 'string' || filePath.length === 0) { throw new Error("filePath must be a non-empty string."); } return utils.fileToBase64(filePath); } /** * Decodes a Base64 string and writes the binary data to a file. * This will overwrite the file if it already exists. * @memberof dxCommonUtils.fs * @param {string} filePath - The path to the file to be written. * @param {string} base64String - The Base64 encoded data. * @returns {boolean} Returns true on success. */ dxCommonUtils.fs.base64ToFile = function (filePath, base64String) { if (typeof filePath !== 'string' || filePath.length === 0) { throw new Error("filePath must be a non-empty string."); } if (typeof base64String !== 'string') { throw new Error("base64String must be a string."); } return utils.base64ToFile(filePath, base64String); } // ===================================================================================== // == Random Namespace =============================================================== // ===================================================================================== dxCommonUtils.random = {}; /** * Generates cryptographically secure random bytes. * @param {number} length - Number of bytes to generate. * @returns {string} Random bytes as a hex string. */ dxCommonUtils.random.getBytes = function (length) { if (typeof length !== 'number' || length <= 0) { throw new Error('Length must be a positive number'); } return utils.generateRandomBytes(length); } /** * Generates a non-cryptographically secure random string from a given charset. * @param {number} length - The length of the string to generate. * @param {string} [charset] - The set of characters to use. Defaults to alphanumeric. * @returns {string} The generated random string. */ dxCommonUtils.random.getStr = function (length, charset) { if (typeof length !== 'number' || length <= 0) { throw new Error('Length must be a positive number'); } const charSet = charset || 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; let result = ''; const charSetLength = charSet.length; for (let i = 0; i < length; i++) { result += charSet.charAt(Math.floor(Math.random() * charSetLength)); } return result; } // ----------- Codec Namespace ------------------- dxCommonUtils.codec = {}; /** * Hexadecimal to byte array eg: 313233616263->[49,50,51,97,98,99] * @param {string} str A hexadecimal string in lowercase with no space in between * @returns {number[]} Digital numbers */ dxCommonUtils.codec.hexToBytes = function (str) { if (str === undefined || str === null || (typeof str) != 'string' || str.length < 1) { throw new Error("dxCommonUtils.codec.hexToBytes:'str' parameter should not be empty") } if (!/^[0-9a-fA-F]+$/.test(str) || str.length % 2 !== 0) { throw new Error("dxCommonUtils.codec.hexToBytes: 'str' parameter must be a valid hex string with an even length"); } let regex = /.{2}/g; let arr = str.match(regex); return arr.map(item => parseInt(item, 16)); } /** * Byte array to hexadecimal eg: [49,50,51,97,98,99] ->313233616263 * @param {number[]} numbers Numeric array * @returns {string} A hexadecimal string in lowercase with no space in between */ dxCommonUtils.codec.bytesToHex = function (numbers) { const hexArray = numbers.map(num => num.toString(16).padStart(2, '0').toLowerCase()); const hexString = hexArray.join(''); return hexString; } /** * Hexadecimal to string conversion eg: 313233616263->123abc * @description WARNING: This function only works for single-byte character sets (like ASCII). * For multi-byte characters (like Chinese), please use `codec.utf8HexToStr`. * @param {string} str The hexadecimal string to be converted * @returns {string} The real string */ dxCommonUtils.codec.hexToStr = function (str) { let regex = /.{2}/g; let arr = str.match(regex); arr = arr.map(item => String.fromCharCode(parseInt(item, 16))); return arr.join(""); } /** * Convert a string to a UTF-8 encoded hexadecimal string * @param {string} str * @returns {string} */ dxCommonUtils.codec.strToUtf8Hex = function (str) { const bytes = []; for (let i = 0; i < str.length; i++) { let code = str.charCodeAt(i); if (code < 0x80) { bytes.push(code); } else if (code < 0x800) { bytes.push(0xc0 | (code >> 6), 0x80 | (code & 0x3f)); } else if (code < 0xd800 || code >= 0xe000) { bytes.push(0xe0 | (code >> 12), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f)); } else { i++; code = 0x10000 + (((code & 0x3ff) << 10) | (str.charCodeAt(i) & 0x3ff)); bytes.push( 0xf0 | (code >> 18), 0x80 | ((code >> 12) & 0x3f), 0x80 | ((code >> 6) & 0x3f), 0x80 | (code & 0x3f) ); } } return dxCommonUtils.codec.bytesToHex(bytes); } /** * Convert the hexadecimal string of utf-8 passed over to a string * @param {string} hex Hexadecimal string * @returns {string} The real string */ dxCommonUtils.codec.utf8HexToStr = function (hex) { let array = dxCommonUtils.codec.hexToBytes(hex) var out, i, len, c; var char2, char3; out = ""; len = array.length; i = 0; while (i < len) { c = array[i++]; switch (c >> 4) { case 0: case 1: case 2: case 3: case 4: case 5: case 6: case 7: out += String.fromCharCode(c); break; case 12: case 13: char2 = array[i++]; out += String.fromCharCode(((c & 0x1F) << 6) | (char2 & 0x3F)); break; case 14: char2 = array[i++]; char3 = array[i++]; out += String.fromCharCode(((c & 0x0F) << 12) | ((char2 & 0x3F) << 6) | ((char3 & 0x3F) << 0)); break; } } return out; } /** * Convert string to hexadecimal eg: 123abc ->313233616263 * @description WARNING: This function only works for single-byte character sets (like ASCII). * For multi-byte characters (like Chinese), please use `codec.strToUtf8Hex`. * @param {string} str The string to be converted * @returns {string} Hexadecimal string */ dxCommonUtils.codec.strToHex = function (str) { if (str === undefined || str === null || typeof (str) != "string") { return null } let val = ""; for (let i = 0; i < str.length; i++) { val += str.charCodeAt(i).toString(16).padStart(2, '0'); } return val } /** * Convert small format to decimal eg: 001001->69632 * @param {string} hexString A hexadecimal string in lowercase with no space in between * @returns {number} Decimal number */ dxCommonUtils.codec.leToDecimal = function (hexString) { let reversedHexString = hexString .match(/.{2}/g) .reverse() .join(""); let decimal = parseInt(reversedHexString, 16); return decimal; } /** * Convert decimal numbers to hexadecimal small format strings * @param {number} decimalNumber Decimal digit * @param {number} byteSize Generate the number of bytes, defaults to 2 * @returns {string} Hexadecimal small format string */ dxCommonUtils.codec.decimalToLeHex = function (decimalNumber, byteSize) { if (decimalNumber === undefined || decimalNumber === null || (typeof decimalNumber) != 'number') { throw new Error("dxCommonUtils.codec.decimalToLeHex:'decimalNumber' parameter should be number") } if (byteSize === undefined || byteSize === null || (typeof byteSize) != 'number' || byteSize <= 0) { byteSize = 2 } const littleEndianBytes = []; for (let i = 0; i < byteSize; i++) { littleEndianBytes.push(decimalNumber & 0xFF); decimalNumber >>= 8; } const littleEndianHex = littleEndianBytes .map((byte) => byte.toString(16).padStart(2, '0')) .join(''); return littleEndianHex; } /** * Convert a hexadecimal string to an ArrayBuffer * @param {string} hexString The hexadecimal string to be converted * @returns {ArrayBuffer} Converted ArrayBuffer */ dxCommonUtils.codec.hexToArrayBuffer = function (hexString) { return dxCommonUtils.codec.hexToUint8Array(hexString).buffer; } /** * Convert hexadecimal string to Uint8Array * @param {string} hexString The hexadecimal string to be converted * @returns {Uint8Array} Uint8Array object */ dxCommonUtils.codec.hexToUint8Array = function (hexString) { if (hexString === undefined || hexString === null || (typeof hexString) != 'string' || hexString.length <= 0) { throw new Error("dxCommonUtils.codec.hexToUint8Array:'hexString' parameter should not be empty") } if (!/^[0-9a-fA-F]+$/.test(hexString) || hexString.length % 2 !== 0) { throw new Error("dxCommonUtils.codec.hexToUint8Array: 'hexString' parameter must be a valid hex string with an even length"); } let byteString = hexString.match(/.{1,2}/g); let byteArray = byteString.map(function (byte) { return parseInt(byte, 16); }); let buffer = new Uint8Array(byteArray); return buffer; } /** * Convert ArrayBuffer to hexadecimal string format * @param {ArrayBuffer} buffer * @returns {string} A hexadecimal string in lowercase with no space in between */ dxCommonUtils.codec.arrayBufferToHex = function (buffer) { return dxCommonUtils.codec.uint8ArrayToHex(new Uint8Array(buffer)) } /** * Convert Uint8Array to hexadecimal string format * @param {Uint8Array} array * @returns {string} A hexadecimal string in lowercase with no space in between */ dxCommonUtils.codec.uint8ArrayToHex = function (array) { let hexString = ''; for (let i = 0; i < array.length; i++) { const byte = array[i].toString(16).padStart(2, '0'); hexString += byte; } return hexString } /** * Decodes a Base64 string into an ArrayBuffer. * This implementation is robust and handles padding correctly. * @memberof dxCommonUtils.codec * @param {string} b64 - The Base64 encoded string. * @returns {ArrayBuffer} The decoded data as an ArrayBuffer. */ dxCommonUtils.codec.base64ToArrayBuffer = function (b64) { if (typeof b64 !== 'string') { throw new Error("Input must be a Base64 string."); } // First, clean up any whitespace from the input string. const b64_clean = b64.replace(/\s+/g, ""); // Regex inspired by the js-base64 library to validate the structure of the string. // This provides a fast failure for malformed strings. const b64re = /^(?:[A-Za-z0-9+/]{4})*?(?:[A-Za-z0-9+/]{2}(?:==)?|[A-Za-z0-9+/]{3}=?)?$/; if (!b64re.test(b64_clean)) { throw new Error("Input is not a valid Base64 string (malformed)."); } const B64_MAP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; const len = b64_clean.length; // This calculation correctly determines the output byte length let byte_len = len * 3 / 4; if (b64_clean.slice(-2) === '==') { byte_len -= 2; } else if (b64_clean.slice(-1) === '=') { byte_len -= 1; } const arr = new Uint8Array(byte_len); let j = 0; for (let i = 0; i < len; i += 4) { // Get the index of each base64 character. // It will be -1 for '=' or any other invalid character. const c1 = B64_MAP.indexOf(b64_clean[i]); const c2 = B64_MAP.indexOf(b64_clean[i + 1]); const c3 = B64_MAP.indexOf(b64_clean[i + 2]); const c4 = B64_MAP.indexOf(b64_clean[i + 3]); // Reconstruct the original 3 bytes from the 4 base64 character indices. const b1 = (c1 << 2) | (c2 >> 4); const b2 = ((c2 & 15) << 4) | (c3 >> 2); const b3 = ((c3 & 3) << 6) | c4; // Write the first byte, which is always present. arr[j++] = b1; // Write the second byte if the third base64 character was not a padding character. if (c3 !== -1) { arr[j++] = b2; } // Write the third byte if the fourth base64 character was not a padding character. if (c4 !== -1) { arr[j++] = b3; } } return arr.buffer; } /** * Encodes an ArrayBuffer into a Base64 string. * This implementation is robust and handles padding correctly. * @memberof dxCommonUtils.codec * @param {ArrayBuffer} buffer - The ArrayBuffer to encode. * @returns {string} The Base64 encoded string. */ dxCommonUtils.codec.arrayBufferToBase64 = function (buffer) { const B64_MAP = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; const bytes = new Uint8Array(buffer); const len = bytes.byteLength; let b64 = ""; for (let i = 0; i < len; i += 3) { const b1 = bytes[i]; const b2 = bytes[i + 1]; const b3 = bytes[i + 2]; const c1 = b1 >> 2; const c2 = ((b1 & 3) << 4) | (b2 >> 4); const c3 = ((b2 & 15) << 2) | (b3 >> 6); const c4 = b3 & 63; b64 += B64_MAP[c1]; b64 += B64_MAP[c2]; if (i + 1 >= len) { b64 += "=="; } else { b64 += B64_MAP[c3]; if (i + 2 >= len) { b64 += "="; } else { b64 += B64_MAP[c4]; } } } return b64; } /** * Calculates the BCC (Block Check Character / XOR Checksum) of the input data. * @memberof dxCommonUtils.codec * @param {string|ArrayBuffer|Uint8Array} data - The data to calculate the checksum for. If a string, it will be treated as UTF-8. * @returns {number} The calculated 8-bit BCC value (0-255). */ dxCommonUtils.codec.bcc = function (data) { const buffer = _normalizeDataToBuffer(data, false, 'Data'); return utils.calculateBcc(buffer); } /** * Get disk space usage * @returns {object} * -total: The total disk space in MB. * -used: The used disk space in MB. * -free: The available disk space in MB. */ dxCommonUtils.getDiskStats = function () { return utils.getDiskStats(); } /** * @private * Internal helper to normalize various data inputs into an ArrayBuffer. * This function is crucial for ensuring that the native C layer receives data in a consistent format. * @param {string|ArrayBuffer|Uint8Array} data The input data. * @param {boolean} [isBase64=false] - If the input is a string, specifies if it's Base64 encoded. Defaults to false (UTF-8). * @param {string} [paramName='Input'] - The name of the parameter for error messages. * @returns {ArrayBuffer} */ function _normalizeDataToBuffer(data, isBase64 = false, paramName = 'Input') { if (typeof data === 'string') { if (isBase64) { return dxCommonUtils.codec.base64ToArrayBuffer(data); } else { // Treat as a UTF-8 string by converting to hex and then to ArrayBuffer. return dxCommonUtils.codec.hexToArrayBuffer(dxCommonUtils.codec.strToUtf8Hex(data)); } } else if (data instanceof ArrayBuffer) { return data; } else if (data instanceof Uint8Array) { // Correctly handle views on larger ArrayBuffers by creating a copy of the viewed section. // If it's not a view, slice() will create a copy of the buffer. return data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength); } else { throw new Error(`${paramName} must be a string, ArrayBuffer, or Uint8Array`); } } /** * @private * Internal helper to normalize crypto inputs, converting Hex strings to ArrayBuffer. * @param {string|ArrayBuffer|Uint8Array} data The input data. * @param {string} paramName The name of the parameter for error messages. * @returns {ArrayBuffer} */ function _normalizeHexInput(data, paramName = 'Input') { if (typeof data === 'string') { return dxCommonUtils.codec.hexToArrayBuffer(data); } else if (data instanceof ArrayBuffer) { return data; } else if (data instanceof Uint8Array) { return data.buffer; } else { throw new Error(`${paramName} must be a hex string, ArrayBuffer, or Uint8Array`); } } /** * Internal helper to convert an ArrayBuffer to a UTF-8 string. * This is a workaround for environments that lack a built-in TextDecoder. * @param {ArrayBuffer} buffer - The ArrayBuffer to convert. * @returns {string} The UTF-8 string. */ function _arrayBufferToUtf8String(buffer) { if (!buffer || buffer.byteLength === 0) { return ""; } // Use the existing, environment-safe codec functions const hex = dxCommonUtils.codec.arrayBufferToHex(buffer); return dxCommonUtils.codec.utf8HexToStr(hex); } /** * Gets the native common utils client object. * @returns {Object} The native client object. */ dxCommonUtils.getNative = function () { return utils; } export default dxCommonUtils;