react-native-avo-inspector
Version:
[](https://badge.fury.io/js/react-native-avo-inspector)
177 lines (176 loc) • 7.51 kB
JavaScript
;
/**
* 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;
}