UNPKG

avo-inspector

Version:

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

273 lines (272 loc) 14 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * Tests for AvoEncryption (React Native - @noble/ciphers based, fully synchronous). * * Covers: * - shouldEncrypt() truth table * - Wire format structural tests * - Cross-SDK interop (TypeScript mirror of Java EncryptionInteropTestUtil) * - Encryption failure handling * - List-type property omission * - publicEncryptionKey in base body * - Prod negative test */ var p256_1 = require("@noble/curves/p256"); var aes_1 = require("@noble/ciphers/aes"); var sha256_1 = require("@noble/hashes/sha256"); var AvoEncryption_1 = require("../AvoEncryption"); // ========================================================================= // Helper: TypeScript mirror of Java EncryptionInteropTestUtil.decrypt // ========================================================================= function hexToBytes(hex) { 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; } function bytesToHex(bytes) { return Array.from(bytes) .map(function (b) { return b.toString(16).padStart(2, "0"); }) .join(""); } /** * Reference decryptor mirroring Java EncryptionInteropTestUtil.decrypt. * Uses @noble/curves + @noble/ciphers (same libs, but acts as independent verification). */ function referenceDecrypt(base64Encrypted, privateKeyHex) { var data = Buffer.from(base64Encrypted, "base64"); // Minimum length: 1 (version) + 65 (ephemeral pub key) + 16 (IV) + 16 (auth tag) + 1 (min ciphertext) if (data.length < 99) { throw new Error("Encrypted data too short: expected at least 99 bytes, got ".concat(data.length)); } // Step 2: Assert version byte if (data[0] !== 0x00) { throw new Error("Unsupported version byte: expected 0x00, got 0x".concat(data[0].toString(16).padStart(2, "0"))); } // Step 3: Extract ephemeral public key bytes [1..65] (65 bytes, uncompressed EC point) var ephemeralPubKeyBytes = new Uint8Array(data.slice(1, 66)); // Step 4: Extract IV [66..81] (16 bytes) var iv = new Uint8Array(data.slice(66, 82)); // Step 5: Extract auth tag [82..97] (16 bytes) var authTag = new Uint8Array(data.slice(82, 98)); // Step 6: Extract ciphertext [98..N] var ciphertext = new Uint8Array(data.slice(98)); // Step 7-8: ECDH with recipient private key -> sharedSecret var privateKeyBytes = hexToBytes(privateKeyHex); var sharedSecretPoint = p256_1.p256.getSharedSecret(privateKeyBytes, ephemeralPubKeyBytes); var sharedSecret = sharedSecretPoint.slice(-32); // X-coordinate // Step 9: KDF: SHA-256(sharedSecret) -> aesKey var aesKey = (0, sha256_1.sha256)(sharedSecret); // Step 10: AES/GCM decrypt // @noble/ciphers gcm expects ciphertext + authTag concatenated var ciphertextWithTag = new Uint8Array(ciphertext.length + authTag.length); ciphertextWithTag.set(ciphertext, 0); ciphertextWithTag.set(authTag, ciphertext.length); var aes = (0, aes_1.gcm)(aesKey, iv); var plainBytes = aes.decrypt(ciphertextWithTag); // Step 11: Return as UTF-8 string return new TextDecoder().decode(plainBytes); } // ========================================================================= // Generate a test key pair // ========================================================================= function generateTestKeyPair() { var privateKeyBytes = p256_1.p256.utils.randomPrivateKey(); var publicKeyBytes = p256_1.p256.getPublicKey(privateKeyBytes, false); return { privateKey: bytesToHex(privateKeyBytes).padStart(64, "0"), publicKey: bytesToHex(publicKeyBytes), }; } // ========================================================================= // Tests // ========================================================================= describe("AvoEncryption (React Native - @noble/ciphers)", function () { // ----------------------------------------------------------------------- // shouldEncrypt() truth table // ----------------------------------------------------------------------- describe("shouldEncrypt()", function () { test("dev + valid key = true", function () { expect((0, AvoEncryption_1.shouldEncrypt)("dev", "04abcdef1234")).toBe(true); }); test("staging + valid key = true", function () { expect((0, AvoEncryption_1.shouldEncrypt)("staging", "04abcdef1234")).toBe(true); }); test("prod + valid key = false", function () { expect((0, AvoEncryption_1.shouldEncrypt)("prod", "04abcdef1234")).toBe(false); }); test("dev + null key = false", function () { expect((0, AvoEncryption_1.shouldEncrypt)("dev", null)).toBe(false); }); test("dev + undefined key = false", function () { expect((0, AvoEncryption_1.shouldEncrypt)("dev", undefined)).toBe(false); }); test("dev + empty string key = false", function () { expect((0, AvoEncryption_1.shouldEncrypt)("dev", "")).toBe(false); }); test("dev + whitespace-only key = false", function () { expect((0, AvoEncryption_1.shouldEncrypt)("dev", " ")).toBe(false); }); }); // ----------------------------------------------------------------------- // Wire format structural test // ----------------------------------------------------------------------- describe("Wire format", function () { var keyPair = generateTestKeyPair(); test("base64Decode(output).length >= 99", function () { var encrypted = (0, AvoEncryption_1.encryptValue)("hello world", keyPair.publicKey); var decoded = Buffer.from(encrypted, "base64"); expect(decoded.length).toBeGreaterThanOrEqual(99); }); test("output[0] == 0x00 (version byte)", function () { var encrypted = (0, AvoEncryption_1.encryptValue)("hello world", keyPair.publicKey); var decoded = Buffer.from(encrypted, "base64"); expect(decoded[0]).toBe(0x00); }); test("output[1] == 0x04 (uncompressed point marker)", function () { var encrypted = (0, AvoEncryption_1.encryptValue)("hello world", keyPair.publicKey); var decoded = Buffer.from(encrypted, "base64"); expect(decoded[1]).toBe(0x04); }); test("encryptValue is synchronous (returns string, not Promise)", function () { var result = (0, AvoEncryption_1.encryptValue)("test", keyPair.publicKey); // If it were async, result would be a Promise, not a string expect(typeof result).toBe("string"); // Ensure it's NOT a Promise expect(result).not.toBeInstanceOf(Promise); }); test("different encryptions produce different output", function () { var enc1 = (0, AvoEncryption_1.encryptValue)("same", keyPair.publicKey); var enc2 = (0, AvoEncryption_1.encryptValue)("same", keyPair.publicKey); expect(enc1).not.toBe(enc2); }); }); // ----------------------------------------------------------------------- // Cross-SDK interop: RN-encrypted ciphertext decryptable by reference decryptor // ----------------------------------------------------------------------- describe("Cross-SDK interop", function () { var keyPair = generateTestKeyPair(); var standardPlaintexts = [ '"hello world"', "42", "3.14", "true", '"test string value"', ]; standardPlaintexts.forEach(function (plaintext) { test("RN encrypt -> reference decrypt: ".concat(plaintext), function () { var encrypted = (0, AvoEncryption_1.encryptValue)(plaintext, keyPair.publicKey); var decrypted = referenceDecrypt(encrypted, keyPair.privateKey); expect(decrypted).toBe(plaintext); }); }); test("round-trip with JSON-stringified object", function () { var obj = { key: "value", number: 123 }; var jsonStr = JSON.stringify(obj); var encrypted = (0, AvoEncryption_1.encryptValue)(jsonStr, keyPair.publicKey); var decrypted = referenceDecrypt(encrypted, keyPair.privateKey); expect(decrypted).toBe(jsonStr); }); }); // ----------------------------------------------------------------------- // encryptEventProperties // ----------------------------------------------------------------------- describe("encryptEventProperties()", function () { var keyPair = generateTestKeyPair(); test("encrypts string property values", function () { var properties = [ { propertyName: "name", propertyType: "string" }, ]; var eventProps = { name: "Alice" }; var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey); expect(result).toHaveLength(1); expect(result[0].propertyName).toBe("name"); expect(result[0].propertyType).toBe("string"); expect(result[0].encryptedPropertyValue).toBeDefined(); // Verify it decrypts correctly var decrypted = referenceDecrypt(result[0].encryptedPropertyValue, keyPair.privateKey); expect(JSON.parse(decrypted)).toBe("Alice"); }); test("encrypts int property values", function () { var properties = [ { propertyName: "age", propertyType: "int" }, ]; var eventProps = { age: 42 }; var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey); expect(result[0].encryptedPropertyValue).toBeDefined(); var decrypted = referenceDecrypt(result[0].encryptedPropertyValue, keyPair.privateKey); expect(JSON.parse(decrypted)).toBe(42); }); test("encrypts boolean property values", function () { var properties = [ { propertyName: "active", propertyType: "boolean" }, ]; var eventProps = { active: true }; var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey); expect(result[0].encryptedPropertyValue).toBeDefined(); var decrypted = referenceDecrypt(result[0].encryptedPropertyValue, keyPair.privateKey); expect(JSON.parse(decrypted)).toBe(true); }); test("omits list-type properties entirely", function () { var properties = [ { propertyName: "tags", propertyType: "list", children: ["string"] }, { propertyName: "name", propertyType: "string" }, ]; var eventProps = { tags: ["a", "b"], name: "Alice" }; var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey); // list property should be omitted entirely expect(result).toHaveLength(1); expect(result[0].propertyName).toBe("name"); }); test("encryption failure: console.warn, omit property, continue", function () { var warnSpy = jest.spyOn(console, "warn").mockImplementation(); var properties = [ { propertyName: "secret", propertyType: "string" }, { propertyName: "name", propertyType: "string" }, ]; var eventProps = { secret: "value1", name: "value2" }; // Use an invalid public key to trigger encryption failure for the first property // We can't easily make only one fail, so let's test with a completely invalid key var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, "invalid-key"); // Both should fail with invalid key, so both should be omitted expect(result).toHaveLength(0); expect(warnSpy).toHaveBeenCalledTimes(1); // Check that warning message matches expected format var calls = warnSpy.mock.calls; expect(calls.some(function (call) { return String(call[0]).includes("[Avo Inspector] Encryption failed:"); })).toBe(true); warnSpy.mockRestore(); }); test("null property values are encrypted as null", function () { var properties = [ { propertyName: "nullProp", propertyType: "null" }, ]; var eventProps = { nullProp: null }; var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey); expect(result).toHaveLength(1); expect(result[0].encryptedPropertyValue).toBeDefined(); var decrypted = referenceDecrypt(result[0].encryptedPropertyValue, keyPair.privateKey); expect(JSON.parse(decrypted)).toBeNull(); }); test("object property values are encrypted", function () { var properties = [ { propertyName: "address", propertyType: "object", children: [ { propertyName: "street", propertyType: "string" }, ], }, ]; var eventProps = { address: { street: "123 Main St" } }; var result = (0, AvoEncryption_1.encryptEventProperties)(properties, eventProps, keyPair.publicKey); expect(result).toHaveLength(1); expect(result[0].encryptedPropertyValue).toBeDefined(); var decrypted = referenceDecrypt(result[0].encryptedPropertyValue, keyPair.privateKey); expect(JSON.parse(decrypted)).toEqual({ street: "123 Main St" }); }); }); });