Home Reference Source Test

src/main/generic/consensus/base/primitive/Secret.js

class Secret extends Serializable {
    /**
     * @param {Secret.Type} type
     * @param {number} purposeId
     */
    constructor(type, purposeId) {
        super();
        this._type = type;
        this._purposeId = purposeId;
    }

    /**
     * @param {SerialBuffer} buf
     * @param {Uint8Array} key
     * @return {Promise.<PrivateKey|Entropy>}
     */
    static fromEncrypted(buf, key) {
        const version = buf.readUint8();

        const roundsLog = buf.readUint8();
        if (roundsLog > 32) throw new Error('Rounds out-of-bounds');
        const rounds = Math.pow(2, roundsLog);

        switch (version) {
            case 1:
                return Secret._decryptV1(buf, key, rounds);
            case 2:
                return Secret._decryptV2(buf, key, rounds);
            case 3:
                return Secret._decryptV3(buf, key, rounds);
            default:
                throw new Error('Unsupported version');
        }
    }

    /**
     * @param {SerialBuffer} buf
     * @param {Uint8Array} key
     * @param {number} rounds
     * @returns {Promise.<PrivateKey>}
     * @private
     */
    static async _decryptV1(buf, key, rounds) {
        const ciphertext = buf.read(Secret.SIZE);
        const salt = buf.read(Secret.ENCRYPTION_SALT_SIZE);
        const check = buf.read(Secret.ENCRYPTION_CHECKSUM_SIZE);
        const plaintext = await CryptoUtils.otpKdfLegacy(ciphertext, key, salt, rounds);

        const privateKey = new PrivateKey(plaintext);
        const publicKey = PublicKey.derive(privateKey);
        const checksum = publicKey.hash().subarray(0, Secret.ENCRYPTION_CHECKSUM_SIZE);
        if (!BufferUtils.equals(check, checksum)) {
            throw new Error('Invalid key');
        }

        return privateKey;
    }

    /**
     * @param {SerialBuffer} buf
     * @param {Uint8Array} key
     * @param {number} rounds
     * @returns {Promise.<PrivateKey>}
     * @private
     */
    static async _decryptV2(buf, key, rounds) {
        const ciphertext = buf.read(Secret.SIZE);
        const salt = buf.read(Secret.ENCRYPTION_SALT_SIZE);
        const check = buf.read(Secret.ENCRYPTION_CHECKSUM_SIZE);
        const plaintext = await CryptoUtils.otpKdfLegacy(ciphertext, key, salt, rounds);

        const checksum = Hash.computeBlake2b(plaintext).subarray(0, Secret.ENCRYPTION_CHECKSUM_SIZE);
        if (!BufferUtils.equals(check, checksum)) {
            throw new Error('Invalid key');
        }

        return new PrivateKey(plaintext);
    }

    /**
     * @param {SerialBuffer} buf
     * @param {Uint8Array} key
     * @param {number} rounds
     * @returns {Promise.<PrivateKey|Entropy>}
     * @private
     */
    static async _decryptV3(buf, key, rounds) {
        const salt = buf.read(Secret.ENCRYPTION_SALT_SIZE);
        const ciphertext = buf.read(Secret.ENCRYPTION_CHECKSUM_SIZE_V3 + /*purposeId*/ 4 + Secret.SIZE);
        const plaintext = await CryptoUtils.otpKdf(ciphertext, key, salt, rounds);

        const check = plaintext.subarray(0, Secret.ENCRYPTION_CHECKSUM_SIZE_V3);
        const payload = plaintext.subarray(Secret.ENCRYPTION_CHECKSUM_SIZE_V3);
        const checksum = Hash.computeBlake2b(payload).subarray(0, Secret.ENCRYPTION_CHECKSUM_SIZE_V3);
        if (!BufferUtils.equals(check, checksum)) {
            throw new Error('Invalid key');
        }

        const purposeId = payload[0] << 24 | payload[1] << 16 | payload[2] << 8 | payload[3];
        const secret = payload.subarray(4);
        switch (purposeId) {
            case PrivateKey.PURPOSE_ID:
                return new PrivateKey(secret);
            case Entropy.PURPOSE_ID:
            default:
                return new Entropy(secret);
        }
    }

    /**
     * @param {Uint8Array} key
     * @return {Promise.<SerialBuffer>}
     */
    async exportEncrypted(key) {
        const salt = new Uint8Array(Secret.ENCRYPTION_SALT_SIZE);
        CryptoWorker.lib.getRandomValues(salt);

        const data = new SerialBuffer(/*purposeId*/ 4 + Secret.SIZE);
        data.writeUint32(this._purposeId);
        data.write(this.serialize());

        const checksum = Hash.computeBlake2b(data).subarray(0, Secret.ENCRYPTION_CHECKSUM_SIZE_V3);
        const plaintext = new SerialBuffer(checksum.byteLength + data.byteLength);
        plaintext.write(checksum);
        plaintext.write(data);
        const ciphertext = await CryptoUtils.otpKdf(plaintext, key, salt, Secret.ENCRYPTION_KDF_ROUNDS);

        const buf = new SerialBuffer(/*version*/ 1 + /*kdf rounds*/ 1 + salt.byteLength + ciphertext.byteLength);
        buf.writeUint8(3); // version
        buf.writeUint8(Math.log2(Secret.ENCRYPTION_KDF_ROUNDS));
        buf.write(salt);
        buf.write(ciphertext);

        return buf;
    }

    /** @type {number} */
    get encryptedSize() {
        return /*version*/ 1
            + /*kdf rounds*/ 1
            + Secret.ENCRYPTION_SALT_SIZE
            + Secret.ENCRYPTION_CHECKSUM_SIZE_V3
            + /*purposeId*/ 4
            + Secret.SIZE;
    }

    /** @type {Secret.Type} */
    get type() {
        return this._type;
    }
}

Secret.Type = {
    PRIVATE_KEY: 1,
    ENTROPY: 2
};
Secret.SIZE = 32;

Secret.ENCRYPTION_SALT_SIZE = 16;
Secret.ENCRYPTION_KDF_ROUNDS = 256;
Secret.ENCRYPTION_CHECKSUM_SIZE = 4;
Secret.ENCRYPTION_CHECKSUM_SIZE_V3 = 2;

Class.register(Secret);