if(typeof globalThis.Buffer === "undefined") { globalThis.Buffer = require("buffer/").Buffer; }
const bs58 = require("bs58");
const Stream = require("readable-stream");
const Utils = require("./Utils");
if(!globalThis.process) {
globalThis.process = require("process/browser");
}
if(typeof crypto === "undefined") {
const crypto = require("crypto");
crypto.getRandomValues = arr => crypto.randomBytes(arr.length);
globalThis.crypto = crypto;
}
/**
* @namespace
* @description This namespace contains cryptographic helper methods to encrypt and decrypt
* data with automatic handling of keys
*/
const Crypto = {
ElvCrypto: async () => {
try {
if(!Crypto.elvCrypto) {
const ElvCrypto = (await import("@eluvio/crypto")).default;
Crypto.elvCrypto = await new ElvCrypto().init();
}
return Crypto.elvCrypto;
} catch(error) {
// eslint-disable-next-line no-console
console.error("Error initializing ElvCrypto:");
// eslint-disable-next-line no-console
console.error(error);
}
},
EncryptedSize: (clearSize) => {
const clearBlockSize = 1000000;
const blocks = Math.floor(clearSize / clearBlockSize);
let encryptedBlockSize = Crypto.EncryptedBlockSize(clearBlockSize);
let encryptedFileSize = blocks * encryptedBlockSize;
if(clearSize % clearBlockSize !== 0) {
encryptedFileSize += Crypto.EncryptedBlockSize(clearSize % clearBlockSize);
}
return encryptedFileSize;
},
EncryptedBlockSize: (clearSize, reencrypt=false) => {
const primaryEncBlockOverhead = 129;
const targetEncBlockOverhead = 608;
const MODBYTES_384_58 = 48;
const clearElementByteSize = 12 * (MODBYTES_384_58 - 1);
const encElementByteSize = 12 * MODBYTES_384_58;
let encryptedBlockSize = Math.floor((clearSize / clearElementByteSize)) * encElementByteSize;
if(clearSize % clearElementByteSize !== 0) {
encryptedBlockSize += encElementByteSize;
}
return reencrypt ? encryptedBlockSize + targetEncBlockOverhead : encryptedBlockSize + primaryEncBlockOverhead;
},
async EncryptConk(conk, publicKey) {
const elvCrypto = await Crypto.ElvCrypto();
publicKey = new Uint8Array(Buffer.from(publicKey.replace("0x", ""), "hex"));
conk = new Uint8Array(Buffer.from(JSON.stringify(conk)));
const {data, ephemeralKey, tag} = await elvCrypto.encryptECIES(conk, publicKey);
const cap = Buffer.concat([
Buffer.from(ephemeralKey),
Buffer.from(tag),
Buffer.from(data)
]);
return Utils.B64(cap);
},
async DecryptCap(encryptedCap, privateKey) {
const elvCrypto = await Crypto.ElvCrypto();
privateKey = new Uint8Array(Buffer.from(privateKey.replace("0x", ""), "hex"));
encryptedCap = Buffer.from(encryptedCap, "base64");
const ephemeralKey = encryptedCap.slice(0, 65);
const tag = encryptedCap.slice(65, 81);
const data = encryptedCap.slice(81);
const cap = elvCrypto.decryptECIES(
new Uint8Array(data),
privateKey,
new Uint8Array(ephemeralKey),
new Uint8Array(tag)
);
return JSON.parse(Buffer.from(cap).toString());
},
async GeneratePrimaryConk({spaceId, objectId}) {
const elvCrypto = await Crypto.ElvCrypto();
const {secretKey, publicKey} = elvCrypto.generatePrimaryKeys();
const symmetricKey = (elvCrypto.generateSymmetricKey()).key;
return {
symm_key: `kpsy${bs58.encode(Buffer.from(symmetricKey))}`,
secret_key: `kpsk${bs58.encode(Buffer.from(secretKey))}`,
public_key: `kppk${bs58.encode(Buffer.from(publicKey))}`,
sid: spaceId,
qid: objectId
};
},
async GenerateTargetConk() {
const elvCrypto = await Crypto.ElvCrypto();
const {secretKey, publicKey} = elvCrypto.generateTargetKeys();
return {
secret_key: `kpsk${bs58.encode(Buffer.from(secretKey))}`,
public_key: `ktpk${bs58.encode(Buffer.from(publicKey))}`
};
},
CapToConk(cap) {
const keyToBytes = key => new Uint8Array(bs58.decode(key.slice(4)));
return {
symmetricKey: keyToBytes(cap.symm_key),
secretKey: keyToBytes(cap.secret_key),
publicKey: keyToBytes(cap.public_key)
};
},
async EncryptionContext(cap) {
const elvCrypto = await Crypto.ElvCrypto();
const {symmetricKey, secretKey, publicKey} = Crypto.CapToConk(cap);
let context, type;
if(publicKey.length === elvCrypto.PRIMARY_PK_KEY_SIZE) {
// Primary context
type = elvCrypto.CRYPTO_TYPE_PRIMARY;
context = elvCrypto.newPrimaryContext(
publicKey,
secretKey,
symmetricKey
);
} else {
// Target context
type = elvCrypto.CRYPTO_TYPE_TARGET;
context = elvCrypto.newTargetDecryptionContext(
secretKey,
symmetricKey
);
}
return {context, type};
},
/**
* Encrypt data with headers
*
* @namedParams
* @param {Object} cap - Encryption "capsule" containing keys
* @param {ArrayBuffer | Buffer} data - Data to encrypt
*
* @returns {Promise<Buffer>} - Decrypted data
*/
Encrypt: async (cap, data) => {
const stream = await Crypto.OpenEncryptionStream(cap);
// Convert Blob to ArrayBuffer if necessary
if(!Buffer.isBuffer(data) && !(data instanceof ArrayBuffer)) {
data = Buffer.from(await new Response(data).arrayBuffer());
}
const dataArray = new Uint8Array(data);
for(let i = 0; i < dataArray.length; i += 1000000) {
const end = Math.min(dataArray.length, i + 1000000);
stream.write(dataArray.slice(i, end));
}
stream.end();
let encryptedChunks = [];
await new Promise((resolve, reject) => {
stream
.on("data", chunk => {
encryptedChunks.push(chunk);
})
.on("finish", () => {
resolve();
})
.on("error", (e) => {
reject(e);
});
});
return Buffer.concat(encryptedChunks);
},
OpenEncryptionStream: async (cap) => {
const elvCrypto = await Crypto.ElvCrypto();
const {context} = await Crypto.EncryptionContext(cap);
const stream = new Stream.PassThrough();
const cipher = elvCrypto.createCipher(context);
return stream
.pipe(cipher)
.on("finish", () => {
context.free();
})
.on("error", (e) => {
throw Error(e);
});
},
/**
* Decrypt data with headers
*
* @namedParams
* @param {Object} cap - Encryption "capsule" containing keys
* @param {ArrayBuffer | Buffer} encryptedData - Data to decrypt
*
* @returns {Promise<Buffer>} - Decrypted data
*/
Decrypt: async (cap, encryptedData) => {
const stream = await Crypto.OpenDecryptionStream(cap);
const dataArray = new Uint8Array(encryptedData);
for(let i = 0; i < dataArray.length; i += 1000000) {
const end = Math.min(dataArray.length, i + 1000000);
stream.write(dataArray.slice(i, end));
}
stream.end();
let decryptedChunks = [];
await new Promise((resolve, reject) => {
stream
.on("data", chunk => {
decryptedChunks.push(chunk);
})
.on("finish", () => {
resolve();
})
.on("error", (e) => {
reject(e);
});
});
return Buffer.concat(decryptedChunks);
},
OpenDecryptionStream: async (cap) => {
const elvCrypto = await Crypto.ElvCrypto();
const {context, type} = await Crypto.EncryptionContext(cap);
const stream = new Stream.PassThrough();
const decipher = elvCrypto.createDecipher(type, context);
return stream
.pipe(decipher)
.on("finish", () => {
context.free();
})
.on("error", (e) => {
throw Error(e);
});
}
};
module.exports = Crypto;