Clients

Java client

The Java client is what your application uses to get its secrets: it loads a vault and hands your code the secrets you ask for.

Install it

Requires Java 17 or later.

The client is published to Valt0's own Maven repository, https://maven.valt0.com, as com.valt0:valt0-client. Add the repository to your build, then the dependency. No account or token is needed.

Its package and its module are both com.valt0, so on the module path your module-info.java needs requires com.valt0;. It works on the class path too, and it has no dependencies of its own.

With Maven, in pom.xml. Only the valt0 repository is required. The central entry is optional: listed first, it makes Maven try Central before Valt0's repository, so your other dependencies are not requested from maven.valt0.com:

Maven
<repositories>
    <!-- Optional: Central first, so other dependencies skip maven.valt0.com -->
    <repository>
        <id>central</id>
        <url>https://repo.maven.apache.org/maven2</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    <repository>
        <id>valt0</id>
        <url>https://maven.valt0.com</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>com.valt0</groupId>
        <artifactId>valt0-client</artifactId>
        <version>1.0.1</version>
    </dependency>
</dependencies>

With Gradle, in build.gradle.kts:

Gradle (Kotlin)
repositories {
    mavenCentral()
    // com.valt0 from Valt0's repository, everything else from Central
    exclusiveContent {
        forRepository { maven("https://maven.valt0.com") }
        filter { includeGroup("com.valt0") }
    }
}

dependencies {
    implementation("com.valt0:valt0-client:1.0.1")
}

Or, with the Groovy DSL, in build.gradle:

Gradle (Groovy)
repositories {
    mavenCentral()
    // com.valt0 from Valt0's repository, everything else from Central
    exclusiveContent {
        forRepository {
            maven { url = 'https://maven.valt0.com' }
        }
        filter { includeGroup 'com.valt0' }
    }
}

dependencies {
    implementation 'com.valt0:valt0-client:1.0.1'
}

Open a vault

A vault is opened by its id; the name is a label you choose.

Valt0.openVault loads a vault from the local service. Pass it the id that valt0 info prints for the vault. The service can only open a vault that has been imported into it. A vault you create is imported for you; for one made somewhere else, run valt0 import first.

It throws a Valt0Exception, a checked exception, if the service cannot be reached or the vault cannot be loaded. To keep them short, the examples on this page leave that exception unhandled; Handle errors, below, shows how to catch it.

Java
import com.valt0.Secret;
import com.valt0.Valt0;
import com.valt0.Vault;

Vault vault = Valt0.openVault("your-vault-id", "your-vault-name");

Read a secret

Ask for a secret by name, use it, then destroy it.

vault.secret(name) fetches one secret and returns a Secret. Declare it in a try-with-resources block, which zeroes the secret's array when the block exits. Take the value out as bytes where you can: bytes() hands back the secret's own array, so the block wipes it too, and anything still holding that array afterwards sees only zeros.

For an API that takes a char[], such as KeyStore or PBEKeySpec, chars() gives you a new array that you wipe yourself. If you need the secret as a string use reveal(). The example shows all three; use the one your code needs.

Java
// try-with-resources zeroes the underlying array as soon as you are finished
try (Secret secret = vault.secret("DATABASE_PASSWORD")) {
    // Preferred: the raw bytes, wiped on close
    byte[] pw = secret.bytes();

    // For APIs that take a char[]: a copy you wipe yourself
    char[] chars = secret.chars();
    keyStore.load(in, chars);
    java.util.Arrays.fill(chars, '\0');

    // Or, when a String is unavoidable
    db.connect(secret.reveal());
}

Without try-with-resources, call destroy() yourself, in a finally so it runs even when something fails:

Java
Secret secret = vault.secret("DATABASE_PASSWORD");
try {
    db.connect(secret.reveal());
} finally {
    secret.destroy();
}

Unless you ask for its value, a Secret prints as [REDACTED]: toString(), a string concatenation, String.format and a log line alike. It is not Serializable or Cloneable, and it keeps its value in a transient field that reflection-based serializers such as Gson skip. The value comes out only through reveal(), chars() or bytes().

Java
System.out.println(secret);                      // [REDACTED]
"pw=" + secret                                   // pw=[REDACTED]
String.format("%s", secret)                      // [REDACTED]
log.info("connecting with {}", secret);          // connecting with [REDACTED]
new ObjectOutputStream(out).writeObject(secret); // NotSerializableException

secret.reveal();                                 // the actual value

Handle errors

The two ways a connection fails have exceptions of their own.

Valt0.openVault and vault.secret throw CantConnectToServiceException when they cannot find the service, which usually means the service is not running: valt0 status tells you whether it is. They throw NoServiceResponseException when the service did not answer within the one second the client waits.

Both extend Valt0Exception, the base class of every exception the client throws, so catch that last for anything else, such as a vault that could not be loaded. All three are checked exceptions.

Java
import com.valt0.CantConnectToServiceException;
import com.valt0.NoServiceResponseException;
import com.valt0.Valt0Exception;

try {
    Vault vault = Valt0.openVault("your-vault-id", "your-vault-name");
    try (Secret secret = vault.secret("DATABASE_PASSWORD")) {
        db.connect(secret.reveal());
    }
} catch (CantConnectToServiceException e) {
    // the service was not found: is it running?
} catch (NoServiceResponseException e) {
    // the service did not answer within the transport timeout (1s)
} catch (Valt0Exception e) {
    // any other error, e.g. the vault could not be loaded
}

Use it from Kotlin

The same library works from Kotlin on the JVM.

Kotlin calls the client directly, and you add it with the same Gradle lines as above. Two things read differently from the Java examples.

use takes the place of try-with-resources. Secret is AutoCloseable, so use destroys it when the block ends, even when something in it fails.

Kotlin has no checked exceptions, so the calls compile without a try. Catch Valt0Exception, or one of its two subclasses, where you want to handle a failure.

Kotlin
import com.valt0.Valt0

val vault = Valt0.openVault("your-vault-id", "your-vault-name")

// use zeroes the underlying array as soon as the block ends
vault.secret("DATABASE_PASSWORD").use { secret ->
    // Preferred: the raw bytes, wiped when the block ends
    val pw = secret.bytes()

    // Or, when a String is unavoidable
    db.connect(secret.reveal())
}

Threads and virtual threads

The client is synchronous and safe to share between threads.

Every call uses its own connection, so one vault can be used from several threads at once. It works the same from platform threads and from virtual threads on Java 21 and later, and a call that is waiting on the service does not pin a virtual thread to its carrier.

API

Valt0.openVault(id, name): Vault

Opens the vault identified by id. Throws a Valt0Exception if the service cannot be reached or the vault cannot be loaded; the two connection failures are CantConnectToServiceException and NoServiceResponseException.

An overload takes a VaultOptions instead, for when you would rather pass the two values as one: new VaultOptions(id, name).

Options

idString
The vault's id, the one valt0 info prints. It is the only value used to find the vault.
nameString
A label for the vault, used in messages.

Vault

The opened vault. It is an interface, so your own tests can stand in for it.

Methods

id()String
The vault's id.
name()String
The vault's name.
secret(name)Secret
Fetches one secret by name.

Secret

One secret, fetched from a vault. It is AutoCloseable, so a try-with-resources block destroys it when the block exits, and it implements javax.security.auth.Destroyable.

new Secret(bytes) makes one holding a copy of bytes, for a test that stands in for a Vault.

Methods

bytes()byte[]
The raw bytes: the secret's own array, not a copy. Preferable to use where you can, because destroy() wipes it.
chars()char[]
The value as a new array, for APIs that take a char[]. Wipe it yourself when you are done. Throws IllegalStateException if the value is not UTF-8 text.
reveal()String
The value as a string. A string cannot be wiped, so use it sparingly. Throws IllegalStateException if the value is not UTF-8 text.
destroy()
Zeroes the array. Calling it again does nothing.
close()
Calls destroy(), for try-with-resources.
isDestroyed()boolean
Whether the secret has been destroyed.