Java and Android Howto

vixentael edited this page Aug 1, 2018 · 29 revisions

Using Themis with Java / Android

Introduction

The Java / Android Themis library provides access to the features and functions of the Themis cryptographic library:

  • Key generation: the creation of public/private key pairs, used in Secure Message and Secure Session.
  • Secure Message: the secure exchange of messages between two parties. RSA + PSS + PKCS#7 or ECC + ECDSA (based on key choice), AES GCM container.
  • Secure Storage (aka Secure Cell): provides secure storage of record based data through symmetric encryption and data authentication. AES GCM / AES CTR containers.
  • Secure Session: the establishment of a session between two peers, within which the data can be securely exchanged with higher security guarantees. EC + ECDH, AES container.

You can learn more about the Themis library in our general documentation.

Examples

Quickstart

Building Themis for Java

Requirements

In addition to the common set of build tools, Themis currently requires either the OpenSSL or LibreSSL package with the developer version of the package (as it provides header files). JDK is also required for building a Java project. More information on getting and installing JDK is here.

Installing Themis and Themis JNI wrapper

Get Themis source code from GitHub:

git clone https://github.com/cossacklabs/themis.git

Note that the default installation assumes the use of the standard LibreSSL/OpenSSL library and will install Themis to the standard /usr/lib and /usr/include locations.

In a typical case, all you need to do is (depending on your rights, sudo might be necessary):

make install

If your path is different, please see Building and installing for more details on how to change it.

Install the Themis JNI wrapper:

make themis_jni

To build JNI code for Java, the build system needs to know the location of jni.h header. By default, it will be looking for the file in $(JAVA_HOME)/include directory. You may specify a different location by setting JDK_INCLUDE_PATH environment variable before running the make commands provided above.

On Mac OS, you might need to explicitly point to Java headers:

export CFLAGS=-I/System/Library/Frameworks/JavaVM.framework/Headers
make themis_jni

If you want to compile themis_jni with BoringSSL, make sure to install BoringSSL as described in Building and installing section, then run:

make clean ENGINE=boringssl CRYPTO_ENGINE_PATH=boringssl ENGINE_INCLUDE_PATH=$BORINGSSL_HOME/boringssl/include ENGINE_LIB_PATH=$BORINGSSL_HOME/boringssl/build/crypto BUILD_PATH=build_with_boringssl_jni all install themis_jni

In the output folder (/build, you will see libthemis_jni shared library).

To build and run core tests, you can use:

make test

All the tests need to be passed with no errors.

Using Themis in your Java project

Use reference .java files from src/wrappers/themis/java/com/cossacklabs/themis in your project build system.

Building Themis for Android

A quick start using Docker

If you have Docker and you just need to build Themis quickly, you can use our prebuilt android-build image:

docker run --rm -it -v $(pwd):/projects cossacklabs/android-build bash -c 'git clone https://github.com/cossacklabs/themis.git && cd themis && git submodule update --init && ./gradlew assembleRelease'
Requirements

Themis for Android uses Gradle build system. A Gradle wrapper is included into the root directory of the project. Like the rest of the modern Android ecosystem, Themis uses BoringSSL as a cryptographic backend. BoringSSL is included as a submodule in the Themis repository.

You should install the latest Android SDK tools/Android Studio, but you don't need full Android Studio to build Themis, just the command line SDK tools.

Compile Themis AAR

Specify paths to your SDK for Gradle:

export ANDROID_HOME=/path/to/android/sdk

Make sure your SDK has the necessary build tools and Android NDK:

$ANDROID_HOME/tools/bin/sdkmanager --update
$ANDROID_HOME/tools/bin/sdkmanager 'build-tools;27.0.3' 'platforms;android-27' 'ndk-bundle'

Since BoringSSL is included as a submodule, make sure all the submodules are initialised in your cloned Themis repository:

git submodule update --init

Build Themis for Android (both Java and Native part):

./gradlew --info assembleRelease

It will create AAR (Android library project) ready to be used in your application in the build/outputs/aar/ folder.

If you want to use SecureSocket and SecureServerSocket classes, make sure you have declared INTERNET permission in your AndroidManifest.xml.

To run Android tests, connect your device(s) and run (make sure ADB is enabled in settings):

./gradlew --info connectedAndroidTest
Using Themis in your Android project

Import Themis AAR into your application project in Android Studio or reference it in your gradle.build file. More about using libraries here.

Examples

  • Android test cases (in the tests directory) are simple self-describing easy-to-understand examples of our APIs usage scenarios. Feel free to explore them.

  • You can also read our blog post Building Secure Chat server, which includes Android client, implementation of Secure Session, and Secure Cell.

Using Themis

Keypair generation

Themis supports both Elliptic Curve and RSA algorithms for asymmetric cryptography. Algorithm type is chosen according to the generated key type. Asymmetric keys are necessary for Secure Message and Secure Session objects.

WARNING: When you distribute private keys to your users, make sure the keys are sufficiently protected. You can find the guidelines here.

NOTE: When using public keys of other peers, make sure they come from trusted sources.

Keypair generation interface

You can easily generate keypairs for Secure Message and Secure Session this way:

Keypair pair = KeypairGenerator.generateKeypair();
PrivateKey privateKey = pair.getPrivateKey();
PublicKey publicKey = pair.getPublicKey();

Secure Message

The Secure Message functions provide a sequence-independent, stateless, contextless messaging system. This may be preferred in cases that don't require frequent sequential message exchange and/or in low-bandwidth contexts. This is secure enough to exchange messages from time to time, but if you'd like to have Perfect Forward Secrecy and higher security guarantees, please consider using Secure Session instead.

The Secure Message functions offer two modes of operation:

In Sign/Verify mode, the message is signed using the sender's private key and is verified by the receiver using the sender's public key. The message is packed in a suitable container and ECDSA is used by default to sign the message (when RSA key is used, RSA+PSS+PKCS#7 digital signature is used).

In Encrypt/Decrypt mode, the message will be encrypted with a randomly generated key (in RSA) or a key derived by ECDH (in ECDSA), via symmetric algorithm with Secure Cell in seal mode (keys are 256 bits long).

The mode is selected by the sender supplying a valid public key of the receiver (encrypt/decrypt) or setting this parameter to NULL, or an empty string to use sign/verify.

Read more about the Secure Message's cryptographic internals here.

Sending many messages to the same recipient
  1. Create a Secure Message object with your PrivateKey and recipient's PublicKey:
SecureMessage encryptor = new SecureMessage(yourPrivateKey, peerPublicKey);
  1. Encrypt each outgoing message:
byte[] encryptedMessage = encryptor.wrap(messageToSend);
  1. Decrypt each incoming message:
byte[] receivedMessage = encryptor.unwrap(wrappedMessage);
Sending messages to many recipients
  1. Create Secure Message object with your PrivateKey:
SecureMessage encryptor = new SecureMessage(yourPrivateKey);
  1. Encrypt each outgoing message specifying recipients' PublicKey:
byte[] encryptedMessage = encryptor.wrap(messageToSend, peerPublicKey);
  1. Decrypt each incoming message specifying the sender's PublicKey:
byte[] receivedMessage = encryptor.unwrap(wrappedMessage, peerPublicKey);
Signing messages
  1. Create Secure Message object with your PrivateKey:
SecureMessage signer = new SecureMessage(yourPrivateKey);
  1. Sign one or more messages:
byte[] signedMessage = signer.sign(message);
Verifying the signed messages
  1. Create Secure Message object with your peer's PublicKey:
SecureMessage verifier = new SecureMessage(peerPublicKey);
  1. Verify the received messages from your peer:
try {
    byte[] verifiedMessage = verifier.verify(signedMessage);
} catch (SecureMessageWrapException e) {
    // invalid signature or other error occured
}

Secure Cell

The Secure Сell functions provide the means of protection for arbitrary data contained in stores, i.e. database records or filesystem files. These functions provide both strong symmetric encryption and data authentication mechanisms.

The general approach is that given:

  • input: some source data to protect,
  • key: a password,
  • context: plus an optional "context information",

Secure Cell functions will produce:

  • cell: the encrypted data,
  • authentication tag: some authentication data.

The purpose of the optional "context information" (i.e. a database row number or file name) is to establish a secure association between this context and the protected data. In short, even when the password is known, if the context is incorrect, the decryption will fail.

The purpose of the authentication data is to verify that given a correct password (and context), the decrypted data is indeed the same as the original source data.

The authentication data must be stored somewhere. The most convenient way is to simply append it to the encrypted data, but this is not always possible due to the storage architecture of an application. The Secure Cell functions offer different variants that address this issue.

By default, the Secure Cell uses the AES-256 encryption algorithm. The generated authentication data is 16 bytes long.

Secure Cell is available in 3 modes:

  • Seal mode: the most secure and user-friendly mode. Your best choice most of the time.
  • Token protect mode: the most secure and user-friendly mode. Your best choice most of the time.
  • Context imprint mode: length-preserving version of Secure Cell with no additional data stored. Should be used with care and caution.

You can learn more about the underlying considerations, limitations, and features here.

Initialising Secure Cell

Create Secure Cell using key as a String or as a Byte Array.

SecureCell cell = new SecureCell(yourSecretByteArrayKey);

// or
SecureCell cell = new SecureCell("your secret password string");

NOTE: When its unspecified, Secure Cell will use SecureCell.MODE_SEAL by default. Read more about the Secure Cell modes and which mode to choose here.

Secure Cell Seal Mode

Initialise cell:

SecureCell cell = new SecureCell("your secret password string", SecureCell.MODE_SEAL);

Encrypt:

// context is optional
SecureCellData cellData = cell.protect(context, data);

The result of the function call is SecureCellData object, which is a simple container for protected data. You may get the actual protected data:

byte[] protectedData = cellData.getProtectedData();

Decrypt:

The context should be same as in the protect function call for successful decryption.

// context is optional
byte[] data = cell.unprotect(context, cellData);
Secure Cell Token-protect Mode

Initialise cell:

SecureCell cell = new SecureCell("your secret password string", SecureCell.MODE_TOKEN_PROTECT);

Encrypt:

// context is optional
SecureCellData cellData = cell.protect(context, data);

In this mode the result holds additional data (opaque to the user, but necessary for successful decryption):

byte[] protectedData = cellData.getProtectedData();

if (cellData.hasAdditionalData()) {
    byte[] additionalData = cellData.getAdditionalData();
}

Decrypt:

// context is optional
byte[] data = cell.unprotect(context, cellData);
Secure Cell Context-Imprint Mode

Initialise cell:

SecureCell cell = new SecureCell("your secret password string", SecureCell.MODE_CONTEXT_IMPRINT);

Encrypt:

// context required
SecureCellData cellData = cell.protect(context, data);

byte[] protectedData = cellData.getProtectedData();

Decrypt:

Note: For successful decryption, the context should be same as in the protect function call.

// context is required
byte[] data = cell.unprotect(context, cellData);

You can also use one object to encrypt different data with different keys:

SecureCellData cellData1 = cell.protect(key1, context1, data1);
...
SecureCellData cellData2 = cell.protect(key2, context2, data2);
...

Secure Session

Secure Session is a sequence- and session- dependent, stateful messaging system. It is suitable for protecting long-lived peer-to-peer message exchanges where the secure data exchange is tied to a specific session context.

Secure Session operates in two stages:

  • session negotiation where the keys are established and cryptographic material is exchanged to generate ephemeral keys and
  • data exchange where exchanging of messages can be carried out between peers.

You can read a more detailed description of the process here.

Put simply, Secure Session takes the following form:

  • Both clients and server construct a Secure Session object, providing:
    • an arbitrary identifier,
    • a private key, and
    • a callback function that enables it to acquire the public key of the peers with which they may establish communication.
  • A client will generate a "connect request" and by whatever means it will dispatch that to the server.
  • A server will enter a negotiation phase in response to a client's "connect request".
  • Clients and servers will exchange messages until a "connection" is established.
  • Once a connection is established, clients and servers may exchange secure messages according to whatever application level protocol was chosen.
Secure Session Workflow

Secure Session has two parties called "client" and "server" for the sake of simplicity, but they could be more precisely called "initiator" and "acceptor" — the only difference between them is in who starts the communication.

Secure Session relies on the user's passing a number of callback functions to send/receive messages — and the keys are retrieved from local storage (see more in Secure Session cryptosystem description).

Secure Sockets

If your application already uses Java sockets for communication, you can easily add/increase security by replacing them with our SecureSocket and SecureServerSocket.

  1. Implement ISessionCallbacks interface:
  • getPublicKeyForId will return peer's trusted public key when it is needed by the system.
  • stateChanged is just a notification callback. You may use it for informational purpose, to update your UI, or just to create a dummy (do-nothing) implementation.

Example using anonymous class:

ISessionCallbacks callbacks = new ISessionCallbacks() {
    @Override
    public PublicKey getPublicKeyForId(SecureSession session, byte[] id) {
        // get trusted PublicKey of user id
        PublicKey publicKey = getUserPublicKeyFromDatabaseOrOtherStorageOrSource(id);
        return publicKey; // or null if key is not found
    }

    @Override
    public void stateChanged(SecureSession session) {
       // update UI: for example, draw a nice padlock signaling to the user that his/her communication is now secured
    }
}
  1. Replace all of your sockets with our secure versions:

On client:

// Socket clientSocket = new Socket(...);
Socket clientSocket = new SecureSocket(..., clientId, clientPrivateKey, callbacks);

On server:

// ServerSocket serverSocket = new ServerSocket(...);
ServerSocket serverSocket = new SecureServerSocket(..., serverId, serverPrivateKey, callbacks);
  1. Enjoy
Basic Secure Session

This API is useful when your application has an already established network processing path and this path is doing more than just use sockets. In this case, you would just want to add some function calls that wrap/unwrap the outgoing/incoming data buffers.

  1. Similarly to the case with Secure Sockets — implement ISessionCallbacks interface:
ISessionCallbacks callbacks = new ISessionCallbacks() {
    @Override
    public PublicKey getPublicKeyForId(SecureSession session, byte[] id) {
        // get trusted PublicKey of user id
        PublicKey publicKey = getUserPublicKeyFromDatabaseOrOtherStorageOrSource(id);
        return publicKey; // or null if key is not found
    }
        
    @Override
    public void stateChanged(SecureSession session) {
       // update UI: for example, draw a nice padlock indicating to the user that his/her communication is now secured
    }
}
  1. Create a SecureSession object:
SecureSession session = new SecureSession(yourId, yourPrivateKey, callbacks);
  1. On the client side, initiate the Secure Session negotiation by generating and sending a connection request:
byte[] connectRequest = session.generateConnectRequest();
// send connectRequest to the server
  1. Start receiving and parsing incoming data on both sides:
// receive some data and store it in receiveBuffer
SecureSession.UnwrapResult result = session.unwrap(receiveBuffer);
        
switch (result.getDataType()) {
    case USER_DATA:
        // this is the actual data that was encrypted by your peer using SecureSession.wrap
        byte[] data = result.getData();
        // process the data according to your application's flow for incoming data
        break;
    case PROTOCOL_DATA:
        // this is the internal Secure Session protocol data. An opaque response was generated, just send it to your peer
        byte[] data = result.getData();
        // send the data to your peer as is
        break;
    case NO_DATA:
        // this is the internal Secure Session protocol data, but no response is needed (this usually takes place on the client side when protocol negotiation completes)
        // do nothing
        break;
}
  1. When the protocol negotiation is completed, you may send the encrypted data to your peer:
byte[] wrappedData = session.wrap(yourData);
// send wrappedData to your peer
Secure Session with transport callbacks

This API is useful when you want to clearly decouple security from the network communication in your application:

  1. Implement ITransportSessionCallbacks interface. This interface extends ISessionCallbacks interface, which means you have to implement two additional functions:
ITransportSessionCallbacks callbacks = new ITransportSessionCallbacks() {
    // implement getPublicKeyForId and stateChanged as in basic ISessionCallbacks
    ...
            
    @Override
    public void write(byte[] buffer) {
        // it will be called when Secure Session needs to send something to your peer
        // just send buffer to your peer
    }
        
    @Override
    public byte[] read() {
        // here you should issue a read request to your underlying transport (for example, read data from socket or pipe)
        // return the buffer with read data
    }
}
  1. Create a SecureTransportSession object:
SecureTransportSession session = new SecureTransportSession(yourId, yourPrivateKey, callbacks);
  1. On the client side, initiate the Secure Session negotiation by sending a connection request:
session.connect();
  1. When the negotiation is complete, you may send/receive the data on both sides:
// sending data
session.write(dataToSend);
        
...
        
// receiving data (probably, through a receive loop)
byte[] receivedData = session.read();

That's it!

Please see the tests files and Mobile WebSocket Example to get a more complete understanding of how Secure Session works.

Secure Comparator

Secure Comparator is an interactive protocol for two parties, which compares and defines if they share the same secret or not. It is built around a Zero Knowledge Proof-based protocol (Socialist Millionaire's Protocol), with a number of security enhancements.

Secure Comparator is transport-agnostic and only requires the user(s) to pass messages in a certain sequence. The protocol itself is already integrated into the functions and requires minimal integration efforts from the developer.

The workflow of Secure Comparator

Secure Comparator has two participating sides. They are called client and server. The only difference between them is in the order of actions (who starts the comparison).

Secure Comparator client
byte[] compareData = ... // shared secret to compare
SecureCompare client = new SecureCompare(compareData);

// Initiating secure compare (client, step1)
byte[] peerData = client.begin();

while (client.getResult() == SecureCompare.CompareResult.NOT_READY) {
    // send data on server and receive response
    sendDataOnServer(peerData);
    peerData = receiveFromServer();

    // proceed and send again
    peerData = client.proceed(peerData);
}

After the loop ends, the comparison is over and its result can be checked through calling getResult:

if (client.getResult() == SecureCompare.CompareResult.MATCH) {
    // secrets match
} else {
    // secrets don't match
}
Secure Comparator server

The server part can be described in any language, but let's pretend that both client and server are using Java:

byte[] compareData = // shared secret to compare
SecureCompare server = new SecureCompare(compareData);

// Initiating secure compare (client, step1)
byte[] peerData = new byte[0];

while (server.getResult() == SecureCompare.CompareResult.NOT_READY) {
    // receive from client
    peerData = receiveFromClient();

    // proceed and send again
    peerData = server.proceed(peerData);
    sendDataOnClient(peerData);
}

After the loop finishes, the comparison is over and its result can be checked through calling getResult:

if (server.getResult() == SecureCompare.CompareResult.MATCH) {
    // secrets match
} else {
    // secrets don't match
}
Clone this wiki locally
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.