Home Reference Source

lib6/encryption/encryption.js

/**
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [https://neo4j.com]
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
import Integer, { int, isInt } from '../integer';
import { EncryptedValue } from './encrypted-value';
import { EncapsulatedKeyManager } from './encapsulated-key-manager';
import CryptoProvider from './node/crypto';
import { isDate, isDateTime, isDuration, isLocalDateTime, isLocalTime, isTime } from '../temporal-types';
import { isVector } from '../vector';
import { isPoint } from '../spatial-types';
import { isUUID } from '../uuid';
import { newError } from '../error';
import { stringify } from '../json';
import { ProtocolVersion } from '../protocol-version';
import UnsupportedType from '../unsupported-type';
const supportedAADTypes = ['BOOLEAN', 'DATE', 'INTEGER', 'LOCAL TIME', 'POINT', 'STRING', 'ZONED TIME', 'UUID', 'BYTES'];
/**
 * Provides Neo4j Property Encryption functions.
 *
 * @since 6.3.0
 * @experimental Part of the Client-Side Encrytion preview feature
 */
export default class EncryptionService {
    /**
     * @private
     */
    constructor(boltProvider, profiles) {
        this._boltProvider = boltProvider;
        this._profiles = new Map();
        profiles.forEach((profile) => {
            if (this._profiles.has(profile.name)) {
                throw newError(`Multiple profiles with name ${profile.name} configured, each profile must have a unique name.`);
            }
            this._profiles.set(profile.name, { profile, keyManager: new EncapsulatedKeyManager(profile, profile.encapsulationService) });
        });
        this._cryptoProvider = new CryptoProvider();
    }
    /**
     * Encrypts data with AES-GCM 256 into a byte array ready to be saved an a property in a Neo4j database.
     * The byte array includes metadata required to allow another Neo4j Driver configured with the same {@link EncapsulatedKeyRecordRepository} to decrypt the data.
     *
     * @param {Object} encryptRequest - The value to encrypt and associated metadata for the encryption
     * @param {any} encryptRequest.value - The value to encrypt, must be a value able to be saved as a Neo4j Property.
     * @param {string | Object | undefined} encryptRequest.keyOptions - Used to determine the key to be used, a plain string is assumed to be the id, if omitted the encryption profile's default key reference will be used
     * @param {string} encryptRequest.encryptionProfile - Name of the {@link EncryptionProfile} to use, must be provided unless the driver is configured with only 1 profile.
     * @param {any | undefined} encryptRequest.aad - Additional Authenticated Data for the encryption.
     *
     * @returns {Promise<Int8Array>} A Int8Array that can be stored as a ByteArray in a Neo4j database.
     */
    encrypt(encryptRequest) {
        return __awaiter(this, void 0, void 0, function* () {
            const profile = this._getProfile(encryptRequest.encryptionProfile);
            const { typeName, typeProtocolMajor, typeProtocolMinor } = this._identifyType(encryptRequest.value);
            const encodedValue = this._boltProvider.encodeValue(encryptRequest.value);
            let encodedAAD;
            let aadType;
            if (encryptRequest.aad !== undefined) {
                aadType = this._identifyType(encryptRequest.aad);
                if (!supportedAADTypes.includes(aadType.typeName)) {
                    throw newError(`Unsupported AAD property type ${aadType.typeName}, supported values are ${supportedAADTypes.toString()}`);
                }
                encodedAAD = this._boltProvider.encodeValue(encryptRequest.aad);
            }
            const key = yield this._getKeyRecord(profile.profile, encryptRequest.keyOptions);
            try {
                const decapsulatedKey = yield this._decapsulateKey(profile.profile, key);
                const { cyphertext, iv } = yield this._cryptoProvider.encrypt(decapsulatedKey, encodedValue, encodedAAD);
                const metadata = {
                    key_id: key.id(),
                    iv: new Int8Array(iv.buffer),
                    aad: encodedAAD !== undefined ? new Int8Array(encodedAAD) : undefined,
                    aad_encoding_scheme_major: aadType === null || aadType === void 0 ? void 0 : aadType.typeProtocolMajor,
                    aad_encoding_scheme_minor: aadType === null || aadType === void 0 ? void 0 : aadType.typeProtocolMinor
                };
                return this._boltProvider.encodeObject(new EncryptedValue(new Int8Array(cyphertext), profile.profile.name, profile.profile.type, profile.profile.version, typeName, typeProtocolMajor, typeProtocolMinor, metadata));
            }
            catch (e) {
                throw newError('Property encryption failed due to internal error, see cause.', '50N42', e);
            }
        });
    }
    /**
     * Decrypts data encrypted with this or another Neo4j Driver's {@link EncryptionService#encrypt} function.
     *
     * @param {Object} decryptRequest - Object containing encrypted ciphertext and required metadata.
     * @param {Int8Array} decryptRequest.ciphertext - The encrypted ciphertext
     * @param {any | undefined} decryptRequest.aad - The Additional Authenticated Data to verify when decrypting
     * @param {boolean | undefined} decryptRequest.usePersistedAad - Wether to use the persisted aad data stored with the ciphertext, mutually exclusive with decryptRequest.aad
     *
     * @returns {Promise<T | UnsupportedType>} The decrypted property or an {@link UnsupportedType} if decoding the decrypted bytes failed.
     */
    decrypt(decryptRequest) {
        return __awaiter(this, void 0, void 0, function* () {
            let encodedAAD;
            const struct = this._boltProvider.decodeObject(decryptRequest.ciphertext);
            if (struct.typeProtocolMajor.notEquals(int(1)) || struct.typeProtocolMinor.greaterThan(int(0))) {
                return new UnsupportedType('Undecryptable Value', struct.typeProtocolMajor.toNumber(), struct.typeProtocolMinor.toNumber(), 'Encrypted value was encoded with a newer driver, you must update your driver version to decode it.');
            }
            if (struct.profileType === 'ENVELOPE' && struct.profileVersion.equals(int(1))) {
                if (decryptRequest.usePersistedAad === true) {
                    encodedAAD = struct.metadata.aad !== undefined ? struct.metadata.aad.buffer : undefined;
                }
                else if (decryptRequest.aad !== undefined) {
                    const aadType = this._identifyType(decryptRequest.aad);
                    if (aadType.typeProtocolMajor !== struct.metadata.aad_encoding_scheme_major || aadType.typeProtocolMinor !== struct.metadata.aad_encoding_scheme_minor) {
                        encodedAAD = this._boltProvider.encodeAAD(decryptRequest.aad, new ProtocolVersion(struct.metadata.aad_encoding_scheme_major, struct.metadata.aad_encoding_scheme_minor));
                    }
                    else {
                        throw newError('AAD used to encrypt was encoded with a newer encoding scheme than this driver supports');
                    }
                }
                const profile = this._getProfile(struct.profileName);
                try {
                    const key = yield this._getKeyRecord(profile.profile, struct.metadata.key_id);
                    const decapsulatedKey = yield this._decapsulateKey(profile.profile, key);
                    const decodedValue = this._boltProvider.decodeValue(yield this._cryptoProvider.decrypt(decapsulatedKey, struct.metadata.iv, struct.cipherOutput.buffer, encodedAAD), new ProtocolVersion(struct.typeProtocolMajor.toNumber(), struct.typeProtocolMinor.toNumber()));
                    const type = this._identifyType(decodedValue);
                    if (type.typeProtocolMajor.equals(struct.typeProtocolMajor) && type.typeProtocolMinor.equals(struct.typeProtocolMinor)) {
                        return decodedValue;
                    }
                    else {
                        return new UnsupportedType(`Encrypted<${type.typeName}>`, struct.typeProtocolMajor.toNumber(), struct.typeProtocolMinor.toNumber(), 'Encrypted value was encoded with a driver too old for this driver to read it.');
                    }
                }
                catch (e) {
                    throw newError('Property decryption failed due to internal error, see cause.', '50N42', e);
                }
            }
            else {
                throw newError(`Unsupported encryption profile version ${struct.profileVersion.toString()} of profile type ${struct.profileType}.`);
            }
        });
    }
    /**
     * Returns the key manager for the specified profile
     *
     * @param {string | undefined} profileName name of the profile to get the key manager of, can be omitted if driver is configured with only one encryption profile
     * @returns {EncapsulatedKeyManager} The key manager of the specified profile.
     */
    keyManager(profileName) {
        const profile = this._getProfile(profileName);
        return profile.keyManager;
    }
    // This function will need to be dynamic based on which version of the encoding is being used, currently only 1.0 (based on bolt 6.1) exists.
    _identifyType(value) {
        if (value === null) {
            const verification = (_) => { throw newError('Encrypted arrays cannot contain null values'); };
            return { typeName: 'NULL', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (typeof value === 'string') {
            const verification = (value) => typeof value === 'string';
            return { typeName: 'STRING', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (typeof value === 'boolean') {
            const verification = (value) => typeof value === 'boolean';
            return { typeName: 'BOOLEAN', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (typeof value === 'number') {
            const verification = (value) => typeof value === 'number';
            return { typeName: 'FLOAT', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (typeof value === 'bigint') {
            const verification = (value) => typeof value === 'bigint';
            return { typeName: 'INTEGER', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (isInt(value)) {
            const verification = (value) => isInt(value);
            return { typeName: 'INTEGER', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (isDate(value)) {
            const verification = (value) => isDate(value);
            return { typeName: 'DATE', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (isDuration(value)) {
            const verification = (value) => isDuration(value);
            return { typeName: 'DURATION', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (isVector(value)) {
            const verification = (_) => { throw newError('Encrypted arrays cannot contain vector values'); };
            return { typeName: 'VECTOR', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (value instanceof Int8Array) {
            const verification = (value) => value instanceof Int8Array;
            return { typeName: 'BYTES', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (isLocalDateTime(value)) {
            const verification = (value) => isLocalDateTime(value);
            return { typeName: 'LOCAL DATETIME', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (isLocalTime(value)) {
            const verification = (value) => isLocalTime(value);
            return { typeName: 'LOCAL TIME', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (isDateTime(value)) {
            const verification = (value) => isDateTime(value);
            return { typeName: 'ZONED DATETIME', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (isTime(value)) {
            const verification = (value) => isTime(value);
            return { typeName: 'ZONED TIME', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (isPoint(value)) {
            const verification = (point) => {
                if (isPoint(point)) {
                    if ((point.srid instanceof BigInt ? Integer.fromString(point.srid.toString()) : int(point.srid))
                        .notEquals(value.srid instanceof BigInt ? Integer.fromString(value.srid.toString()) : int(value.srid))) {
                        throw newError('Encrypted arrays of Points must only contain Points with identical srids');
                    }
                    if ((point.z == null && value.z != null) || (point.z != null && value.z == null)) {
                        throw newError('Encrypted arrays of Points must only contain Points of the same dimensionality.');
                    }
                    return true;
                }
                return false;
            };
            return { typeName: 'POINT', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (isUUID(value)) {
            const verification = (value) => isUUID(value);
            return { typeName: 'UUID', typeProtocolMajor: int(1), typeProtocolMinor: int(0), verification };
        }
        if (Array.isArray(value)) {
            const type = this._identifyType(value[0]);
            if (!value.map(element => type.verification(element)).every(element => element)) {
                throw newError('Every element of an encrypted array must be of the same property type');
            }
            let major = int(1);
            let minor = int(0);
            if (type.typeProtocolMajor.greaterThan(major) || (type.typeProtocolMajor.equals(major) && type.typeProtocolMinor.greaterThan(minor))) {
                major = type.typeProtocolMajor;
                minor = type.typeProtocolMinor;
            }
            const verification = (_) => false;
            return { typeName: 'LIST', typeProtocolMajor: major, typeProtocolMinor: minor, verification };
        }
        throw newError(`could not identify type of: ${stringify(value)}`);
    }
    _getProfile(name) {
        if (name == null) {
            if (this._profiles.size === 1) {
                // @ts-expect-error
                return this._profiles.get(this._profiles.keys().next().value);
            }
            else {
                throw newError('When using a driver with multiple encryption profiles configured, which encryption profile to use must be explicitly provided');
            }
        }
        else {
            const profile = this._profiles.get(name);
            if (profile === undefined) {
                throw newError(`Could not find encryption profile with name ${name}`);
            }
            return profile;
        }
    }
    _getKeyRecord(profile, options) {
        return __awaiter(this, void 0, void 0, function* () {
            const key = yield profile.findKey(options);
            if (key == null) {
                throw newError(`Could not find key with key options: ${stringify(options)}`);
            }
            return key;
        });
    }
    _decapsulateKey(profile, key) {
        return __awaiter(this, void 0, void 0, function* () {
            const decapsulatedKey = yield profile.encapsulationService.decapsulate(key.encapsulation(), key.metadata());
            if (decapsulatedKey.length !== 32) {
                throw newError(`Expected decapulated key to be 32 bytes, found ${decapsulatedKey.length}`);
            }
            return decapsulatedKey;
        });
    }
}