SIGN IN SIGN UP

feat(prop-enc): introduce Neo4j Property Encryption (#1781)

This update introduces Neo4j Property Encryption, a new preview feature that allows Neo4j properties to be encrypted and decrypted on the driver side.

Property values are encrypted and encoded into bytes. The resulting bytes represent the encrypted value and its associated metadata. Users can choose how and where to store those bytes, provided the chosen representation is supported as a Neo4j property type. To decrypt a value, users retrieve the encoded bytes from the format in which they stored them and provide those bytes to the driver for decryption.

The feature is designed to be easy to use, with an opinionated approach to encryption and decryption. All official Neo4j drivers are expected to support the feature, enabling interoperability between drivers when they are configured with compatible encryption profiles and key management components.

## Property Encryption

Neo4j Property Encryption is exposed through `PropertyEncryption`, which is available directly from the `Driver` object. This makes the API easy to access, particularly in existing applications where the driver object is already available.

Asynchronous and reactive variants are also available.

Example:
```java
var propertyEncryption = driver.propertyEncryption(); // PropertyEncryption

propertyEncryption = driver.propertyEncryption(PropertyEncryption.class); // PropertyEncryption, same as above
var asyncPropertyEncryption = driver.propertyEncryption(AsyncPropertyEncryption.class); // AsyncPropertyEncryption
var reactivePropertyEncryption = driver.propertyEncryption(org.neo4j.driver.encryption.reactive.ReactivePropertyEncryption.class); // ReactivePropertyEncryption using java.util.concurrent.Flow types
var reactiveStreamsPropertyEncryption = driver.propertyEncryption(org.neo4j.driver.encryption.reactivestreams.ReactivePropertyEncryption.class); // ReactivePropertyEncryption using org.reactivestreams types
```

The following example shows how values are encrypted and decrypted. The concepts used by the example, including encryption profiles and key aliases, are explained in the sections below.

Values are encrypted by creating a `PropertyEncryptionRequest` and providing it to `PropertyEncryption` or one of its alternative variants. `PropertyEncryptionRequest#builder()` returns a staged builder for creating a `PropertyEncryptionRequest`.

Example:

```java
var secretValue = "secret";
var userId = 1000;
var encryptionRequest = PropertyEncryptionRequest.builder()
        .fromValue(secretValue) // the value to encrypt
        .withAAD(userId) // an optional AAD
        .usingKeyAlias("users-key") // the key to encrypt with, this is explained later
        .build();
byte[] encrypted = propertyEncryption.encryptToBytes(encryptionRequest); // returns an encoded representation of the encrypted value and its associated metadata
```

The resulting bytes contain the encrypted value together with its metadata. The application can store these bytes in any representation supported as a Neo4j property type.

Encoded encrypted values are decrypted by creating a `PropertyDecryptionRequest` and providing it to `PropertyEncryption` or one of its alternative variants. `PropertyDecryptionRequest#builder()` returns a staged builder for creating a `PropertyDecryptionRequest`.

Example:

```java
var userId = 1000;
var decryptionRequest = PropertyDecryptionRequest.builder()
        .fromValue(encrypted) // the encoded encrypted value
        .withAAD(userId) // external AAD
        .build();
Value value = propertyEncryption.decrypt(decryptionRequest); // returns decrypted value
```

## Supported Types

Drivers support encrypting Neo4j Database [Property types](https://neo4j.com/docs/cypher-manual/current/values-and-types/property-structural-constructed/#property-types) only, namely:
- BOOLEAN
- DATE
- DURATION
- FLOAT
- INTEGER
- LIST
  - Homogeneous lists of property types can be stored as properties (except VECTOR and LIST types, which cannot be stored in lists), although lists in general cannot be stored as properties. Lists stored as properties cannot contain null values.
- LOCAL DATETIME
- LOCAL TIME
- POINT
- STRING
- VECTOR
- ZONED DATETIME
- ZONED TIME
- UUID
- BYTES
- NULL

## Encryption Profiles

An encryption profile defines the settings and operating mode used by the driver when encrypting and decrypting values.

Each profile has a unique, user-defined name. The profile name is stored with encrypted values and allows drivers to identify which profile must be used to decrypt a value.

Multiple profiles can be configured at the same time. When driver interoperability is required, users are expected to configure each driver with compatible profiles.

Profiles are configured at the driver level using a driver configuration option:

```java
var config = Config.builder()
        .withPropertyEncryptionProfiles(profiles) // PropertyEncryptionProfile...
        .build();
```

`PropertyEncryptionProfile` is the base type for encryption profiles. At present, the driver provides one profile type: `EnvelopePropertyEncryptionProfile`. Additional profile types may be introduced in the future.

The `PropertyEncryptionRequest` builder allows a profile name to be specified explicitly:

```java
var encryptionRequest = PropertyEncryptionRequest.builder()
        .fromValue(secretValue)
        .withAAD(userId)
        .usingProfile("profile-name") // sets profile name
        .usingKeyAlias("users-key")
        .build();
```

Specifying a profile name is mandatory when multiple profiles are configured. When only one profile is configured, the profile name is optional for convenience.

## Key Encapsulation Service

To explain the `EnvelopePropertyEncryptionProfile`, we first need to introduce the components it uses to protect and store data encryption keys. These are `KeyEncapsulationService` and `EncapsulatedKeyRecordRepository`.

Envelope encryption uses data encryption keys to encrypt property values. Those keys must themselves be protected so that they can be stored and later recovered when a property needs to be decrypted. `KeyEncapsulationService` is responsible for generating, encapsulating, and decapsulating data encryption keys. It generates a new data encryption key together with its encapsulation, and can later decapsulate the encapsulation to recover the data encryption key when it is needed for encryption or decryption.

The `EncapsulatedKeyRecordRepository`, described in the next section, stores the encapsulations and their associated metadata.

The key encapsulation mechanism is implementation-specific. It may use key wrapping, symmetric or asymmetric cryptography, a key management service (KMS), or a post-quantum key encapsulation mechanism such as ML-KEM.

KeyEncapsulationService can be implemented by users, but the driver provides implementations for local key encapsulation and several cloud key management services.

### Local

The local implementation uses a user-provided AES-256 `SecretKey` as a master key for encapsulating and decapsulating data encryption keys.

The encapsulation uses AES-GCM (`"AES/GCM/NoPadding"`) with the provided master key. The resulting encapsulation contains a 256-bit AES data encryption key protected by the master key, together with a 96-bit (12-byte) initialization vector (IV) and a 128-bit (16-byte) authentication tag.

A custom provider and secure random source can be provided. Otherwise, the Java runtime determines and provides the `java.security.Provider` and `java.security.SecureRandom` according to its configuration.

Example:

```java
var encapsulationService = KeyEncapsulationServices.local(masterKey, provider, ivSecureRandom);
```

### AWS KMS

The AWS KMS implementation generates 256-bit AES data encryption keys locally and uses AWS KMS to encapsulate and decapsulate those keys.

A custom `java.security.Provider` can be provided for key generation. Otherwise, the Java runtime determines and provides the `java.security.Provider` according to its configuration.

Example:

```java
var defaultOptions = AwsKeyEncapsulationOptions.of(keyId);
var encapsulationService = AWSKeyEncapsulationServices.create(defaultOptions, provider);
```

This implementation is provided as a separate Neo4j Java Driver module, `neo4j-java-driver-encryption-aws-kms`, which must be used together with the driver module.

Maven example:

```xml
<dependencies>
    <dependency>
        <groupId>org.neo4j.driver</groupId>
        <artifactId>neo4j-java-driver</artifactId>
    </dependency>
    <dependency>
        <groupId>org.neo4j.driver</groupId>
        <artifactId>neo4j-java-driver-encryption-aws-kms</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.neo4j.driver</groupId>
            <artifactId>neo4j-java-driver-bom</artifactId>
            <type>pom</type>
            <scope>import</scope>
            <version>6.3.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>
```

### Azure Key Vault

The Azure Key Vault implementation generates 256-bit AES data encryption keys locally and uses Azure Key Vault to encapsulate and decapsulate those keys.

A custom provider and secure random source can be provided. Otherwise, the Java runtime determines and provides the `java.security.Provider` and `java.security.SecureRandom` according to its configuration.

Example:

```java
var defaultOptions = AzureEncapsulationOptions.of(keyId);
var encapsulationService = AzureKeyEncapsulationServices.create(defaultOptions, provider);
```

This implementation is provided as a separate Neo4j Java Driver module, `neo4j-java-driver-encryption-azure-keyvault`, which must be used together with the driver module.

Maven example:

```xml
<dependencies>
    <dependency>
        <groupId>org.neo4j.driver</groupId>
        <artifactId>neo4j-java-driver</artifactId>
    </dependency>
    <dependency>
        <groupId>org.neo4j.driver</groupId>
        <artifactId>neo4j-java-driver-encryption-azure-keyvault</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.neo4j.driver</groupId>
            <artifactId>neo4j-java-driver-bom</artifactId>
            <type>pom</type>
            <scope>import</scope>
            <version>6.3.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>
```

### Google Cloud KMS

The Google Cloud KMS implementation generates 256-bit AES data encryption keys locally and uses Google Cloud KMS to encapsulate and decapsulate those keys.

A custom provider and secure random source can be provided. Otherwise, the Java runtime determines and provides the `java.security.Provider` and `java.security.SecureRandom` according to its configuration.

Example:

```java
var defaultOptions = CloudKmsKeyEncapsulationOptions.of(project, location, keyRing, cryptoKey);
var encapsulationService = CloudKeyEncapsulationServices.create(defaultOptions, provider);
```

This implementation is provided as a separate Neo4j Java Driver module, `neo4j-java-driver-encryption-google-cloud-kms`, which must be used together with the driver module.

Maven example:

```xml
<dependencies>
    <dependency>
        <groupId>org.neo4j.driver</groupId>
        <artifactId>neo4j-java-driver</artifactId>
    </dependency>
    <dependency>
        <groupId>org.neo4j.driver</groupId>
        <artifactId>neo4j-java-driver-encryption-google-cloud-kms</artifactId>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.neo4j.driver</groupId>
            <artifactId>neo4j-java-driver-bom</artifactId>
            <type>pom</type>
            <scope>import</scope>
            <version>6.3.0</version>
        </dependency>
    </dependencies>
</dependencyManagement>
```

## Encapsulated Key Record Repository

An `EncapsulatedKeyRecordRepository` is a user-provided repository for storing encapsulated data keys and their associated metadata.

Each encapsulated key record has a globally unique and immutable identifier. The repository implementation is responsible for assigning these identifiers.

The repository is also responsible for enforcing alias uniqueness.

The repository is used by the encryption profile to retrieve the encapsulated key required to encrypt or decrypt a property value.

### Envelope Encryption Profile

The `EnvelopePropertyEncryptionProfile` provides envelope encryption for property values.

Envelope encryption separates the encryption of data from the protection of the key used to encrypt that data. Property values are encrypted using a 256-bit data encryption key with AES-GCM (`"AES/GCM/NoPadding"` specifically). The data encryption key and its corresponding encapsulation are produced by the configured `KeyEncapsulationService`.

The encapsulated key and its associated metadata are stored in the configured `EncapsulatedKeyRecordRepository`.

When a property is encrypted, the driver resolves the requested data key from the repository and uses the `KeyEncapsulationService` to decapsulate the data encryption key. The resulting key is then used for AES-GCM encryption.

When a property is decrypted, the driver similarly retrieves the encapsulated key and uses the `KeyEncapsulationService` to obtain the data encryption key required for decryption.

Each encryption operation uses a 96-bit (12-byte) initialization vector (IV). AES-GCM uses a 128-bit (16-byte) authentication tag to provide integrity and authenticity of the encrypted data and any associated authenticated data (AAD).

AAD is optional and is supplied explicitly when encrypting a value. When present, the AAD is stored alongside the encrypted value in the encoded bytes returned by the driver. During decryption, the caller selects whether to provide AAD or use the AAD configuration recorded in the encoded encrypted value. When the recorded AAD configuration is used, the AAD persisted with the encrypted value is used during decryption. If no AAD was used during encryption, no effective AAD is used.

Providing an explicit AAD value is generally recommended because it allows the encrypted value to be bound to trusted external context that is not part of the encoded encrypted value itself.

Both the `java.security.Provider` used for AES-GCM and the `java.security.SecureRandom` from which IVs are sourced are configurable. If neither is explicitly provided, the Java runtime determines and provides them according to its configuration.

An envelope encryption profile is configured with a profile name, a `KeyEncapsulationService`, and an `EncapsulatedKeyRecordRepository`:

Example:

```java
var profile = EnvelopePropertyEncryptionProfile.builder("profile-name", keyEncapsulationService, keyRepository)
        .withCryptoContext(provider, ivSecureRandom) // sets custom crypto context
        .build();
```

## Key Identifiers and Aliases

A data key can be referenced by its globally unique identifier or, if assigned, by an alias.

A key identifier is immutable and globally unique. An alias is a mutable, application-level reference that can be reassigned to a different key over time.

Aliases allow applications to refer to keys using stable application-level names without having to know the key's identifier.

## Key Management

The envelope encryption profile requires data keys to exist before they can be used to encrypt or decrypt property values.

Creating a data key involves obtaining a new data encryption key and its encapsulation from the `KeyEncapsulationService` and creating the corresponding record in the `EncapsulatedKeyRecordRepository`.

To simplify these operations, `PropertyEncryption` provides an `EncapsulatedKeyManager` through `PropertyEncryption#keyManager()` when a single profile is configured and `PropertyEncryption#keyManager(String)` when multiple profiles are configured.

The key manager provides operations to create, find, and delete keys. It also provides operations to set and delete key aliases. When caching is enabled, key management operations update the key cache and key alias index as necessary (see the next section).

Example:

```java
var keyManager = propertyEncryption.keyManager("profile-name");
EncapsulatedKey key = keyManager.create("users-key");
```

## Key Caching

The driver caches decapsulated data encryption keys by default to avoid repeatedly resolving and decapsulating the same key.

Depending on the `KeyEncapsulationService` and `EncapsulatedKeyRecordRepository` implementations, resolving a key may require network exchanges. Caching can therefore reduce the number of repeated key resolution operations.

The key cache is keyed by the key's globally unique identifier and has a configurable maximum size and time-to-live (TTL).

The key cache can be configured when creating an envelope encryption profile:

```java
var profile = EnvelopePropertyEncryptionProfile.builder("profile-name", keyEncapsulationService, keyRepository)
        .withKeyCache(100, Duration.ofHours(1)) // sets custom key cache size and ttl
        .build();
```

The key cache is bounded and uses a least-recently-used (LRU) eviction policy when its configured maximum size is reached. Entries that have exceeded their configured TTL are treated as cache misses and are not used.

The key cache can be disabled.

## Key Alias Index

Aliases are resolved separately through a key alias index that maps aliases to key identifiers. The alias index does not contain key material.

The alias index has its own configurable maximum size and TTL. This allows alias mappings to expire independently of cached keys and limits the period for which a driver may use a stale alias after it has been reassigned.

The alias index can be configured when creating an envelope encryption profile:

```java
var profile = EnvelopePropertyEncryptionProfile.builder("profile-name", keyEncapsulationService, keyRepository)
        .withKeyAliasIndex(100, Duration.ofMinutes(1)) // sets custom key alias index size and ttl
        .build();
```

Like the key cache, the alias index is bounded and uses a least-recently-used (LRU) eviction policy when its configured maximum size is reached. Entries that have exceeded their configured TTL are treated as cache misses and are not used.

The key alias index is disabled when the key cache is disabled. It can also be disabled independently.

Both settings can be configured together:

```java
var profile = EnvelopePropertyEncryptionProfile.builder("profile-name", keyEncapsulationService, keyRepository)
        .withKeyCache(100, Duration.ofHours(1)) // sets custom key cache size and ttl
        .withKeyAliasIndex(100, Duration.ofMinutes(1)) // sets custom key alias index size and ttl
        .build();
```

## Observability

Observability is a preview feature available in the driver since `6.0.0`. With the additional `neo4j-java-driver-observation-micrometer` module, users can handle driver observations through the Micrometer Observation API, which can be used to create metrics and traces.

Neo4j Property Encryption adds observations for the following operations:
- Property encryption
  - Encryption to bytes
  - Decryption
- Key management
  - Create encapsulated key
  - Find encapsulated key by alias
  - Set encapsulated key alias
  - Delete encapsulated key
- Key encapsulation service
  - Encapsulate key
  - Decapsulate key
- Encapsulated key record repository
  - Create encapsulated key record
  - Find encapsulated key record by id
  - Find encapsulated key record by alias
  - Set encapsulated key record alias by id
  - Delete encapsulated key record by id

## Security Providers

Cryptographic operations are performed through the Java Cryptography Architecture (JCA) and related Java security APIs rather than being implemented directly by the driver.

By default, the driver uses the cryptographic implementations selected by the Java runtime. Where applicable, users can explicitly provide a `java.security.Provider` and `java.security.SecureRandom` to control the cryptographic implementations used by the driver.

The cryptographic algorithms and parameters used by this feature, including key sizes, initialization vector (IV) sizes, and authentication tag sizes, have been selected with FIPS requirements in mind. However, FIPS compliance depends on more than the algorithms and parameters used by the driver. It can also depend on the cryptographic provider or validated cryptographic module, its configuration, the runtime environment, key management, storage, and other aspects of the deployment that are outside the driver's control.

Users requiring FIPS-compliant operation are responsible for ensuring that the driver and its surrounding environment are configured appropriately for their requirements. The driver uses the provider and secure random source supplied by the user, subject to the requirements documented in the relevant Javadoc.

### Bouncy Castle FIPS

For example, the driver can be configured to use the Bouncy Castle FIPS provider `org.bouncycastle:bc-fips`.

The following example uses Bouncy Castle FIPS `2.1.1` (NIST 4943 FIPS 140-3 Active, check NIST directly for the latest validation status).

Maven:

```xml
<dependencies>
    <dependency>
        <groupId>org.neo4j.driver</groupId>
        <artifactId>neo4j-java-driver</artifactId>
    </dependency>
    <dependency>
        <groupId>org.bouncycastle</groupId>
        <artifactId>bc-fips</artifactId>
        <version>2.1.1</version>
    </dependency>
</dependencies>
```

Driver:

```java
var provider = new BouncyCastleFipsProvider();
var ivSecureRandom = SecureRandom.getInstance("NONCEANDIV", provider);
var keyEncapsulationService = KeyEncapsulationServices.local(
        masterKey,
        provider,
        ivSecureRandom
);
var profile = EnvelopePropertyEncryptionProfile.builder(
                "profile-name",
                keyEncapsulationService,
                keyRepository
        ).withCryptoContext(provider, ivSecureRandom)
        .build();
var config = Config.builder()
        .withPropertyEncryptionProfiles(profile)
        .build();
```

A similar approach can be used with `org.bouncycastle:bc-fips:1.0.2.4` (NIST 4616 FIPS 140-2 Historical, check NIST directly for the latest validation status).
D
Dmitriy Tverdiakov committed
d9053419b8fda105895003df157a53dac0f6be89
Parent: f9c3511
Committed by GitHub <noreply@github.com> on 9/21/2026, 11:11:03 AM