Introduction
This repository provides the e4
Go package, the client library for Teserakt's E4, and end-to-end encryption and key management framework for MQTT and other publish-subscribe protocols.
The e4
package defines a Client
object that has a minimal interface, making its integration straightforward via the following methods:
-
ProtectMessage(payload []byte, topic string)
takes a cleartext payload to protect and the associated topic, and returns a []byte
that is the payload encrypted and authenticated with the topic's key.
-
Unprotect(protected []byte, topic string)
takes a protected payload and attempts to decrypt and verify it. If topic
is the special topic reserved for control messages, then the control message is processed and the client's state updated accordingly.
Note that we talk of message protection instead of just encryption because the protection operation includes also authentication and replay defense. The unprotection operation thus involves decryption and additional checks, and includes the processing of control messages sent by the server.
E4's server (C2) is necessary to send control messages and manage a fleet of clients through GUIs, APIs, and automation components.
The server can for example deploy key rotation policies, grant and revoke rights, and enable forward secrecy.
Please contact us to request access to a private instance of the server, or test the limited public version.
Without the C2 server, the E4 client library can be used to protect messages using static keys, manually managed.
Using our client application
To try E4 without writing your own application, we created a simple interactive client application that you can use in combination with our public demo server interface. You can directly download the client's binary for your platform or build it yourself, and then follow the instructions in the client's README.
Creating a client
The following instructions assume that your program imports e4
as follows:
import e4 "github.com/teserakt-io/e4go"
The E4 protocol supports both symmetric key and public-key mode.
Depending on the mode, different functions should be used to instantiate a client:
Symmetric-key client
A symmetric-key client can be created from a 16-byte identifier (type []byte
), a 32-byte key (type []byte
), and an e4.ReadWriteSeeker
implementation, used to persist the client's state (see client storage section for details):
client, err := e4.NewClient(&e4.SymIDAndKey{ID: id, Key: key}, store)
A symmetric-key client can also be created from a name (string
of arbitrary length) and a password (string
of a least 16 characters), as follows:
client, err := e4.NewClient(&e4.SymNameAndPassword{Name: name, Password: password}, store)
The latter is a wrapper over NewSymKeyClient()
that creates the ID by hashing name
with SHA-3-256, and deriving a key using Argon2.
Public-key client
A public-key client can be created from a 16-byte identifier (type []byte
), an Ed25519 private key (type ed25519.PrivateKey
), an e4.ReadWriteSeeker
implementation, which will be used to store the client's state (see client storage section for details), and a Curve25519 public key (32-byte []byte
):
client, err := e4.NewClient(&e4.PubIDAndKey{ID:id, Key: key, C2PubKey: c2PubKey}, store)
Compared to the symmetric-key mode, and additional argument is c2PubKey
, the public key of the C2 server that sends control messages.
A public-key client can also be created from a name (string
of arbitrary length) and a password (string
of a least 16 characters), as follows:
client, err := e4.NewClient(&e4.PubNameAndPassword{Name:name, Password: password, C2PubKey: c2PubKey}, store)
The Ed25519 private key is then created from a seed that is derived from the password using Argon2.
The Ed25519 public key can also be retrieved:
config := &e4.PubNameAndPassword{Name:name, Password: password, C2PubKey: c2PubKey}
pubKey, err := config.PubKey()
From a saved state
A client instance can be recovered using the LoadClient()
helper given an e4.ReadWriteSeeker
implementation::
client, err := e4.LoadClient(store)
Note that a client's state is automatically saved to the provided store when the client is created, and every time its state changes, and therefore does not need be manually saved.
Client storage
E4 client offer a way to persist its internal state, allowing to shut it down and reload without having to retransmit all the keys, by providing an e4.ReadWriteSeeker
implementation to the client. This interface is compatible with any io.ReadWriteSeeker, such as the os.File
type, which should be the most common option. But it also allows custom implementations for platforms where filesystem isn't available, see the e4.NewInMemoryStore([]byte)
we provide as an example of custom storage implementation.
Integration instructions
To integrate E4 into your application, the protect/unprotect logic needs be added between the network layer and the application layer when transmitting/receiving a message.
This section provides further instructions related to error handling and to the special case of control messages received from the C2 server.
Note that E4 is essentially an application security layer, therefore it processes the payload of a message (such as an MQTT payload), excluding header fields.
References to "messages" below therefore refer to payload data (or application message),as opposed to the network-level message.
Receiving a message
Assume that you receive messages over MQTT or Kafka, and have topics and payload defined as
var topic string
var message []byte
Having instantiated a client, you can then unprotect the message as follows:
plaintext, err := client.Unprotect(message, topic)
if err != nil {
}
If you receive no error, plaintext
may still be nil
. This happens when E4
has processed a control message, that is, a message sent by the C2 server, for example to provision or delete a topic key.
In this case, you do not need to act on the message, since E4 has already processed it. If you want to detect this case you can test for
if len(plainText) == 0 { ... }
or alternatively
if client.IsReceivingTopic(topic)
which indicates a message on E4's control channel.
You should not have to parse E4's messages yourself.
Control messages are thus deliberately not returned to users.
If plaintext
is not nil
and err
is nil, your application can proceed with the unprotected, plaintext message.
Transmitting a message
To protect a message to be transmitted, suppose say that you have the topic and payload defined as:
var topic string
var message []byte
You can then use the Protect
method from the client instance as follows:
protected, err := client.Protect(message, topic)
if err != nil {
}
Handling errors
All errors should be reported, and the plaintext
and protected
values discarded upon an error, except potentially in one case:
if you receive an ErrTopicKeyNotFound
error from ProtectMessage()
or Unprotect()
, it is because the client does not have the key for this topic.
Therefore,
-
When transmitting a message, your application can either discard the message to be sent, or choose to transmit it in clear.
-
When receiving a message, your application can either discard the message (for example if all messages are assumed to be encrypted in your network), or forward the message to the application (if you call Unprotect()
for all messages yet tolerate the receiving of unencrypted messages over certain topics, which thus don't have a topic key).
In order to have the key associated to a certain topic, you must instruct the C2 to deliver said topic key to the client.
Key generation
To ease key creation, we provide a key generation application that you can use to generate symmetric, Ed25519 or Curve25519 keys needed for E4 operations.
You can download the binary for your platform or build it yourself, and then follow the instructions in the keygen README.
Our key generator relies on Go's crypto/rand
package, which guarantees cryptographically secure randomness across various platforms.
Bindings
Android
Latest bindings for Android can be downloaded from the release page.
On an environment having an Android SDK and NDK available, an Android AAR package can be generated invoking the following script:
./scripts/android_bindings.sh
This will generate:
dist/bindings/android/e4.aar
: the Android package, containing compiled Java class and native libraries for most common architecturesdist/bindings/android/e4-sources.jar
: the Java source files
After importing the AAR in your project, E4 client can be created and invoked
in a similar way than the Go version, for example using Kotlin:
import java.io.RandomAccessFile
import io.teserakt.e4.E4
import io.teserakt.e4.SymNameAndPassword
import io.teserakt.crypto.Crypto
val cfg = SymNameAndPassword()
cfg.name = "deviceXYZ"
cfg.password = "secretForDeviceXYZ"
val store = FileStore(filesDir.absolutePath + "/" + cfg.name + ".json")
val client = E4.newClient(cfg, store)
val topic = "/deviceXYZ/data";
val protectedMessage = client.protectMessage("Hello".toByteArray(Charsets.UTF_8), topic)
val unprotectedMessage = client.unprotect(protectedMessage, topic)
Here We are using a custom file storage implemented as such:
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.RandomAccessFile;
import io.teserakt.e4.Store;
public class FileStore implements Store {
private static final int SEEK_START = 0;
private static final int SEEK_CURRENT = 1;
private static final int SEEK_END = 2;
private RandomAccessFile file;
public FileStore(String filepath) throws FileNotFoundException {
this.file = new RandomAccessFile(filepath, "rw");
}
public long read(byte[] buf) throws IOException {
return this.file.read(buf);
}
public long write(byte[] buf) throws IOException {
this.file.write(buf);
return buf.length;
}
public long seek(long offset, long whence) throws Exception {
long abs;
switch((int)whence) {
case SEEK_START:
abs = offset;
break;
case SEEK_CURRENT:
abs = this.file.getChannel().position() + offset;
break;
case SEEK_END:
abs = this.file.length() + offset;
break;
default:
throw new Exception("invalid whence");
}
if (abs < 0) {
throw new Exception("negative position");
}
this.file.getChannel().position(abs);
return abs;
}
}
Contributing
Before contributing, please read our CONTRIBUTING guide.
Security
To report a security vulnerability (or potential vulnerability where private discussion is preferred) see SECURITY.
Support
To request support, please contact team@teserakt.io.
Intellectual property
e4go is copyright (c) Teserakt AG 2018-2020, and released under Apache 2.0 License (see LICENCE).