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: + * + * + */ + 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