avo-inspector
Version:
[](https://badge.fury.io/js/avo-inspector)
273 lines (272 loc) • 14 kB
JavaScript
;
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" });
});
});
});