diff --git a/build.sbt b/build.sbt
index 4b84522..19547c0 100644
--- a/build.sbt
+++ b/build.sbt
@@ -14,7 +14,7 @@ val scalaV = scalaVersion := "2.12.8"
val commons = Seq(
scalaV,
- version := "0.0.4",
+ version := "0.0.5",
fork in Test := true,
parallelExecution in Test := false,
organization := "one.fluence",
diff --git a/hashsign/js/src/test/scala/flyence/crypto/EcdsaSpec.scala b/hashsign/js/src/test/scala/fluence/crypto/EcdsaSpec.scala
similarity index 99%
rename from hashsign/js/src/test/scala/flyence/crypto/EcdsaSpec.scala
rename to hashsign/js/src/test/scala/fluence/crypto/EcdsaSpec.scala
index 8c6e4c2..ee61f60 100644
--- a/hashsign/js/src/test/scala/flyence/crypto/EcdsaSpec.scala
+++ b/hashsign/js/src/test/scala/fluence/crypto/EcdsaSpec.scala
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package flyence.crypto
+package fluence.crypto
import cats.data.EitherT
import cats.instances.try_._
diff --git a/hashsign/js/src/test/scala/flyence/crypto/JSHashSpec.scala b/hashsign/js/src/test/scala/fluence/crypto/JSHashSpec.scala
similarity index 98%
rename from hashsign/js/src/test/scala/flyence/crypto/JSHashSpec.scala
rename to hashsign/js/src/test/scala/fluence/crypto/JSHashSpec.scala
index a03ae61..98be79a 100644
--- a/hashsign/js/src/test/scala/flyence/crypto/JSHashSpec.scala
+++ b/hashsign/js/src/test/scala/fluence/crypto/JSHashSpec.scala
@@ -15,7 +15,7 @@
* along with this program. If not, see .
*/
-package flyence.crypto
+package fluence.crypto
import fluence.crypto.facade.ecdsa.{SHA1, SHA256}
import org.scalatest.{Matchers, WordSpec}
diff --git a/hashsign/jvm/src/main/scala/fluence/crypto/ecdsa/Ecdsa.scala b/hashsign/jvm/src/main/scala/fluence/crypto/ecdsa/Ecdsa.scala
index 91a436f..96b7e61 100644
--- a/hashsign/jvm/src/main/scala/fluence/crypto/ecdsa/Ecdsa.scala
+++ b/hashsign/jvm/src/main/scala/fluence/crypto/ecdsa/Ecdsa.scala
@@ -62,14 +62,20 @@ class Ecdsa(curveType: String, scheme: String, hasher: Option[Crypto.Hasher[Arra
_ ← nonFatalHandling {
g.initialize(ecSpec, input.map(new SecureRandom(_)).getOrElse(new SecureRandom()))
}(s"Could not initialize KeyPairGenerator")
- p ← EitherT.fromOption(Option(g.generateKeyPair()), CryptoError("Could not generate KeyPair. Unexpected."))
+ p ← EitherT.fromOption(Option(g.generateKeyPair()), CryptoError("Generated key pair is null"))
keyPair ← nonFatalHandling {
- //store S number for private key and compressed Q point on curve for public key
- val pk = ByteVector(p.getPublic.asInstanceOf[ECPublicKey].getQ.getEncoded(true))
- val bg = p.getPrivate.asInstanceOf[ECPrivateKey].getS
- val sk = ByteVector.fromValidHex(bg.toString(HEXradix))
+ val pk = p.getPublic match {
+ case pk: ECPublicKey => ByteVector(p.getPublic.asInstanceOf[ECPublicKey].getQ.getEncoded(true))
+ case p => throw new ClassCastException(s"Cannot cast public key (${p.getClass}) to Ed25519PublicKeyParameters")
+ }
+ val sk = p.getPrivate match {
+ case sk: ECPrivateKey =>
+ val bg = p.getPrivate.asInstanceOf[ECPrivateKey].getS
+ ByteVector.fromValidHex(bg.toString(HEXradix))
+ case s => throw new ClassCastException(s"Cannot cast private key (${p.getClass}) to Ed25519PrivateKeyParameters")
+ }
KeyPair.fromByteVectors(pk, sk)
- }("Could not generate KeyPair. Unexpected.")
+ }("Could not generate KeyPair")
} yield keyPair
}
diff --git a/hashsign/jvm/src/main/scala/fluence/crypto/ecdsa/Ed25519.scala b/hashsign/jvm/src/main/scala/fluence/crypto/ecdsa/Ed25519.scala
new file mode 100644
index 0000000..455f560
--- /dev/null
+++ b/hashsign/jvm/src/main/scala/fluence/crypto/ecdsa/Ed25519.scala
@@ -0,0 +1,170 @@
+/*
+ * Copyright (C) 2017 Fluence Labs Limited
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package fluence.crypto.ecdsa
+
+import java.security._
+
+import cats.Monad
+import cats.data.EitherT
+import fluence.crypto.KeyPair.Secret
+import fluence.crypto.signature.{SignAlgo, SignatureChecker, Signer}
+import fluence.crypto.{KeyPair, _}
+import org.bouncycastle.crypto.KeyGenerationParameters
+import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator
+import org.bouncycastle.crypto.params.{Ed25519PrivateKeyParameters, Ed25519PublicKeyParameters}
+import org.bouncycastle.crypto.signers.Ed25519Signer
+import scodec.bits.ByteVector
+
+import scala.language.higherKinds
+
+/**
+ * Edwards-curve Digital Signature Algorithm (EdDSA)
+ */
+class Ed25519(strength: Int) extends JavaAlgorithm {
+
+ import CryptoError.nonFatalHandling
+
+ val generateKeyPair: Crypto.KeyPairGenerator =
+ new Crypto.Func[Option[Array[Byte]], KeyPair] {
+ override def apply[F[_]](
+ input: Option[Array[Byte]]
+ )(implicit F: Monad[F]): EitherT[F, CryptoError, fluence.crypto.KeyPair] =
+ for {
+ g ← getKeyPairGenerator
+ random = input.map(new SecureRandom(_)).getOrElse(new SecureRandom())
+ keyParameters = new KeyGenerationParameters(random, strength)
+ _ = g.init(keyParameters)
+ p ← EitherT.fromOption(Option(g.generateKeyPair()), CryptoError("Generated key pair is null"))
+ keyPair ← nonFatalHandling {
+ val pk = p.getPublic match {
+ case pk: Ed25519PublicKeyParameters => pk.getEncoded
+ case p => throw new ClassCastException(s"Cannot cast public key (${p.getClass}) to Ed25519PublicKeyParameters")
+ }
+ val sk = p.getPrivate match {
+ case sk: Ed25519PrivateKeyParameters => sk.getEncoded
+ case s => throw new ClassCastException(s"Cannot cast private key (${p.getClass}) to Ed25519PrivateKeyParameters")
+ }
+ KeyPair.fromBytes(pk, sk)
+ }("Could not generate KeyPair")
+ } yield keyPair
+ }
+
+ /**
+ * Restores pair of keys from the known secret key.
+ * The public key will be the same each method call with the same secret key.
+ * @param sk secret key
+ * @return key pair
+ */
+ def restorePairFromSecret[F[_]: Monad](sk: Secret): EitherT[F, CryptoError, KeyPair] =
+ for {
+ keyPair ← nonFatalHandling {
+ val secret = new Ed25519PrivateKeyParameters(sk.bytes, 0)
+ KeyPair.fromBytes(secret.generatePublicKey().getEncoded, sk.bytes)
+ }("Could not generate KeyPair from private key")
+ } yield keyPair
+
+ def sign[F[_]: Monad](
+ keyPair: KeyPair,
+ message: ByteVector
+ ): EitherT[F, CryptoError, signature.Signature] =
+ signMessage(keyPair.secretKey.bytes, message.toArray)
+ .map(bb ⇒ fluence.crypto.signature.Signature(ByteVector(bb)))
+
+ def verify[F[_]: Monad](
+ publicKey: KeyPair.Public,
+ signature: fluence.crypto.signature.Signature,
+ message: ByteVector
+ ): EitherT[F, CryptoError, Unit] =
+ verifySign(publicKey.bytes, signature.bytes, message.toArray)
+
+ private def signMessage[F[_]: Monad](
+ privateKey: Array[Byte],
+ message: Array[Byte]
+ ): EitherT[F, CryptoError, Array[Byte]] =
+ for {
+ sign ← nonFatalHandling {
+ val privKey = new Ed25519PrivateKeyParameters(privateKey, 0)
+ val signer = new Ed25519Signer
+ signer.init(true, privKey)
+ signer.update(message, 0, message.length)
+ signer.generateSignature()
+ }("Cannot sign message")
+
+ } yield sign
+
+ private def verifySign[F[_]: Monad](
+ publicKey: Array[Byte],
+ signature: Array[Byte],
+ message: Array[Byte],
+ ): EitherT[F, CryptoError, Unit] =
+ for {
+ verify ← nonFatalHandling {
+ val pubKey = new Ed25519PublicKeyParameters(publicKey, 0)
+ val signer = new Ed25519Signer
+ signer.init(false, pubKey)
+ signer.update(message, 0, message.length)
+ signer.verifySignature(signature)
+ }("Cannot verify message")
+
+ _ ← EitherT.cond[F](verify, (), CryptoError("Signature is not verified"))
+ } yield ()
+
+ private def getKeyPairGenerator[F[_]: Monad] =
+ nonFatalHandling {
+ new Ed25519KeyPairGenerator()
+ }(
+ "Cannot get key pair generator"
+ )
+}
+
+object Ed25519 {
+
+ /**
+ * Keys in tendermint are generating with a random seed of 32 bytes
+ */
+ val tendermintEd25519 = new Ed25519(256)
+ val tendermintAlgo: SignAlgo = signAlgo(256)
+
+ def ed25519(strength: Int) = new Ed25519(strength)
+
+ def signAlgo(strength: Int): SignAlgo = {
+ val algo = ed25519(strength)
+ SignAlgo(
+ name = "ed25519",
+ generateKeyPair = algo.generateKeyPair,
+ signer = kp ⇒
+ Signer(
+ kp.publicKey,
+ new Crypto.Func[ByteVector, signature.Signature] {
+ override def apply[F[_]](
+ input: ByteVector
+ )(implicit F: Monad[F]): EitherT[F, CryptoError, signature.Signature] =
+ algo.sign(kp, input)
+ }
+ ),
+ checker = pk ⇒
+ new SignatureChecker {
+ override def check[F[_]: Monad](
+ signature: fluence.crypto.signature.Signature,
+ plain: ByteVector
+ ): EitherT[F, CryptoError, Unit] =
+ algo.verify(pk, signature, plain)
+ }
+ )
+ }
+}
diff --git a/hashsign/jvm/src/test/scala/fluence/crypto/SignatureSpec.scala b/hashsign/jvm/src/test/scala/fluence/crypto/EcdsaSpec.scala
similarity index 98%
rename from hashsign/jvm/src/test/scala/fluence/crypto/SignatureSpec.scala
rename to hashsign/jvm/src/test/scala/fluence/crypto/EcdsaSpec.scala
index f4bcbfd..e8cdd93 100644
--- a/hashsign/jvm/src/test/scala/fluence/crypto/SignatureSpec.scala
+++ b/hashsign/jvm/src/test/scala/fluence/crypto/EcdsaSpec.scala
@@ -18,7 +18,6 @@
package fluence.crypto
import java.io.File
-import java.math.BigInteger
import cats.data.EitherT
import cats.instances.try_._
@@ -30,7 +29,7 @@ import scodec.bits.ByteVector
import scala.util.{Random, Try}
-class SignatureSpec extends WordSpec with Matchers {
+class EcdsaSpec extends WordSpec with Matchers {
def rndBytes(size: Int): Array[Byte] = Random.nextString(10).getBytes
diff --git a/hashsign/jvm/src/test/scala/fluence/crypto/Ed25519Spec.scala b/hashsign/jvm/src/test/scala/fluence/crypto/Ed25519Spec.scala
new file mode 100644
index 0000000..0c80d42
--- /dev/null
+++ b/hashsign/jvm/src/test/scala/fluence/crypto/Ed25519Spec.scala
@@ -0,0 +1,174 @@
+/*
+ * Copyright (C) 2017 Fluence Labs Limited
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+package fluence.crypto
+
+import java.io.File
+
+import cats.data.EitherT
+import cats.instances.try_._
+import fluence.crypto.ecdsa.Ed25519
+import fluence.crypto.keystore.FileKeyStorage
+import fluence.crypto.signature.Signature
+import org.scalatest.{Matchers, WordSpec}
+import scodec.bits.ByteVector
+
+import scala.util.{Random, Try}
+
+class Ed25519Spec extends WordSpec with Matchers {
+
+ def rndBytes(size: Int): Array[Byte] = Random.nextString(10).getBytes
+
+ def rndByteVector(size: Int) = ByteVector(rndBytes(size))
+
+ private implicit class TryEitherTExtractor[A <: Throwable, B](et: EitherT[Try, A, B]) {
+
+ def extract: B =
+ et.value.map {
+ case Left(e) ⇒ fail(e) // for making test fail message more describable
+ case Right(v) ⇒ v
+ }.get
+
+ def isOk: Boolean = et.value.fold(_ ⇒ false, _.isRight)
+ }
+
+ "ed25519 algorithm" should {
+ "correct sign and verify data" in {
+ val algorithm = Ed25519.ed25519(32)
+
+ val keys = algorithm.generateKeyPair.unsafe(None)
+ val pubKey = keys.publicKey
+ val data = rndByteVector(10)
+ val sign = algorithm.sign[Try](keys, data).extract
+
+ algorithm.verify[Try](pubKey, sign, data).isOk shouldBe true
+
+ val randomData = rndByteVector(10)
+ val randomSign = algorithm.sign(keys, randomData).extract
+
+ algorithm.verify(pubKey, randomSign, data).isOk shouldBe false
+
+ algorithm.verify(pubKey, sign, randomData).isOk shouldBe false
+ }
+
+ "correctly work with signer and checker" in {
+ val algo = Ed25519.signAlgo(32)
+ val keys = algo.generateKeyPair.unsafe(None)
+ val signer = algo.signer(keys)
+ val checker = algo.checker(keys.publicKey)
+
+ val data = rndByteVector(10)
+ val sign = signer.sign(data).extract
+
+ checker.check(sign, data).isOk shouldBe true
+
+ val randomSign = signer.sign(rndByteVector(10)).extract
+ checker.check(randomSign, data).isOk shouldBe false
+ }
+
+ "throw an errors on invalid data" in {
+ val algo = Ed25519.signAlgo(32)
+ val keys = algo.generateKeyPair.unsafe(None)
+ val signer = algo.signer(keys)
+ val checker = algo.checker(keys.publicKey)
+ val data = rndByteVector(10)
+
+ val sign = signer.sign(data).extract
+
+ the[CryptoError] thrownBy {
+ checker.check(Signature(rndByteVector(10)), data).value.flatMap(_.toTry).get
+ }
+ val invalidChecker = algo.checker(KeyPair.fromByteVectors(rndByteVector(10), rndByteVector(10)).publicKey)
+ the[CryptoError] thrownBy {
+ invalidChecker
+ .check(sign, data)
+ .value
+ .flatMap(_.toTry)
+ .get
+ }
+ }
+
+ "store and read key from file" in {
+ val algo = Ed25519.signAlgo(32)
+ val keys = algo.generateKeyPair.unsafe(None)
+
+ val keyFile = File.createTempFile("test", "")
+ if (keyFile.exists()) keyFile.delete()
+ val storage = new FileKeyStorage(keyFile)
+
+ storage.storeKeyPair(keys).unsafeRunSync()
+
+ val keysReadE = storage.readKeyPair
+ val keysRead = keysReadE.unsafeRunSync()
+
+ val signer = algo.signer(keys)
+ val data = rndByteVector(10)
+ val sign = signer.sign(data).extract
+
+ algo.checker(keys.publicKey).check(sign, data).isOk shouldBe true
+ algo.checker(keysRead.publicKey).check(sign, data).isOk shouldBe true
+
+ //try to store key into previously created file
+ storage.storeKeyPair(keys).attempt.unsafeRunSync().isLeft shouldBe true
+ }
+
+ "restore key pair from secret key" in {
+ val algo = Ed25519.signAlgo(32)
+ val testKeys = algo.generateKeyPair.unsafe(None)
+
+ val ed25519 = Ed25519.ed25519(32)
+
+ val newKeys = ed25519.restorePairFromSecret(testKeys.secretKey).extract
+
+ testKeys shouldBe newKeys
+ }
+
+ "work with tendermint keys" in {
+ /*
+ {
+ "address": "C08269A8AACD53C3488F16F285821DAC77CF5DEF",
+ "pub_key": {
+ "type": "tendermint/PubKeyEd25519",
+ "value": "FWB5lXZ/TT2132+jXp/8aQzNwISwp9uuFz4z0TXDdxY="
+ },
+ "priv_key": {
+ "type": "tendermint/PrivKeyEd25519",
+ "value": "P6jw9q/Rytdxpv5Wxs1aYA8w82uS0x3CpmS9+GpaMGIVYHmVdn9NPbXfb6Nen/xpDM3AhLCn264XPjPRNcN3Fg=="
+ }
+ }
+ */
+
+ val privKeyBase64 = "P6jw9q/Rytdxpv5Wxs1aYA8w82uS0x3CpmS9+GpaMGIVYHmVdn9NPbXfb6Nen/xpDM3AhLCn264XPjPRNcN3Fg=="
+ val pubKeyBase64 = "FWB5lXZ/TT2132+jXp/8aQzNwISwp9uuFz4z0TXDdxY="
+
+ val privKey = ByteVector.fromBase64Descriptive(privKeyBase64).right.get
+ val pubKey = ByteVector.fromBase64Descriptive(pubKeyBase64).right.get
+
+ val restored = Ed25519.tendermintEd25519
+ .restorePairFromSecret[Try](KeyPair.Secret(privKey.dropRight(32)))
+ .value
+ .get
+ .right
+ .get
+ .publicKey
+ .bytes
+
+ restored shouldBe pubKey.toArray
+ restored shouldBe privKey.drop(32).toArray
+ }
+ }
+}