java-tdsa-lib
The java-tdsa-lib is used to write Java applications that serve as a participant in MPC protocols. These applications can be Android clients, desktop clients, or even server participants.
Getting Started
To fetch the java-tdsa-lib you need credentials for the Sepior Nexus server.
Maven Project
To be able to get the java-ers-lib first configure your $HOME/.m2/settings.xml file with credentials for the Nexus server, e.g.:
<settings>
<servers>
<server>
<id>sepior-server</id>
<username>$NEXUS_USER</username>
<password>$NEXUS_PASSWORD</password>
</server>
</servers>
</settings>
Then add the following repositories and dependencies to your pom.xml file:
<repositories>
<repository>
<id>maven-public</id>
<url>https://nexus.sepior.net/repository/maven-public</url>
</repository>
<repository>
<id>sepior-server</id>
<url>https://nexus.sepior.net/repository/sepior-tdsa-lib</url>
</repository>
</repositories>
...
<properties>
<java-tdsa-lib.version>X.Y.Z</java-tdsa-lib.version>
</properties>
...
<dependency>
<groupId>com.sepior.tdsa</groupId>
<artifactId>java-tdsa-lib</artifactId>
<version>${java-tdsa-lib.version}</version>
</dependency>
That's it. You can now run
mvn install
Gradle Project
Add the following lines to your $HOME/.gradle/gradle.properties file:
sepiorUser=${NEXUS_USER}
sepiorPassword=${NEXUS_PASSWORD}
Then add the following to the dependencies section of the relevant build.gradle file:
implementation group: 'com.sepior.tdsa', name: 'java-tdsa-lib', version: ${java-tdsa-lib.version}
Main Classes
The API is described using Javadoc which can be obtained from the Nexus Server. These are the main classes and interfaces used to integrate the library into an application.
TdsaClient
This interface contains all the high-level functionality of the MPC system. It has methods for key generation, signing, backup, emergency recovery, etc.
TdsaLibraryConfiguration
This class contains configuration data for the participant that the application implements. Things like session timeout, number of concurrent sessions, addresses and public keys for the other participants are configured from this class. It also allows loading the configuration from a YAML file.
Here is an example configuration for a client:
remoteParticipants:
-
- - id: server1
uri: https://server1.example.com
primaryPublicKey: MCowBQYDK2VuAyEASHbSMtlNJl_tyJK0zrbE0yRw6R0_h9TYAXP_q9Y6R2I
secondaryPublicKey: MEIwBQYDK2VvAzkA9RiRjfQ8xerJ86nGqnrk9BRKqfDQpY8ZtPmkAzSu0NjaUTNXzBgWp1DiJciu_SIotnvDJRZ6cPY
- - id: server2
uri: https://server2.example.com
primaryPublicKey: MCowBQYDK2VuAyEAzfE8CqpH88wE61WQSxRYVxVBUjgNjNEOqyg8HhOS5Qk
secondaryPublicKey: MEIwBQYDK2VvAzkA8lpsj-V14lfxe1oxvwr68XyRFZv17NaZ3ii24rnTy1532v2GpcvB4D8bZrda-7KOvpXDpqvYh2o
participantIds:
-
- server1
- server2
clientIndex: 0
participantIndex: 0
storageConfiguration:
encryptionKey: BYN2T7JImuY3NW5xnSt2XUprRidXc4IJ
defaultPresignatureLimit: 100
retries: 2
retryDelay: 1000
TdsaFactory
This is the factory class used to create an actual instance of TdsaClient using a TdsaLibraryConfiguration and an implementation of KeyStorage.
To summarize: the task of an integrator (for a client application) is to provide an implementation of KeyStorage and create an instance of TdsaLibraryConfiguration. These are used to obtain a TdsaClient from TdsaFactory.
Below the TdsaClient interface is listed as version 3.3.2 of java-tdsa-lib
package com.sepior.tdsa.integration;
/**
* A client that can initiate TDSA sessions and access local key storage.
*/
public interface TdsaClient {
/**
* Locally initializes an empty key and generates associated authentication key pairs. These key pairs are used to
* authenticate all operations on the key at the server participant.
*
* @param protocolId The protocol to use for the new key.
* @return A KeyInitResult that must be registered at server participants in order for keyGen() and presigGen() calls to succeed.
* @throws TdsaInputException If protocolId is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
KeyInitResult keyInit(String protocolId) throws TdsaInputException, TdsaException;
/**
* Performs multiparty computation to generate a secret shared ECDSA signing key. The private master key share, the public master key,
* and the master chain code are securely stored at the caller. The key must have been initialized by a call to keyInit() and the
* KeyRequest must have been registered at the other participants.
*
* @param keyId Key identifier.
* @throws TdsaInputException If keyId is invalid
* @throws TdsaSessionException If the mpc protocol. This can happen if the key was not initialized
* or registered at server participants or in case of communication errors or if a participant behaves maliciously.
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
void keyGen(String keyId) throws TdsaInputException, TdsaSessionException, TdsaException;
/**
* Randomizes the key shares for a given key and device. This will invalidate key shares and
* presignatures on all other devices. If this operation fails it should be attempted again at a later point in
* time until it succeeds.
*
* @param keyId Key identifier.
* @throws TdsaInputException If keyId is invalid
* @throws TdsaSessionException If the mpc protocol fails. This can happen in case of communication errors or if a participant behaves maliciously.
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
void reshare(String keyId) throws TdsaInputException, TdsaSessionException, TdsaException;
/**
* Performs multiparty computation to generate presignatures that are used to produce full signatures during signing.
* This operation also synchronizes the presignature state between all parties. Each sign operation will consume a presignature.
*
* @param keyId Key identifier.
* @param deviceId Device identifier. All the generated presignatures will be associated with this device.
* @param count The number of presignatures to generate.
* @return The number of presignatures actually generated (can be less than the number requested).
* @throws TdsaInputException If input is invalid
* @throws TdsaSessionException If the mpc protocol fails. This can happen in case of communication errors or if a participant behaves maliciously.
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
int presigGen(String keyId, String deviceId, int count) throws TdsaInputException, TdsaSessionException, TdsaException;
/**
* Generates a full or partial signature if at least one presignature is available for the given key and device. This method is used by
* the first participant who initiates a sign operation.
*
* @param keyId Key identifier.
* @param deviceId Device identifier.
* @param messageHash A hash of the message to sign. The hash length must match the size of underlying group order.
* Let n be the size of the group in bits and use a hash length of 256 bits if n ≤ 256, 384 bits if 256 < n ≤ 384 and
* 512 bits otherwise.
* @param chainPath The chain path specifying the key to be used for signing according to BIP-32. Must have length > 0.
* @return Full or partial signature.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If no presignatures are available or if the operation fails for other reasons (E.g. storage or configuration failure).
*/
SignResult sign(String keyId, String deviceId, byte[] messageHash, int[] chainPath) throws TdsaInputException, TdsaException;
/**
* Generates a full or partial signature if a presignature matching the partial signature is available. This method is used by
* participants who needs to process a partial signature from another participant.
*
* @param keyId Key identifier.
* @param messageHash A hash of the message to sign. The hash length must match the size of underlying group order.
* Let n be the size of the group in bits and use a hash length of 256 bits if n ≤ 256, 384 bits if 256 < n ≤ 384 and
* 512 bits otherwise.
* @param chainPath The chain path specifying the key to be used for signing according to BIP-32. Must have length > 0.
* @param partialSignature A partial signature obtained from another participant. For the first participant this value must be null.
* @return Full or partial signature.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If no presignatures are available or if the operation fails for other reasons (E.g. storage or configuration failure).
*/
SignResult sign(String keyId, byte[] messageHash, int[] chainPath, byte[] partialSignature) throws TdsaInputException, TdsaException;
/**
* Gets the number of presignatures for a given key and device.
*
* @param keyId Key identifier.
* @param deviceId Device identifier. If null then presignatures for all devices are included.
* @return Number of available presignatures.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
int getPresigCount(String keyId, String deviceId) throws TdsaInputException, TdsaException;
/**
* Deletes all presignatures for the given key and device.
*
* @param keyId Key identifier.
* @param deviceId Device identifier. If null then presignatures for all devices are deleted.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
void deletePresigs(String keyId, String deviceId) throws TdsaInputException, TdsaException;
/**
* Gets the maximum number of presignatures that can be stored in the local database for a given key. This is a
* global limit that also includes presignatures used to initiate sign operations by other participants.
*
* @param keyId Key identifier.
* @return Maximum number of presignatures.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
int getPresigLimit(String keyId) throws TdsaInputException, TdsaException;
/**
* Sets the maximum number of presignatures that can be stored in the local database for a given key. This is a
* global limit that also includes presignatures used to initiate sign operations by other participants.
*
* @param keyId Key identifier.
* @param presigLimit Maximum number of presignatures.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
void setPresigLimit(String keyId, int presigLimit) throws TdsaInputException, TdsaException;
/**
* Deletes all local data associated with a given key. This does not affect the key material stored at the
* other participants.
*
* @param keyId Key Identifier.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
void deleteKey(String keyId) throws TdsaInputException, TdsaException;
/**
* Gets information about a given key.
*
* @param keyId Key Identifier.
* @return Key information or null if the key does not exist.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
KeyInfo getKeyInfo(String keyId) throws TdsaInputException, TdsaException;
/**
* Gets the public key for a child key of a private key share.
*
* @param keyId Key identifier.
* @param chainPath The chain path specifying the key to be used according to BIP-32. Must have length > 0.
* @return a DER encoding of the SubjectPublicKeyInfo ASN.1 type from X.509. This corresponds to the format used by OpenSSL. See RFC5480 for more information.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
byte[] getPublicKey(String keyId, int[] chainPath) throws TdsaInputException, TdsaException;
/**
* Gets the xpub for a child key of a private key share.
*
* @param keyId Key identifier.
* @param chainPath The chain path specifying the key to be used according to BIP-32. Must have length > 2.
* @return a Base58Check encoding of the xpub.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
String getXpub(String keyId, int[] chainPath) throws TdsaInputException, TdsaException;
/**
* Gets ERS certificates.
*
* @return Public certificates of the emergency recovery services.
* @throws TdsaException If the operation fails.
*/
byte[][] getErsCertificates() throws TdsaException;
/**
* Gets an encryption of the private key share, the BIP-32 master chain code and the auxiliary data under the public key
* of the ERS.
* <p>
* Data is encrypted with AES-GCM using a randomly generated 256-bit key and all header information is authenticated.
* Then the random AES key is encrypted with the ERS public public key using RSA-OAEP with SHA-1 and MGF1 padding.
*
* @param keyId Key identifier.
* @param ersPublicKey ERS public key to use to encrypt the recovery data. Must be an RSA public key represented as a DER
* encoding of the SubjectPublicKeyInfo ASN.1 type from X.509.
* @param auxiliaryData Additional optional data to include in the encrypted recovery info.
* @return Encrypted recovery information.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
byte[] getRecoveryInfo(String keyId, byte[] ersPublicKey, byte[] auxiliaryData) throws TdsaInputException, TdsaException;
/**
* Checks that all metadata in the recovery infos match. If this check fails then the recovery infos might not be
* enough for the ERS to reconstruct the private key.
*
* @param recoveryInfos List of recovery infos obtained from different participants.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
*/
void checkRecoveryInfos(byte[]... recoveryInfos) throws TdsaInputException, TdsaException;
/**
* Gets an encryption of all key data and the auxiliary data under a key derived from the provided secret.
* <p>
* Data is encrypted with AES-GCM using a 256-bit key. The encryption key is derived from the secret using scrypt
* with the salt SEPIOR and parameters N = 1024, r = 8, p = 16.
*
* @param keyId Key identifier.
* @param auxiliaryData Additional data to include in the encrypted backup.
* @param secret Secret value used to derive the encryption key. Can be of any length.
* @return Encrypted backup archive.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
* @see #restore
*/
byte[] backup(String keyId, byte[] auxiliaryData, byte[] secret) throws TdsaInputException, TdsaException;
/**
* Decrypts encrypted key data and returns the extracted key id and auxiliary data.
* Nothing will be restored by calling this method.
* Restore the key data using {@link #restore(byte[], byte[])}}.
*
* @param encryptedBackup Encrypted backup data generated by a call to {@link #backup(String, byte[], byte[])}.
* @param secret Secret value used to derive the decryption key. Can be of any length.
* @return Backed up key id and auxiliary data.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
* @see #backup
*/
RestoreResult extractAuxiliaryData(byte[] encryptedBackup, byte[] secret) throws TdsaInputException, TdsaException;
/**
* Imports encrypted key data and populates the TDSAClient with the key data associated with
* the returned key id.
* Also returns some auxiliary data if present.
*
* @param encryptedBackup Encrypted backup data generated by a call to {@link #backup(String, byte[], byte[])}.
* @param secret Secret value used to derive the decryption key. Can be of any length.
* @return Backed up key id and auxiliary data.
* @throws TdsaInputException If input is invalid
* @throws TdsaException If the operation fails for other reasons (E.g. storage or configuration failure).
* @see #backup
*/
RestoreResult restore(byte[] encryptedBackup, byte[] secret) throws TdsaInputException, TdsaException;
final class KeyInitResult {
private final String keyId;
private final byte[] keyRequest;
public KeyInitResult(String keyId, byte[] keyRequest) {
this.keyId = keyId;
this.keyRequest = keyRequest;
}
/**
* Gets the key id of the generated key.
*
* @return Key identifier.
*/
public String getKeyId() {
return keyId;
}
/**
* Gets the key request that is to be registered at server participants.
* This is necessary to authorize subsequent operations for the key in question.
*
* @return Key request.
*/
public byte[] getKeyRequest() {
return keyRequest;
}
}
final class KeyInfo {
private final String keyId;
private final String keySharingId;
public KeyInfo(String keyId, String keySharingId) {
this.keyId = keyId;
this.keySharingId = keySharingId;
}
/**
* Gets the key identifier.
*
* @return Key identifier.
*/
public String getKeyId() {
return keyId;
}
/**
* Gets the key sharing identifier.
*
* @return Key sharing identifier.
*/
public String getKeySharingId() {
return keySharingId;
}
}
final class SignResult {
private final byte[] signature;
private final boolean isFullSignature;
private final int recoveryId;
public SignResult(byte[] signature, boolean isFullSignature, int recoveryId) {
this.signature = signature;
this.isFullSignature = isFullSignature;
this.recoveryId = recoveryId;
}
/**
* Gets an encoding of the signature (full or partial).
* The full signature is a DER encoding of the r and s signature values. This corresponds to the format used by OpenSSL. See RFC5480 for more information.
* <p>
* The partial signature uses a different format that is only relevant for the Sepior library.
*
* @return Full or partial signature.
*/
public byte[] getSignature() {
return signature;
}
/**
* Is the signature a full or partial signature.
*
* @return true if the wrapped signature is a full signature and false if it is a partial signature
*/
public boolean isFullSignature() {
return isFullSignature;
}
/**
* The recovery id makes it possible to reconstruct the public key given a signature.
*
* @return Recovery id.
*/
public int getRecoveryId() {
return recoveryId;
}
}
final class RestoreResult {
private final String keyId;
private final byte[] auxiliaryData;
public
RestoreResult(String keyId, byte[] auxiliaryData) {
this.keyId = keyId;
this.auxiliaryData = auxiliaryData;
}
/**
* The key id of the restored key.
*
* @return Key identifier.
*/
public String getKeyId() {
return keyId;
}
/**
* The auxiliary data that was backed up together with key data.
*
* @return Auxiliary data.
*/
public byte[] getAuxiliaryData() {
return auxiliaryData;
}
}
}
Database Encryption
If the parameter storageConfiguration.encryptionKey is specified then database records are encrypted using a default encryptor which uses AES-GCM to encrypt data, but it is also possible to use a custom encryptor, e.g. to use a HSM, a different key derivation method or another algorithm.
A custom encryptor must implement the following interface:
public interface Encryptor {
String getName();
byte[] encrypt(String keyId, byte[] plaintext);
byte[] decrypt(String keyId, byte[] ciphertext);
}
Here is a description of each method that must be implemented:
Method | Description |
---|---|
String getName() | Returns the name of the encryptor. This is only used for logging purposes. |
byte[] encrypt(String keyId, byte[] plaintext) | Takes a keyId and some plaintext and returns an authenticated encryption of the plaintext with keyId as additional authenticated data. |
byte[] decrypt(String keyId, byte[] ciphertext) | Performs authenticated decryption of the ciphertext with the keyId as additional authenticated data. Must throw an IllegalArgumentException if decryption fails. |
The configuration parameter storageConfiguration.encryptorClass must be set to the full class name of the implementation.
Arguments to the constructor of the custom implementation can be specified in the configuration parameter storageConfiguration.encryptorArguments. A value of DATABASE_ENCRYPTION_KEY will be replaced with the contents of the configuration parameter storageConfiguration.encryptionKey. If the constructor does not take any arguments then this configuration parameter can be omitted.
Android
The TDSA library works on Android as well. However, there are a few things to consider.
The interface to the underlying storage is through a DataSource object. However, most storage providers' DataSource does not work on Android due to missing classes. We provide an AndroidDataSource class which provides a file-backed DataSource implementation. It can be used like this:
String databaseFileName = context.getApplicationInfo().dataDir + "/keystorage.db";
DataSource ds = new AndroidDataSource(databaseFileName);
Encrypting data at rest works just as for normal Java. However, it is recommended to write a custom encryptor that makes use of the Android key storage.
Updated 10 months ago