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;
});
}
}