UNPKG

react-native-avo-inspector

Version:

[![npm version](https://badge.fury.io/js/react-native-avo-inspector.svg)](https://badge.fury.io/js/react-native-avo-inspector)

177 lines (176 loc) 7.51 kB
"use strict"; /** * AvoEncryption — React Native ECIES encryption using @noble/ciphers. * * CRITICAL: React Native's Hermes runtime does NOT have crypto.subtle. * This module uses @noble/ciphers/aes gcm() for AES-GCM instead — fully synchronous. * Ephemeral key generation and ECDH use @noble/curves/p256. * * Wire format (MUST match all SDKs): * [0x00][65-byte ephemeral pubkey (uncompressed)][16-byte IV][16-byte auth tag][ciphertext] * → base64 * * Algorithm: * 1. Generate ephemeral P-256 key pair * 2. ECDH: ephemeral private key + recipient public key → shared secret (X-coordinate) * 3. KDF: SHA-256(shared secret) → 32-byte AES key * 4. AES-256-GCM encrypt with random 16-byte IV * 5. Serialize: version(1) + ephemeralPubKey(65) + IV(16) + authTag(16) + ciphertext * 6. Base64 encode */ Object.defineProperty(exports, "__esModule", { value: true }); exports.shouldEncrypt = shouldEncrypt; exports.encryptValue = encryptValue; exports.encryptEventProperties = encryptEventProperties; var p256_1 = require("@noble/curves/p256"); var aes_1 = require("@noble/ciphers/aes"); var sha256_1 = require("@noble/hashes/sha256"); var webcrypto_1 = require("@noble/ciphers/webcrypto"); /** * Converts a hex string to a Uint8Array. * Strips optional 0x/0X prefix (matching Android SDK behavior). */ function hexToBytes(hex) { if (hex.startsWith("0x") || hex.startsWith("0X")) { hex = hex.substring(2); } var bytes = new Uint8Array(hex.length / 2); for (var i = 0; i < hex.length; i += 2) { bytes[i / 2] = parseInt(hex.substring(i, i + 2), 16); } return bytes; } /** * Determines whether encryption should be performed. * * Truth table: * dev + key → true * staging + key → true * prod + key → false * dev + null → false * dev + empty → false */ function shouldEncrypt(env, publicEncryptionKey) { if (env === "prod") return false; if (publicEncryptionKey === null || publicEncryptionKey === undefined || publicEncryptionKey.trim().length === 0) { return false; } return true; } /** * Encrypts a value using ECIES (P-256 ECDH + AES-256-GCM). * Fully synchronous — no async/await. * * @param value - The plaintext string to encrypt (already JSON-stringified by caller if needed) * @param publicKeyHex - The recipient's P-256 public key as a hex string (uncompressed, 04...) * @returns Base64-encoded ciphertext in Avo ECIES wire format */ function encryptValue(value, publicKeyHex) { // Fail fast with a helpful message when running in React Native without the polyfill. // Node.js and browsers provide crypto natively; only Hermes (RN) lacks it. if (!process.env.BROWSER && typeof navigator !== "undefined" && navigator.product === "ReactNative" && (typeof globalThis.crypto === "undefined" || typeof globalThis.crypto.getRandomValues !== "function")) { throw new Error("crypto.getRandomValues is not available. " + "If you are using React Native, add 'react-native-get-random-values' to your project " + "and import it at the top of your entry file (index.js) before any other imports: " + "import 'react-native-get-random-values';"); } // 1. Parse recipient public key from hex var recipientPublicKeyBytes = hexToBytes(publicKeyHex); // 2. Generate ephemeral P-256 key pair var ephemeralPrivateKeyBytes = p256_1.p256.utils.randomPrivateKey(); var ephemeralPublicKeyBytes = p256_1.p256.getPublicKey(ephemeralPrivateKeyBytes, false // uncompressed = 65 bytes (0x04 + 32-byte X + 32-byte Y) ); // 3. ECDH: compute shared secret // getSharedSecret returns the full point; extract X-coordinate (last 32 bytes) var sharedSecretPoint = p256_1.p256.getSharedSecret(ephemeralPrivateKeyBytes, recipientPublicKeyBytes); var sharedSecret = sharedSecretPoint.slice(-32); // 4. KDF: SHA-256(sharedSecret) → 32-byte AES key var aesKey = (0, sha256_1.sha256)(sharedSecret); // 5. Generate random 16-byte IV var iv = (0, webcrypto_1.randomBytes)(16); // 6. AES-256-GCM encrypt var plaintext = new TextEncoder().encode(value); var aes = (0, aes_1.gcm)(aesKey, iv); var ciphertextWithTag = aes.encrypt(plaintext); // @noble/ciphers gcm() returns ciphertext + authTag concatenated // Split the LAST 16 bytes as the auth tag var authTagLength = 16; var ciphertextLength = ciphertextWithTag.length - authTagLength; var ciphertext = ciphertextWithTag.slice(0, ciphertextLength); var authTag = ciphertextWithTag.slice(ciphertextLength); // 7. Serialize: [Version 0x00 (1B)] + [EphemeralPubKey (65B)] + [IV (16B)] + [AuthTag (16B)] + [Ciphertext] var resultLength = 1 + ephemeralPublicKeyBytes.length + 16 + 16 + ciphertext.length; var result = new Uint8Array(resultLength); var offset = 0; result[0] = 0x00; // Version byte offset += 1; result.set(ephemeralPublicKeyBytes, offset); offset += ephemeralPublicKeyBytes.length; result.set(iv, offset); offset += 16; result.set(authTag, offset); offset += 16; result.set(ciphertext, offset); // 8. Base64 encode (platform-safe: avoids Buffer which is unavailable in Hermes/React Native) var binary = ""; for (var i = 0; i < result.length; i++) { binary += String.fromCharCode(result[i]); } return btoa(binary); } /** * Encrypts event property values for transmission. * * Rules: * - List-type properties are omitted entirely * - On encryption failure: console.warn, omit the property, continue * - Returns new array with encryptedPropertyValue set (propertyType/propertyName preserved) */ function encryptEventProperties(properties, eventProps, publicKeyHex) { var result = []; var encryptionFailed = false; for (var _i = 0, properties_1 = properties; _i < properties_1.length; _i++) { var prop = properties_1[_i]; // Omit list-type properties entirely if (prop.propertyType === "list") { continue; } // If encryption already failed for this batch, skip remaining properties // to avoid spamming the console with the same error if (encryptionFailed) { continue; } try { var value = eventProps[prop.propertyName]; var jsonValue = JSON.stringify(value === undefined ? null : value); var encrypted = encryptValue(jsonValue, publicKeyHex); var entry = { propertyName: prop.propertyName, propertyType: prop.propertyType, encryptedPropertyValue: encrypted, }; // Preserve children if present if (prop.children !== undefined) { entry.children = prop.children; } // Preserve validation fields if present if (prop.failedEventIds !== undefined) { entry.failedEventIds = prop.failedEventIds; } if (prop.passedEventIds !== undefined) { entry.passedEventIds = prop.passedEventIds; } result.push(entry); } catch (e) { encryptionFailed = true; console.warn("[Avo Inspector] Encryption failed: ".concat(e instanceof Error ? e.message : String(e), ". Remaining event properties will be omitted from the encrypted payload.")); } } return result; }