@opendatalabs/vana-sdk
Version:
A TypeScript library for interacting with Vana Network smart contracts.
244 lines • 8.37 kB
JavaScript
import { ECIESError, isECIESEncrypted } from "./interface.js";
import { CURVE, CIPHER, KDF } from "./constants.js";
import { constantTimeEqual } from "./utils.js";
import { concat } from "viem";
class BaseECIESUint8 {
// Cache for validated public keys to avoid repeated validation
static validatedKeys = /* @__PURE__ */ new WeakMap();
/**
* Normalizes a public key to uncompressed format.
*
* @param publicKey - Public key in any format.
* @returns Uncompressed public key (65 bytes).
* @throws {ECIESError} If key format is invalid.
*/
normalizePublicKey(publicKey) {
if (BaseECIESUint8.validatedKeys.get(publicKey)) {
return publicKey;
}
if (publicKey.length === CURVE.UNCOMPRESSED_PUBLIC_KEY_LENGTH) {
if (publicKey[0] !== CURVE.PREFIX.UNCOMPRESSED) {
throw new ECIESError(
"Invalid uncompressed public key prefix",
"INVALID_KEY"
);
}
if (!this.validatePublicKey(publicKey)) {
throw new ECIESError("Invalid public key", "INVALID_KEY");
}
BaseECIESUint8.validatedKeys.set(publicKey, true);
return publicKey;
}
if (publicKey.length === CURVE.COMPRESSED_PUBLIC_KEY_LENGTH) {
if (publicKey[0] === CURVE.PREFIX.COMPRESSED_EVEN || publicKey[0] === CURVE.PREFIX.COMPRESSED_ODD) {
const decompressed = this.decompressPublicKey(publicKey);
if (!decompressed) {
throw new ECIESError(
"Failed to decompress public key",
"INVALID_KEY"
);
}
BaseECIESUint8.validatedKeys.set(decompressed, true);
return decompressed;
}
throw new ECIESError(
`Invalid compressed public key prefix: expected 0x02 or 0x03, got 0x${publicKey[0].toString(16).padStart(2, "0")}`,
"INVALID_KEY"
);
}
throw new ECIESError(
`Invalid public key length: ${publicKey.length}`,
"INVALID_KEY"
);
}
/**
* Encrypts data using ECIES.
*
* @param publicKey - The recipient's public key (compressed or uncompressed)
* @param message - The data to encrypt
* @returns Promise resolving to encrypted data structure
*/
async encrypt(publicKey, message) {
let ephemeralPrivateKey;
let sharedSecret;
let kdf;
let encryptionKey;
let macKey;
try {
if (!(publicKey instanceof Uint8Array)) {
throw new ECIESError("Public key must be a Uint8Array", "INVALID_KEY");
}
if (!(message instanceof Uint8Array)) {
throw new ECIESError(
"Message must be a Uint8Array",
"ENCRYPTION_FAILED"
);
}
if (publicKey.length === 0) {
throw new ECIESError("Public key cannot be empty", "INVALID_KEY");
}
const pubKey = this.normalizePublicKey(publicKey);
do {
ephemeralPrivateKey = this.generateRandomBytes(
CURVE.PRIVATE_KEY_LENGTH
);
} while (!this.verifyPrivateKey(ephemeralPrivateKey));
const ephemeralPublicKey = this.createPublicKey(
ephemeralPrivateKey,
false
);
if (!ephemeralPublicKey) {
throw new ECIESError(
"Failed to generate ephemeral public key",
"ENCRYPTION_FAILED"
);
}
sharedSecret = this.performECDH(pubKey, ephemeralPrivateKey);
kdf = this.sha512(sharedSecret);
encryptionKey = kdf.slice(
KDF.ENCRYPTION_KEY_OFFSET,
KDF.ENCRYPTION_KEY_OFFSET + KDF.ENCRYPTION_KEY_LENGTH
);
macKey = kdf.slice(
KDF.MAC_KEY_OFFSET,
KDF.MAC_KEY_OFFSET + KDF.MAC_KEY_LENGTH
);
const iv = this.generateRandomBytes(CIPHER.IV_LENGTH);
const ciphertext = await this.aesEncrypt(encryptionKey, iv, message);
const macData = concat([iv, ephemeralPublicKey, ciphertext]);
const mac = this.hmacSha256(macKey, macData);
return {
iv,
ephemPublicKey: ephemeralPublicKey,
ciphertext,
mac
};
} catch (error) {
if (error instanceof ECIESError) throw error;
throw new ECIESError(
`Encryption failed: ${error instanceof Error ? error.message : "Unknown error"}`,
"ENCRYPTION_FAILED",
error instanceof Error ? error : void 0
);
} finally {
if (ephemeralPrivateKey) this.clearBuffer(ephemeralPrivateKey);
if (sharedSecret) this.clearBuffer(sharedSecret);
if (kdf) this.clearBuffer(kdf);
if (encryptionKey) this.clearBuffer(encryptionKey);
if (macKey) this.clearBuffer(macKey);
}
}
/**
* Decrypts ECIES encrypted data.
*
* @param privateKey - The recipient's private key (32 bytes)
* @param encrypted - The encrypted data structure from encrypt()
* @returns Promise resolving to the original plaintext
*/
async decrypt(privateKey, encrypted) {
let sharedSecret;
let kdf;
let encryptionKey;
let macKey;
try {
if (!(privateKey instanceof Uint8Array)) {
throw new ECIESError("Private key must be a Uint8Array", "INVALID_KEY");
}
if (!isECIESEncrypted(encrypted)) {
throw new ECIESError(
"Invalid encrypted data structure",
"DECRYPTION_FAILED"
);
}
if (privateKey.length !== CURVE.PRIVATE_KEY_LENGTH) {
throw new ECIESError(
`Invalid private key length: ${privateKey.length}`,
"INVALID_KEY"
);
}
if (!this.verifyPrivateKey(privateKey)) {
throw new ECIESError("Invalid private key", "INVALID_KEY");
}
if (encrypted.ephemPublicKey.length !== CURVE.UNCOMPRESSED_PUBLIC_KEY_LENGTH) {
throw new ECIESError(
`Invalid ephemeral public key: expected ${CURVE.UNCOMPRESSED_PUBLIC_KEY_LENGTH} bytes (uncompressed), got ${encrypted.ephemPublicKey.length} bytes`,
"INVALID_KEY"
);
}
if (encrypted.ephemPublicKey[0] !== CURVE.PREFIX.UNCOMPRESSED) {
throw new ECIESError(
"Invalid ephemeral public key: must be uncompressed format with 0x04 prefix (eccrypto standard)",
"INVALID_KEY"
);
}
if (!this.validatePublicKey(encrypted.ephemPublicKey)) {
throw new ECIESError("Invalid ephemeral public key", "INVALID_KEY");
}
const ephemeralPublicKey = encrypted.ephemPublicKey;
sharedSecret = this.performECDH(ephemeralPublicKey, privateKey);
kdf = this.sha512(sharedSecret);
encryptionKey = kdf.slice(
KDF.ENCRYPTION_KEY_OFFSET,
KDF.ENCRYPTION_KEY_OFFSET + KDF.ENCRYPTION_KEY_LENGTH
);
macKey = kdf.slice(
KDF.MAC_KEY_OFFSET,
KDF.MAC_KEY_OFFSET + KDF.MAC_KEY_LENGTH
);
const macData = concat([
encrypted.iv,
encrypted.ephemPublicKey,
encrypted.ciphertext
]);
const expectedMac = this.hmacSha256(macKey, macData);
if (!constantTimeEqual(encrypted.mac, expectedMac)) {
throw new ECIESError("MAC verification failed", "MAC_MISMATCH");
}
const decrypted = await this.aesDecrypt(
encryptionKey,
encrypted.iv,
encrypted.ciphertext
);
return decrypted;
} catch (error) {
if (error instanceof ECIESError) throw error;
throw new ECIESError(
`Decryption failed: ${error instanceof Error ? error.message : "Unknown error"}`,
"DECRYPTION_FAILED",
error instanceof Error ? error : void 0
);
} finally {
if (sharedSecret) this.clearBuffer(sharedSecret);
if (kdf) this.clearBuffer(kdf);
if (encryptionKey) this.clearBuffer(encryptionKey);
if (macKey) this.clearBuffer(macKey);
}
}
/**
* Clears sensitive data from memory using multi-pass overwrite.
*
* @remarks
* Uses multiple passes with different patterns to make it harder
* for JIT compilers to optimize away the operation. While not
* guaranteed in JavaScript, this is a best-effort approach to
* clear sensitive data from memory.
*
* @param buffer - The buffer to clear
*/
clearBuffer(buffer) {
if (buffer && buffer.length > 0) {
buffer.fill(0);
buffer.fill(255);
buffer.fill(170);
buffer.fill(0);
for (let i = 0; i < buffer.length; i++) {
buffer[i] = i & 255 ^ 90;
}
buffer.fill(0);
}
}
}
export {
BaseECIESUint8
};
//# sourceMappingURL=base.js.map