Using Themis in Ruby

Introduction

The Ruby extension (wrapper) provides access to the features and functions of the Themis cryptographic library:

  • Key generation: creation of public/private key pairs, used in Secure Message and Secure Session.
  • Secure Message: 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: a session between two peers, within which the data can be securely exchanged with higher security guarantees. EC + ECDH, AES container.
  • Secure Comparator: comparison of a secret between two parties without leaking anything related to the secret: Zero-Knowledge Proof-based authentication system. Hardened Socialist Millionaire Protocol + ed25519.

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

There are also example console utils available for the Ruby wrapper for Themis (as well as for some other wrappers — see the full list here). They help understand the specific mechanics of encryption/decryption processes of this specific wrapper. You can find the example console utils for the Ruby wrapper here.

Supported versions

rbthemis is tested on ruby 2.4 and higher.

Quickstart

Installing stable version from RubyGems

RubyThemis wrapper is available on RubyGems. In order to use the wrapper you still need to have the core library installed.

1. Install Themis Core as a system library using your system's package manager.

2. Install RubyThemis gem: gem install rbthemis

NOTE: You'll need to add sudo if you're installing it system-wide, not just in a dedicated or virtual user environment.

⚠️ IMPORTANT: Recently we have renamed the gem to rbthemis. If you were previously using an older version called rubythemis, please uninstall it from your system using bash gem uninstall rubythemis before installing rbthemis.

Building latest version from source

If the stable package version does not suit your needs, you can manually build and install the latest version of Themis from source code.

IMPORTANT: Remember that 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). In either case, we strongly recommend you using the most recent version of these packages.

1. Build and install Themis Core library into your system.

2. Build and install RubyThemis wrapper from source code.

Importing Themis

Add the following to your code:

require 'rbthemis'

and you're good to go!

Examples

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

class SKeyPairGen
  def ec
  def rsa
end

Description:

  • ec
    Return private key and public key strings for Elliptic Curve algorithm.
    Raises ThemisError on failure.

  • rsa
    Return private key and public key strings for RSA algorithm.
    Raises ThemisError on failure.

Example

generator = Themis::SKeyPairGen.new
private_key, public_key = generator.ec
private_key, public_key = generator.rsa

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 using appropriate methods. The sender uses wrap and unwrap methods for encrypt/decrypt mode. A valid public key of the receiver and a private key of the sender are required in this mode. For sign/verify mode sign and verify methods should be used. They only require a private key for signing and a public key for verification respectively.

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

Secure Message interface:

class Smessage
  def initialize(private_key, peer_public_key)
  def wrap(message)
  def unwrap(message)
end

def s_sign(private_key, message)
def s_verify(peer_public_key, message)

Description:

  • Smessage: encrypted secure message.

  • initialize(private_key, peer_public_key)
    Initialise encrypted Secure Message object with private_key and peer_public_key.
    Raises ThemisError on failure.

  • wrap(message)
    Encrypt message.
    Return encrypted Secure Message container as a binary string.
    Raises ThemisError on failure.

  • unwrap(message)
    Decrypt binary message. Return plaintext. Raises ThemisError on failure.

  • Signed secure message

  • s_sign(private_key, message)
    Sign message with private_key.
    Return signed Secure Message container as a binary string.
    Raises ThemisError on failure.

  • s_verify(peer_public_key, message)
    Verify message with peer_public_key. Peer public key should be from the same keypair as private key.
    Return original message without signature.
    Raises ThemisError on failure.

Example

For a detailed explanation of Secure Message, see Secure Message description.

Initialise encrypter:

smessage = Themis::Smessage.new(private_key, peer_public_key)

Encrypt message:

begin
  encrypted_message = smessage.wrap(message)
rescue ThemisError => e
  # error occured
end

Decrypt message:

begin
  decrypted_message = smessage.unwrap(encrypted_message)
rescue ThemisError => e
  # error occured
end

Sign message:

begin
  signed_message = Themis.s_sign(private_key, message)
rescue ThemisError => e
  # error occured
end

Verify message:

Peer public key should be from the same keypair as private key.

begin
  message = Themis.s_verify(peer_public_key, message_signed_by_peer)
rescue ThemisError => e
  # error occured
end

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.

Secure Cell interface:

class Scell
  def initialize(key, mode)
  def encrypt(message, context = nil)
  def decrypt(message, context = nil)
end

Description:

  • initialize(key, mode)
    Initialise Secure Cell object with master key and mode. Where mode is one of:
  • SEAL_MODE - for Seal mode.
  • TOKEN_PROTECT_MODE - for Token-Protect mode.
  • CONTEXT_IMPRINT_MODE - for Context-Imprint mode.

  • encrypt(message, context = nil)
    Encrypt message with optional context.
    Return encrypted string in seal and context-imprint modes.
    Return an array of two strings (encrypted string and token) in token-protect mode.
    Raises ThemisError on failure.

  • decrypt(message, context = nil)
    Decrypt message with optional context.
    (For token-protect mode pass an array of message and token.)
    Return decrypted string.
    Raises ThemisError on failure.

Examples

Secure Cell Seal Mode
scell_seal = Themis::Scell.new(key, Themis::Scell::SEAL_MODE)

encrypted_message = scell_seal.encrypt(message, context)

decrypted_message = scell_seal.decrypt(encrypted_message, context)
Secure Cell Token-Protect Mode
scell_token = Themis::Scell.new(key, Themis::Scell::TOKEN_PROTECT_MODE)

encrypted_message, token = scell_token.encrypt(message, context)

decrypted_message = scell_token.decrypt([encrypted_message, token], context)
Secure Cell Context-Imprint Mode

NOTE: Context is required in context-imprint mode.

scell_context = Themis::Scell.new(key, Themis::Scell::CONTEXT_IMPRINT_MODE)

encrypted_message = scell_context.encrypt(message, context)

decrypted_message = scell_context.decrypt(encrypted_message, context)

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 interface

class Ssession
  def initialize(id, private_key, transport)
  def established?
  def connect_request
  def wrap(message)
  def unwrap(message)
end

Description:

  • initialize(id, private_key, transport)
    Initialize Secure Session object with peer id, private_key and a transport — reference to callback object with the following interface: ruby class transport < Themis::Callbacks def get_pub_key_by_id(id) # - return public_key associated with id end Raises ThemisError on failure.

  • established?
    Checks whether the connection has been established.
    Raises ThemisError on failure.

  • connect_request
    Create connection initialisation message, send it to the peer.
    Raises ThemisError on failure.

  • wrap(message)
    Encrypts user message for sending.
    Raises ThemisError on failure.

  • unwrap(message)
    Decrypts message from the peer.
    Return a pair of status code and message. If the status is Themis::SEND_AS_IS, the returned message is a connection reply that must be sent to the peer without any corrections. Otherwise it is a decrypted user message received from the peer.
    Raises ThemisError on failure.

Secure Session Workflow

Secure Session can be used in two ways: - send/receive - when communication flow is fully controlled by a Secure Session object. - wrap/unwrap - when communication is controlled by the user.

NOTE: We consider wrap/unwrap more fit for typical Ruby frameworks (we've looked into Ruby on Rails and Eventmachine), so currently Secure Session supports wrap/unwrap mode only. However, if you find that a fully-automatic send/receive mode might be a good use case for something, let us know.

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).

Wrap/Unwrap

Initialise callbacks:

class CallbacksForThemis < Themis::Callbacks
  def get_pub_key_by_id(id)
    # retrieve public_key for id from trusted storage (file, db, etc.)
    return public_key
  end
end
Secure Session client

First, initialisation:

@callbacks = CallbacksForThemis.new
@session = Themis::Ssession.new(id, client_private_key, @callbacks)

# only the client calls this method
connect_request = @session.connect_request
send_to_server(connect_request)

connection_message = receive_from_server()
res, message = @session.unwrap(connection_message)

while res == Themis::SEND_AS_IS
  send_to_server(message)

  connection_message = receive_from_server()
  res, message = @session.unwrap(connection_message)
end

After the loop finishes, Secure Session is established and is ready to be used.

To encrypt an outgoing message use:

encrypted_message = @session.wrap(message)
send_to_server(encrypted_message)

To decrypt the received message use:

encrypted_message = receive_from_server()
message = @session.unwrap(encrypted_message)
Secure Session server

First, initialise everything:

@callbacks = CallbacksForThemis.new
@session = Themis::Ssession.new(id, server_private_key, @callbacks)

# there is not connect_request call - it is a server

# receive message from client
connection_message = receive_from_client()
res, message = @session.unwrap(connection_message)

while res == Themis::SEND_AS_IS
  send_to_client(message)

  connection_message = receive_from_client()
  res, message = @session.unwrap(connection_message)
end

Secure Session is ready. Sending and receiving encrypted messages are the same.

Send encrypted:

encrypted_message = @session.wrap(message)
send_to_client(encrypted_message)

Receive and decrypt:

encrypted_message = receive_from_client()
message = @session.unwrap(encrypted_message)

That's it! See the full example available in docs/examples/ruby.

Secure Comparator

Secure Comparator is an interactive protocol for two parties that compares whether 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 ingrained into the functions and requires minimal integration efforts from the developer.

Secure Comparator interface

class Scomparator
   def initialize(shared_secret)
   def begin_compare
   def proceed_compare(control_message)
   def result
end

Description:

  • initialize(shared_secret)
    Initialise Secure Comparator object with a shared_secret to compare.
    Raises ThemisError on failure.

  • begin_compare
    Return a Secure Comparator initialisation message.
    Raises ThemisError on failure.

  • proceed_compare(control_message)
    Process control_message and return the next one.
    Raises ThemisError on failure.

  • result
    Return the status of comparison:

  • NOT_READY – continue calling proceed_compare
  • MATCH or NOT_MATCH – comparison result

Secure Comparator workflow

Secure Comparator has two parties — called "client" and "server" — the only difference between them is in who starts the comparison.

Secure Comparator client

@comparator = Themis::Scomparator.new('Test shared secret')

# this call is specific to the client
comparison_message = @comparator.begin_compare

while @comparator.result == Themis::Scomparator::NOT_READY do
    user_send_function(comparison_message)
    comparison_message = user_receive_function()
    comparison_message = @comparator.proceed_compare(comparison_message)
end

After the loop finishes, the comparison is over and its result can be checked by calling @comparator.result.

Secure Comparator server

@comparator = Themis::Scomparator.new('Test shared secret')

while @comparator.result == Themis::Scomparator::NOT_READY do
    comparison_message = user_receive_function()
    comparison_message = @comparator.proceed_compare(comparison_message)
    user_send_function(comparison_message)
end

After the loop finishes, the comparison is over and its result can be checked by calling @comparator.result.

That's it! See the full example available in docs/examples/ruby.