diff --git a/.travis.yml b/.travis.yml
index 7707c9d8..1b0cddad 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,8 +14,10 @@ cache:
- $HOME/.gradle/wrapper/
install: true
script:
- - sudo apt-get install -y autoconf automake libtool make tar gcc-multilib libaio-dev
+ - sudo add-apt-repository -y ppa:elt/libsodium
+ - sudo apt-get update
+ - sudo apt-get install -y autoconf automake libtool make tar gcc-multilib libaio-dev libsodium-dev
- gradle build
- - gradle test
+ - gradle test --stacktrace --info
notifications:
email: false
diff --git a/README.md b/README.md
index 345e41e2..28b57860 100644
--- a/README.md
+++ b/README.md
@@ -15,6 +15,15 @@ gradle build
Perhaps you might like using the ssh protocol to clone... in which case do `git clone git@github.com:ConsenSys/athena.git`
+## libsodium
+
+In order to be compatible with the original Haskell Constellation, the lib sodium library has been used to provide the encryption primitives.
+
+In order to use this, you will first need to install lib sodium on your machine.
+
+mac:
+`brew install libsodium`
+
## Native transports
In order to make sure the http related communications are as optimised as possible, we use native transports with the
diff --git a/build.gradle b/build.gradle
index 750ac3b9..483b97be 100644
--- a/build.gradle
+++ b/build.gradle
@@ -18,7 +18,7 @@ dependencies {
compile 'com.moandjiezana.toml:toml4j:0.7.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.0'
implementation 'org.mapdb:mapdb:3.0.5'
-
+ implementation group: 'com.muquit.libsodiumjna', name: 'libsodium-jna', version: '1.0.4'
testImplementation 'junit:junit:4.12'
// logger
diff --git a/src/main/java/net/consensys/athena/api/config/Config.java b/src/main/java/net/consensys/athena/api/config/Config.java
index 440db40c..afcd8b7c 100644
--- a/src/main/java/net/consensys/athena/api/config/Config.java
+++ b/src/main/java/net/consensys/athena/api/config/Config.java
@@ -24,6 +24,17 @@ public interface Config {
*/
long port();
+ /**
+ * Path at which to locate the lib sodium shared library. Default:
+ *
+ *
+ * - Linux /usr/local/lib/libsodium.so
+ *
- Mac /usr/local/lib/libsodium.dylib
+ *
- Windows C:/libsodium/libsodium.dll
+ *
+ */
+ String libSodiumPath();
+
/**
* Directory to which paths to all other files referenced in the config are relative to.
*
diff --git a/src/main/java/net/consensys/athena/api/enclave/CombinedKey.java b/src/main/java/net/consensys/athena/api/enclave/CombinedKey.java
index 3d7d432d..4f3f1f9a 100644
--- a/src/main/java/net/consensys/athena/api/enclave/CombinedKey.java
+++ b/src/main/java/net/consensys/athena/api/enclave/CombinedKey.java
@@ -11,4 +11,6 @@
* @see java.security.Key
* @see java.security.PrivateKey
*/
-public interface CombinedKey {}
+public interface CombinedKey {
+ byte[] getEncoded();
+}
diff --git a/src/main/java/net/consensys/athena/impl/config/MemoryConfig.java b/src/main/java/net/consensys/athena/impl/config/MemoryConfig.java
index 670a03c6..bddc5cfb 100644
--- a/src/main/java/net/consensys/athena/impl/config/MemoryConfig.java
+++ b/src/main/java/net/consensys/athena/impl/config/MemoryConfig.java
@@ -33,6 +33,7 @@ public class MemoryConfig implements Config {
private Optional generateKeys = Optional.empty();
private Optional showVersion = Optional.empty();
private long verbosity;
+ private String libSodiumPath;
public void setUrl(URL url) {
this.url = url;
@@ -134,6 +135,10 @@ public void setVerbosity(long verbosity) {
this.verbosity = verbosity;
}
+ public void setLibSodiumPath(String libSodiumPath) {
+ this.libSodiumPath = libSodiumPath;
+ }
+
@Override
public URL url() {
return url;
@@ -258,4 +263,9 @@ public Optional showVersion() {
public long verbosity() {
return verbosity;
}
+
+ @Override
+ public String libSodiumPath() {
+ return libSodiumPath;
+ }
}
diff --git a/src/main/java/net/consensys/athena/impl/config/TomlConfigBuilder.java b/src/main/java/net/consensys/athena/impl/config/TomlConfigBuilder.java
index e9117382..5f096b30 100644
--- a/src/main/java/net/consensys/athena/impl/config/TomlConfigBuilder.java
+++ b/src/main/java/net/consensys/athena/impl/config/TomlConfigBuilder.java
@@ -2,6 +2,7 @@
import net.consensys.athena.api.config.Config;
import net.consensys.athena.api.config.ConfigException;
+import net.consensys.athena.impl.enclave.sodium.LibSodiumSettings;
import java.io.File;
import java.io.InputStream;
@@ -45,6 +46,9 @@ public Config build(InputStream config) throws ConfigException {
memoryConfig.setSocket(new File(toml.getString("socket")));
}
+ memoryConfig.setLibSodiumPath(
+ toml.getString("libsodiumpath", LibSodiumSettings.defaultLibSodiumPath()));
+
try {
memoryConfig.setOtherNodes(convertListToURLArray(toml.getList("othernodes")));
} catch (ConfigException e) {
diff --git a/src/main/java/net/consensys/athena/impl/enclave/sodium/LibSodiumEnclave.java b/src/main/java/net/consensys/athena/impl/enclave/sodium/LibSodiumEnclave.java
new file mode 100644
index 00000000..a40c9820
--- /dev/null
+++ b/src/main/java/net/consensys/athena/impl/enclave/sodium/LibSodiumEnclave.java
@@ -0,0 +1,111 @@
+package net.consensys.athena.impl.enclave.sodium;
+
+import net.consensys.athena.api.config.Config;
+import net.consensys.athena.api.enclave.CombinedKey;
+import net.consensys.athena.api.enclave.Enclave;
+import net.consensys.athena.api.enclave.EnclaveException;
+import net.consensys.athena.api.enclave.EncryptedPayload;
+import net.consensys.athena.api.enclave.HashAlgorithm;
+import net.consensys.athena.api.enclave.KeyStore;
+import net.consensys.athena.impl.enclave.SimpleEncryptedPayload;
+import net.consensys.athena.impl.enclave.bouncycastle.Hasher;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.security.Security;
+
+import com.muquit.libsodiumjna.SodiumLibrary;
+import com.muquit.libsodiumjna.exceptions.SodiumLibraryException;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.jetbrains.annotations.NotNull;
+
+public class LibSodiumEnclave implements Enclave {
+ private static final Logger log = LogManager.getLogger();
+
+ static {
+ Security.addProvider(new org.bouncycastle.jce.provider.BouncyCastleProvider());
+ }
+
+ private final Hasher hasher = new Hasher();
+ private KeyStore keyStore;
+
+ public LibSodiumEnclave(Config config, KeyStore keyStore) {
+ SodiumLibrary.setLibraryPath(config.libSodiumPath());
+
+ this.keyStore = keyStore;
+ }
+
+ @Override
+ public byte[] digest(HashAlgorithm algorithm, byte[] input) {
+ return hasher.digest(algorithm, input);
+ }
+
+ @Override
+ public EncryptedPayload encrypt(byte[] plaintext, PublicKey senderKey, PublicKey[] recipients) {
+ try {
+ PrivateKey senderPrivateKey = keyStore.getPrivateKey(senderKey);
+ if (senderPrivateKey == null) {
+ throw new EnclaveException("No PrivateKey found in keystore");
+ }
+ byte[] secretKey =
+ SodiumLibrary.randomBytes(SodiumLibrary.cryptoSecretBoxKeyBytes().intValue());
+ byte[] secretNonce = secretNonce();
+ byte[] cipherText = SodiumLibrary.cryptoSecretBoxEasy(plaintext, secretNonce, secretKey);
+
+ byte[] nonce = nonce();
+ CombinedKey[] combinedKeys = getCombinedKeys(recipients, senderPrivateKey, secretKey, nonce);
+ return new SimpleEncryptedPayload(senderKey, secretNonce, nonce, combinedKeys, cipherText);
+ } catch (SodiumLibraryException e) {
+ throw new EnclaveException(e);
+ }
+ }
+
+ @Override
+ public byte[] decrypt(EncryptedPayload ciphertextAndMetadata, PublicKey identity) {
+ try {
+ PrivateKey privateKey = keyStore.getPrivateKey(identity);
+ if (privateKey == null) {
+ throw new EnclaveException("No PrivateKey found in keystore");
+ }
+ CombinedKey key = ciphertextAndMetadata.getCombinedKeys()[0];
+ byte[] secretKey =
+ SodiumLibrary.cryptoBoxOpenEasy(
+ key.getEncoded(),
+ ciphertextAndMetadata.getCombinedKeyNonce(),
+ ciphertextAndMetadata.getSender().getEncoded(),
+ privateKey.getEncoded());
+ return SodiumLibrary.cryptoSecretBoxOpenEasy(
+ ciphertextAndMetadata.getCipherText(), ciphertextAndMetadata.getNonce(), secretKey);
+ } catch (SodiumLibraryException e) {
+ throw new EnclaveException(e);
+ }
+ }
+
+ private byte[] secretNonce() {
+ int secretNonceBytesLength = SodiumLibrary.cryptoSecretBoxNonceBytes().intValue();
+
+ return SodiumLibrary.randomBytes(secretNonceBytesLength);
+ }
+
+ private byte[] nonce() {
+ int nonceBytesLength = SodiumLibrary.cryptoBoxNonceBytes().intValue();
+ return SodiumLibrary.randomBytes(nonceBytesLength);
+ }
+
+ @NotNull
+ private CombinedKey[] getCombinedKeys(
+ PublicKey[] recipients, PrivateKey senderPrivateKey, byte[] secretKey, byte[] nonce)
+ throws SodiumLibraryException {
+ CombinedKey[] combinedKeys = new CombinedKey[recipients.length];
+ for (int i = 0; i < recipients.length; i++) {
+ PublicKey recipient = recipients[i];
+ byte[] encryptedKey =
+ SodiumLibrary.cryptoBoxEasy(
+ secretKey, nonce, recipient.getEncoded(), senderPrivateKey.getEncoded());
+ SodiumCombinedKey combinedKey = new SodiumCombinedKey(encryptedKey);
+ combinedKeys[i] = combinedKey;
+ }
+ return combinedKeys;
+ }
+}
diff --git a/src/main/java/net/consensys/athena/impl/enclave/sodium/LibSodiumSettings.java b/src/main/java/net/consensys/athena/impl/enclave/sodium/LibSodiumSettings.java
new file mode 100644
index 00000000..9ec4c6d9
--- /dev/null
+++ b/src/main/java/net/consensys/athena/impl/enclave/sodium/LibSodiumSettings.java
@@ -0,0 +1,21 @@
+package net.consensys.athena.impl.enclave.sodium;
+
+import java.io.File;
+
+import com.sun.jna.Platform;
+
+public class LibSodiumSettings {
+
+ public static String defaultLibSodiumPath() {
+ if (Platform.isMac()) {
+ return "/usr/local/lib/libsodium.dylib";
+ } else if (Platform.isWindows()) {
+ return "C:/libsodium/libsodium.dll";
+ } else if (new File("/usr/lib/x86_64-linux-gnu/libsodium.so").exists()) {
+ //Ubuntu trusty location.
+ return "/usr/lib/x86_64-linux-gnu/libsodium.so";
+ } else {
+ return "/usr/local/lib/libsodium.so";
+ }
+ }
+}
diff --git a/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumCombinedKey.java b/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumCombinedKey.java
new file mode 100644
index 00000000..3b00e5b7
--- /dev/null
+++ b/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumCombinedKey.java
@@ -0,0 +1,17 @@
+package net.consensys.athena.impl.enclave.sodium;
+
+import net.consensys.athena.api.enclave.CombinedKey;
+
+public class SodiumCombinedKey implements CombinedKey {
+
+ byte[] key;
+
+ public SodiumCombinedKey(byte[] key) {
+ this.key = key;
+ }
+
+ @Override
+ public byte[] getEncoded() {
+ return key;
+ }
+}
diff --git a/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumMemoryKeyStore.java b/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumMemoryKeyStore.java
new file mode 100644
index 00000000..171b590e
--- /dev/null
+++ b/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumMemoryKeyStore.java
@@ -0,0 +1,36 @@
+package net.consensys.athena.impl.enclave.sodium;
+
+import net.consensys.athena.api.enclave.EnclaveException;
+import net.consensys.athena.api.enclave.KeyStore;
+
+import java.security.PrivateKey;
+import java.security.PublicKey;
+import java.util.HashMap;
+import java.util.Map;
+
+import com.muquit.libsodiumjna.SodiumKeyPair;
+import com.muquit.libsodiumjna.SodiumLibrary;
+import com.muquit.libsodiumjna.exceptions.SodiumLibraryException;
+
+public class SodiumMemoryKeyStore implements KeyStore {
+
+ Map store = new HashMap<>();
+
+ @Override
+ public PrivateKey getPrivateKey(PublicKey publicKey) {
+ return store.get(publicKey);
+ }
+
+ @Override
+ public PublicKey generateKeyPair() {
+ try {
+ SodiumKeyPair keyPair = SodiumLibrary.cryptoBoxKeyPair();
+ SodiumPrivateKey privateKey = new SodiumPrivateKey(keyPair.getPrivateKey());
+ SodiumPublicKey publicKey = new SodiumPublicKey(keyPair.getPublicKey());
+ store.put(publicKey, privateKey);
+ return publicKey;
+ } catch (SodiumLibraryException e) {
+ throw new EnclaveException(e);
+ }
+ }
+}
diff --git a/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumPrivateKey.java b/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumPrivateKey.java
new file mode 100644
index 00000000..3a64ab82
--- /dev/null
+++ b/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumPrivateKey.java
@@ -0,0 +1,34 @@
+package net.consensys.athena.impl.enclave.sodium;
+
+import java.security.PrivateKey;
+import java.util.Arrays;
+
+public class SodiumPrivateKey implements PrivateKey {
+
+ private byte[] privateKey;
+
+ public SodiumPrivateKey(byte[] privateKey) {
+
+ this.privateKey = privateKey;
+ }
+
+ @Override
+ public String getAlgorithm() {
+ return "sodium";
+ }
+
+ @Override
+ public String getFormat() {
+ return "raw";
+ }
+
+ @Override
+ public byte[] getEncoded() {
+ return privateKey;
+ }
+
+ @Override
+ public String toString() {
+ return "SodiumPrivateKey{" + "privateKey=" + Arrays.toString(privateKey) + '}';
+ }
+}
diff --git a/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumPublicKey.java b/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumPublicKey.java
new file mode 100644
index 00000000..75c62fc8
--- /dev/null
+++ b/src/main/java/net/consensys/athena/impl/enclave/sodium/SodiumPublicKey.java
@@ -0,0 +1,27 @@
+package net.consensys.athena.impl.enclave.sodium;
+
+import java.security.PublicKey;
+
+public class SodiumPublicKey implements PublicKey {
+
+ private byte[] publicKey;
+
+ public SodiumPublicKey(byte[] publicKey) {
+ this.publicKey = publicKey;
+ }
+
+ @Override
+ public String getAlgorithm() {
+ return null;
+ }
+
+ @Override
+ public String getFormat() {
+ return null;
+ }
+
+ @Override
+ public byte[] getEncoded() {
+ return publicKey;
+ }
+}
diff --git a/src/test/java/net/consensys/athena/impl/config/TomlConfigBuilderTest.java b/src/test/java/net/consensys/athena/impl/config/TomlConfigBuilderTest.java
index 6d59b1da..08c5c88b 100644
--- a/src/test/java/net/consensys/athena/impl/config/TomlConfigBuilderTest.java
+++ b/src/test/java/net/consensys/athena/impl/config/TomlConfigBuilderTest.java
@@ -4,6 +4,7 @@
import net.consensys.athena.api.config.Config;
import net.consensys.athena.api.config.ConfigException;
+import net.consensys.athena.impl.enclave.sodium.LibSodiumSettings;
import java.io.File;
import java.io.InputStream;
@@ -93,6 +94,8 @@ public void testFullFileRead() throws Exception {
expectedFile = new File("known-servers");
assertEquals(expectedFile, testConf.tlsKnownServers());
+
+ assertEquals("/somepath", testConf.libSodiumPath());
}
@Test
@@ -143,6 +146,8 @@ public void testFullFileReadUsingDefaults() throws Exception {
expectedFile = new File("tls-known-servers");
assertEquals(expectedFile, testConf.tlsKnownServers());
+
+ assertEquals(LibSodiumSettings.defaultLibSodiumPath(), testConf.libSodiumPath());
}
@Test
@@ -154,6 +159,7 @@ public void testInvalidConfigsThrowException() throws Exception {
try {
Config testConf = configBuilder.build(configAsStream);
+ fail("Expected Config Exception to be thrown");
} catch (ConfigException e) {
String message =
"Invalid Configuration Options\n"
@@ -180,6 +186,7 @@ public void testMissingMandatoryConfigsThrowException() throws Exception {
try {
Config testConf = configBuilder.build(configAsStream);
+ fail("Expected Config Exception to be thrown");
} catch (ConfigException e) {
String message =
"Invalid Configuration Options\n"
@@ -200,6 +207,7 @@ public void testMissingStoragePathThrowException() throws Exception {
try {
Config testConf = configBuilder.build(configAsStream);
+ fail("Expected Config Exception to be thrown");
} catch (ConfigException e) {
String message =
"Invalid Configuration Options\n"
diff --git a/src/test/java/net/consensys/athena/impl/enclave/LibSodiumEnclaveTest.java b/src/test/java/net/consensys/athena/impl/enclave/LibSodiumEnclaveTest.java
new file mode 100644
index 00000000..b5df068c
--- /dev/null
+++ b/src/test/java/net/consensys/athena/impl/enclave/LibSodiumEnclaveTest.java
@@ -0,0 +1,106 @@
+package net.consensys.athena.impl.enclave;
+
+import static org.junit.Assert.*;
+
+import net.consensys.athena.api.enclave.CombinedKey;
+import net.consensys.athena.api.enclave.EnclaveException;
+import net.consensys.athena.api.enclave.EncryptedPayload;
+import net.consensys.athena.api.enclave.KeyStore;
+import net.consensys.athena.impl.config.MemoryConfig;
+import net.consensys.athena.impl.enclave.sodium.LibSodiumEnclave;
+import net.consensys.athena.impl.enclave.sodium.LibSodiumSettings;
+import net.consensys.athena.impl.enclave.sodium.SodiumMemoryKeyStore;
+import net.consensys.athena.impl.enclave.sodium.SodiumPublicKey;
+
+import java.security.PublicKey;
+
+import com.muquit.libsodiumjna.SodiumKeyPair;
+import com.muquit.libsodiumjna.SodiumLibrary;
+import com.muquit.libsodiumjna.exceptions.SodiumLibraryException;
+import org.junit.Before;
+import org.junit.Test;
+
+public class LibSodiumEnclaveTest {
+ KeyStore memoryKeyStore = new SodiumMemoryKeyStore();
+ MemoryConfig config = new MemoryConfig();
+ LibSodiumEnclave enclave;
+
+ @Before
+ public void setUp() throws Exception {
+ config.setLibSodiumPath(LibSodiumSettings.defaultLibSodiumPath());
+ enclave = new LibSodiumEnclave(config, memoryKeyStore);
+ }
+
+ @Test
+ public void testVersion() {
+ System.out.println(SodiumLibrary.libsodiumVersionString());
+ }
+
+ @Test
+ public void testSodium() throws SodiumLibraryException {
+ int nonceBytesLength = SodiumLibrary.cryptoBoxNonceBytes().intValue();
+ byte[] nonce = SodiumLibrary.randomBytes((int) nonceBytesLength);
+ SodiumKeyPair senderPair = SodiumLibrary.cryptoBoxKeyPair();
+ SodiumKeyPair recipientPair = SodiumLibrary.cryptoBoxKeyPair();
+
+ byte[] message = "hello".getBytes();
+ checkEncryptDecrypt(nonce, senderPair, recipientPair, message);
+
+ byte[] secretKey =
+ SodiumLibrary.randomBytes(SodiumLibrary.cryptoSecretBoxKeyBytes().intValue());
+ checkEncryptDecrypt(nonce, senderPair, recipientPair, secretKey);
+ }
+
+ private void checkEncryptDecrypt(
+ byte[] nonce, SodiumKeyPair senderPair, SodiumKeyPair recipientPair, byte[] message)
+ throws SodiumLibraryException {
+ byte[] ciphertext =
+ SodiumLibrary.cryptoBoxEasy(
+ message, nonce, recipientPair.getPublicKey(), senderPair.getPrivateKey());
+
+ byte[] decrypted =
+ SodiumLibrary.cryptoBoxOpenEasy(
+ ciphertext, nonce, senderPair.getPublicKey(), recipientPair.getPrivateKey());
+ assertArrayEquals(message, decrypted);
+ }
+
+ @Test
+ public void testEncryptDecrypt() throws SodiumLibraryException {
+ PublicKey senderKey = memoryKeyStore.generateKeyPair();
+ PublicKey recipientKey = memoryKeyStore.generateKeyPair();
+
+ String plaintext = "hello";
+ EncryptedPayload encryptedPayload =
+ enclave.encrypt(plaintext.getBytes(), senderKey, new PublicKey[] {recipientKey});
+ byte[] bytes = enclave.decrypt(encryptedPayload, recipientKey);
+ String decrypted = new String(bytes);
+ assertEquals(plaintext, decrypted);
+ }
+
+ @Test
+ public void testEncryptThrowsExceptionWhenMissingKey() throws Exception {
+ PublicKey fake = new SodiumPublicKey("fake".getBytes());
+ PublicKey recipientKey = memoryKeyStore.generateKeyPair();
+ try {
+ enclave.encrypt("plaintext".getBytes(), fake, new PublicKey[] {recipientKey});
+ fail("Should have thrown an Enclave Exception");
+ } catch (EnclaveException e) {
+ assertEquals("No PrivateKey found in keystore", e.getMessage());
+ }
+ }
+
+ @Test
+ public void testDecryptThrowsExceptionWhnMissingKey() throws Exception {
+ PublicKey fake = new SodiumPublicKey("fake".getBytes());
+ PublicKey sender = memoryKeyStore.generateKeyPair();
+ try {
+ EncryptedPayload payload =
+ new SimpleEncryptedPayload(
+ sender, new byte[] {}, new byte[] {}, new CombinedKey[] {}, new byte[] {});
+ enclave.decrypt(payload, fake);
+ fail("Should have thrown an Enclave Exception");
+ } catch (EnclaveException e) {
+ assertEquals("No PrivateKey found in keystore", e.getMessage());
+ }
+ }
+}
diff --git a/src/test/resources/fullConfigTest.toml b/src/test/resources/fullConfigTest.toml
index c170627c..aa64a910 100644
--- a/src/test/resources/fullConfigTest.toml
+++ b/src/test/resources/fullConfigTest.toml
@@ -21,4 +21,5 @@ tlsclientcert = "client-cert.pem"
tlsclientchain = []
tlsclientkey = "client-key.pem"
tlsclienttrust = "ca"
-tlsknownservers = "known-servers"
\ No newline at end of file
+tlsknownservers = "known-servers"
+libsodiumpath="/somepath"
\ No newline at end of file