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 + } + } +}