Node.js
cryptoUtil
cryptoUtilIn the following sections, we will use different cryptographic primitives to implement parts of the PAD protocol. Here we provide a cryptoUtil.js Node.js script with these cryptographic primitives. (Requires Node.js v14+)
// cryptoUtil.js
const crypto = require('crypto');
const EPHEMERAL_KEY_SIZE = 16;
/**
* @typedef {Object} SymCiphertext
* @property {string} ciphertext
* @property {string} iv
*/
/**
* @typedef {Object} HybridCiphertext
* @property {SymCiphertext} encryptedMessage
* @property {string} encryptedEphemeralKey
*/
/**
* @param {object} options
* @param {Object.<string, crypto.KeyObject>}
*/
exports.generateSigningKeyPair = (options={type: 'ec', namedCurve:'prime256v1'}) => {
return crypto.generateKeyPairSync(options.type, options);
}
/**
* @param {Buffer} data
* @param {Buffer} symKey
* @param {string} algorithm
* @return {SymCiphertext}
*/
function encryptSym(data, symKey, algorithm='aes-128-cbc') {
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv(algorithm, symKey, iv);
let ciphertext = cipher.update(data);
ciphertext = Buffer.concat([ciphertext, cipher.final()]);
return {
ciphertext: ciphertext.toString('base64'),
iv: iv.toString('base64'),
};
}
exports.encryptSym = encryptSym;
/**
* @param {SymCiphertext} ciphertext
* @param {symKey} string
* @param {string} algorithm
* @return {Buffer}
*/
function decryptSym(ciphertext, symKey, algorithm) {
const iv = Buffer.from(ciphertext.iv, 'base64');
const decipher = crypto.createDecipheriv(algorithm, symKey, iv);
let data = decipher.update(Buffer.from(ciphertext.ciphertext, 'base64'));
data = Buffer.concat([data, decipher.final()]);
return data;
}
exports.decryptSym = decryptSym;
/**
* @param {Buffer} data
* @param {crypto.KeyLike} encKey
* @return {Buffer | HybridCiphertext} the ciphertext as a bytes string
*/
function encryptAsym(data, encKey) {
try {
return crypto.publicEncrypt(encKey, data);
} catch (err) {
if (err.code === 'ERR_OSSL_RSA_DATA_TOO_LARGE_FOR_KEY_SIZE') {
// avoid infinite recursive calls
if (data.length <= EPHEMERAL_KEY_SIZE) {
throw new Error('Ephemeral key size is too large for the encryption key type');
}
return encryptHybrid(data, encKey);
}
throw err;
}
}
exports.encryptAsym = encryptAsym;
/**
* @param {Buffer | HybridCiphertext} ciphertext
* @param {crypto.KeyLike} decKey
* @return {Buffer}
*/
function decryptAsym(ciphertext, decKey) {
if (Buffer.isBuffer(ciphertext)) {
return crypto.privateDecrypt(decKey, ciphertext);
}
return decryptHybrid(ciphertext, decKey);
}
exports.decryptAsym = decryptAsym;
/**
* @param {Buffer} data
* @param {crypto.KeyLike} encKey
* @return {HybridCiphertext}
*/
function encryptHybrid(data, encKey) {
const ephemeralKey = crypto.randomBytes(EPHEMERAL_KEY_SIZE);
const encryptedMessage = encryptSym(data, ephemeralKey);
const encryptedEphemeralKey = encryptAsym(ephemeralKey, encKey).toString('base64');
// encryptedEphemeralKey must be returned by crypto.publicEncrypt, i.e. a Buffer
return { encryptedMessage, encryptedEphemeralKey };
}
exports.encryptHybrid = encryptHybrid;
/**
* @param {HybridCiphertext} ciphertext
* @param {crypto.KeyLike} decKey
* @return {Buffer}
*/
function decryptHybrid(ciphertext, decKey) {
const encryptedEphemeralKey = Buffer.from(ciphertext.encryptedEphemeralKey, 'base64');
const ephemeralKey = decryptAsym(encryptedEphemeralKey, decKey);
const {encryptedMessage} = ciphertext;
const data = decryptSym(encryptedMessage, ephemeralKey);
return data;
}
exports.decryptHybrid = decryptHybrid;
/**
* @param {string | Buffer} data
* @param {crypto.KeyLike} signingKey
* @param {string} algorithm
* @return {Buffer}
*/
function sign(data, signingKey, algorithm='SHA256') {
const sign = crypto.createSign(algorithm);
sign.update(data);
sign.end();
const signature = sign.sign(signingKey);
return signature;
}
exports.sign = sign;
/**
* @param {string | Buffer} data
* @param {crypto.KeyLike} verificationKey
* @param {string | Buffer} signature
* @param {string} algorithm
* @return {boolean}
*/
function verify(data, verificationKey, signature, algorithm='SHA256') {
if (typeof signature === 'string') {
signature = Buffer.from(signature, 'base64');
}
const verify = crypto.createVerify('SHA256');
verify.update(data);
verify.end();
const verification = verify.verify(verificationKey, signature);
return verification;
};
exports.verify = verify;
/**
* @param {...(string | Buffer)} data
* @return {Buffer}
*/
function hash(...data) {
const sha256 = crypto.createHash('sha256');
for (const d of data) {
sha256.update(d);
}
return sha256.digest();
}
exports.hash = hash;
/**
* @param {Buffer} a
* @param {Buffer} b
* @return {Buffer}
*/
function xor(a, b) {
assert(a.length === b.length);
const result = a.map((byte, i) => byte ^ b[i]);
return Buffer.from(result);
}
exports.xor = xor;EPHEMERAL_KEY_SIZE(global variable): is the number of random bytes of the ephemeral keys used in a hybrid (symmetric & asymmetric) encryption.encryptSym(function): performs a symmetric encryption. Algorithm:AES-128-CBC.decryptSym(function): performs a symmetric decryption. Algorithm:AES-128-CBCencryptAsym(function): performs an asymmetric encryption. If the data to be encrypted is too large, it uses a hybrid encryption instead. Algorithm: depends on input key typedecryptAsym(function): performs an asymmetric decryption. If the ciphertext is a result from a hybrid encryption, then it uses hybrid decryption; Otherwise, it uses asymmetric decryption. Algorithm: depends on input key typeencryptHybrid(function): performs a hybrid encryption: performs a symmetric encryption on the data with an ephemeral key, then performs an asymmetric encryption on the ephemeral key with the encryption key.decryptHybrid(function): performs a hybrid decryption: retrieve the ephemeral key with an asymmetric decryption. Then it performs a symmetric decryption on the payload with the ephemeral key.sign(function): digitally signs a piece of data with a signing key. Algorithm: depends on input key type; data is first hashed withSHA256;verify(function): verifies if a signature matches with the alleged data and verification key. Algorithm: depends on input key type; data is first hashed withSHA256.hash(function): hashes the data in the argument list. Algorithm:SHA256xor(function): performs exclusive-or on two binary arrays
Encrypting
An Encryption is a JSON object which has the following form:
, where hybrid-ciphertext has the form
The encryption phase involves the encryptor sending an Encryption to the PAD server. Thus, it is essential to understand how it is created. We will go through its properties one by one.
"description": Description of theencryption. This can be an arbitrary string."tokenHash": Hased value of atoken.tokenHash = SHA256(token)."ciphertext": The ciphertext of the secret encrypted with the decryptor's public key and the symmetric keyk."trusteeShares": A dictionary containing all the encrypted and hashed trustee shares.[trusteeId]: A dictionary entry where the key is a trustee's ID, and the value is an object of encrypted and hashed trustee shares."encrypted": The trustee's share of the masked symmetric keyk \oplus Rused to encrypt the secret, encoded as a base64 string."hashed":SHA256of the trustee's share, encoded as a hex string.
"validatorShares": A dictionary containing all the encrypted validator shares.[validatorId]: A dictionary entry where the key is a validator's ID, and the value is an object of encrypted validator shares."encrypted": The validator's share of the maskRto the symmetric key used to encrypt the secret together with the decryptor's public key. Since the public key is large, the validator's shares are large too. Thus, the encryption of a validator's share uses an ephemeral key as a symmetric key.kand decryptor's key.
Creating token and tokenHash
token and tokenHashA token is a random number sent from the encryptor to the decryptor for her to later request for a piece of encryptor's data. It is essential to keep it secret between the encryptor and decryptor until the data request phase. An encryption's ID is tokenHash, the hash value of token. For portability and readability, both token and tokenHash are represented as hexadecimal strings.
Important: note that
tokenshould be taken as a lowercase string when hashed intotokenHash.
Creating ciphertext
ciphertextThe ciphertext part of an encryption should be encrypted with the symmetric key first, then the decryptor's encryption key. To ensure integrity, a digital signature from the encryptor against the ciphertext in the first encryption should be attached before the second encryption. In general, the first step creates the ciphertext Then the second step creates For example, suppose secret = "my_secret" is the encryptor's secret, the following code snippet generates c.
With c, the ciphertext can be created like this:
Secret sharing
The essential part of creating trusteeShares and validatorShares is secret sharing. We use the library secrets.js-grempe. In PAD, we also support 1 to be the thresholds. Moreover, we assumed the number of trustees and validators in instances is at most 256, it suffices to use 8 bits for secret sharing indices.
Creating trusteeShares
trusteeSharesTo create trusteeShares, the encryptor should first have knowledge about the settings of the instance, including the trustee threshold and the list of trustees referencing the instance (check out GET /metadata and GET /all-trustees/{trustee-id}. These information are also in one of the first few blocks on the ledger). The symmetric key k is then masked with a random number. This masked symmetric key is the secret shared among the trustees. After that, each share of trustees' is encrypted with their individual encryption key (the mapping between secret sharing index and trustee does not matter, but we recommend following the order in the instance's metadata. For example, trustee1 may hold a share of index 00 in an encryption, but hold another share of index 01 in another encryption). For auditing purposes, the hash of each share before encrypting should also be included, so that after a trustee post her share, consistency can be checked.
Creating validatorShares
validatorSharesThis is similar to creating trusteeShares, except the secret shared among the validators is the mask R together with the decryptor's public key to identify him.
Creating channelKey
channelKeyYou will also need a signing key pair for creating the new channel. This ensures that only the encryptor can modify the Encryption object using endpoint for updating an Encryption in a channel.
This channel key pair should be generated freshly and must not be the key pair that identifies the encryptor. Only the public (verification) key is sent to the server. The encryptor should keep the private (signing) key so long as she would update the Encryption therein.
That's it! We have gone through the steps of creating a new channel. Recall that the encryption is being sent to the PAD service server and the token is then shared with the decryptor out-of-band. The encryption channel allows the encryptor to update the secret which is useful in some use cases.
Updating Encryption object
Encryption objectUsing the channel signing key and the token-hash, the encryptor can update her Encryption object in the channel associated with the token. The following code snippets show how one create encryptionPayload and signature required for the operation.
Decrypting
The decryption phase happens after the encryption has been uploaded, a data request has been posted, and sufficient number of trustees and validators, respectivelly, have responded. At this stage, the decryptor has enough information to decrypt the encryptor's secret.
Verifying responses correctness
It is important for the decryptor to check correctness and integrity of the trustee and validator responses. Checking correctness can be done by checking consistency between a response and its hash submitted by the encryptor at encryption time. Checking integrity involves verifying a signature against the response payload.
Verifying ciphertext integrity
ciphertext integrityRecall that the ciphertext payload includes the encryptor's signature before encrypted with decryptor's encryption key. This ensures that the payload is not modified by third party, including the PAD server. The signature needs to be verified before decrypting.
Reconstructing masked symmetric key from trustees
To perform the symmetric decryption we need sufficient responses from trustees and validators, respectively. Then those will combine to the masked symmetric key and the masked. We show how to reconstruct the masked symmetric key from trustees' responses in this section. Obviously, it has redundancy with our previous example. In actual implementation, these scripts can be merged. For example, parse the response, push the share to an array only if it is valid, then combine those later.
Reconstructing the mask from validators
Decrypting!
The decryptor now has everything to retrieve the encryptor's secret.
Last updated
Was this helpful?