Compare commits

...

10 Commits

Author SHA1 Message Date
Dmitry Kurinskiy
4951c4a2a8 Move from PureCodec to pure Kleisli (#14) 2019-09-13 19:30:09 +03:00
Dima
64f12eb609
Fix hash for ed25119 (#12)
* no hasher for default ed25519

* support interop buffers
2019-07-15 14:34:54 +03:00
Dima
1502772f8e no hasher for default ed25519 (#11) 2019-07-09 13:33:36 +03:00
Dima
d63509f077
add hash.js dependency (#10) 2019-07-03 16:19:20 +03:00
Dima
f7557a1d75 specs refactoring, move cross-platform specs to the shared subproject (#9) 2019-07-03 12:44:27 +03:00
Dima
16f5a93562
ed25519 for js (#8) 2019-07-02 12:49:00 +03:00
Dmitry Kurinskiy
38608df192
Slogging dependency is kept only in keystore (#7) 2019-06-13 15:28:05 +03:00
Dima
1a8313b321
ed25519 implementation (#6)
* ed25519 implementation
2019-06-06 13:47:24 +03:00
Dmitry Kurinskiy
e9b1e2d905
Updated dependencies (#5) 2019-04-18 18:39:48 +03:00
Dmitry Kurinskiy
fcf7e08813 Updated dependencies (#3) 2019-01-23 21:30:50 +03:00
41 changed files with 1164 additions and 1366 deletions

View File

@ -1,20 +1,11 @@
version = 2.0.1
docstrings = JavaDoc docstrings = JavaDoc
maxColumn = 120 maxColumn = 120
rewriteTokens { align = some
"=>": "⇒" align.tokens = [{code = "=>", owner = "Case"}, ":=", "%", "%%", "%%%"]
"<-": "←"
}
align = none
align {
openParenCallSite = false
openParenDefnSite = false
tokens = [
"%", "%%", "%%%", ":=", "~="
]
}
assumeStandardLibraryStripMargin = true assumeStandardLibraryStripMargin = true
includeCurlyBraceInSelectChains = false includeCurlyBraceInSelectChains = false
@ -62,3 +53,4 @@ rewrite {
SortImports SortImports
] ]
} }

View File

@ -2,9 +2,7 @@ sudo: required
language: scala language: scala
scala: scala:
- 2.12.5 - 2.12.9
jdk:
- oraclejdk8
# These directories are cached to S3 at the end of the build # These directories are cached to S3 at the end of the build
cache: cache:

View File

@ -1,5 +1,4 @@
[![Build Status](https://travis-ci.org/fluencelabs/crypto.svg?branch=master)](https://travis-ci.org/fluencelabs/crypto) [![Build Status](https://travis-ci.org/fluencelabs/crypto.svg?branch=master)](https://travis-ci.org/fluencelabs/crypto)
[![Gitter](https://badges.gitter.im/fluencelabs/crypto.svg)](https://gitter.im/fluencelabs/crypto?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge)
# Сrypto # Сrypto
@ -45,7 +44,7 @@ Simplified JWT implementation, meaning a JSON-serialized header and claim with s
// Bintray repo is used so far. Migration to Maven Central is planned // Bintray repo is used so far. Migration to Maven Central is planned
resolvers += Resolver.bintrayRepo("fluencelabs", "releases") resolvers += Resolver.bintrayRepo("fluencelabs", "releases")
val cryptoV = "0.0.1" val cryptoV = "0.0.3"
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"one.fluence" %%% "crypto-core" % cryptoV, // basic types and APIs "one.fluence" %%% "crypto-core" % cryptoV, // basic types and APIs

View File

@ -1,6 +1,6 @@
import de.heikoseeberger.sbtheader.License import de.heikoseeberger.sbtheader.License
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._ import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
import sbtcrossproject.crossProject import sbtcrossproject.CrossPlugin.autoImport.crossProject
name := "crypto" name := "crypto"
@ -10,11 +10,11 @@ javaOptions in Test ++= Seq("-ea")
skip in publish := true // Skip root project skip in publish := true // Skip root project
val scalaV = scalaVersion := "2.12.5" val scalaV = scalaVersion := "2.12.9"
val commons = Seq( val commons = Seq(
scalaV, scalaV,
version := "0.0.2", version := "0.1.0",
fork in Test := true, fork in Test := true,
parallelExecution in Test := false, parallelExecution in Test := false,
organization := "one.fluence", organization := "one.fluence",
@ -25,21 +25,21 @@ val commons = Seq(
headerLicense := Some(License.AGPLv3("2017", organizationName.value)), headerLicense := Some(License.AGPLv3("2017", organizationName.value)),
bintrayOrganization := Some("fluencelabs"), bintrayOrganization := Some("fluencelabs"),
publishMavenStyle := true, publishMavenStyle := true,
scalafmtOnCompile := true,
bintrayRepository := "releases", bintrayRepository := "releases",
resolvers += Resolver.bintrayRepo("fluencelabs", "releases") resolvers ++= Seq(Resolver.bintrayRepo("fluencelabs", "releases"), Resolver.sonatypeRepo("releases"))
) )
commons commons
val CodecV = "0.0.3" val CatsV = "2.0.0"
val CirceV = "0.12.1"
val CatsEffectV = "1.0.0-RC3"
val SloggingV = "0.6.1" val SloggingV = "0.6.1"
val ScalatestV = "3.0.+" val ScalatestV = "3.0.+"
val bouncyCastle = "org.bouncycastle" % "bcprov-jdk15on" % "1.59" val bouncyCastle = "org.bouncycastle" % "bcprov-jdk15on" % "1.61"
enablePlugins(AutomateHeaderPlugin) enablePlugins(AutomateHeaderPlugin)
@ -50,7 +50,8 @@ lazy val `crypto-core` = crossProject(JVMPlatform, JSPlatform)
.settings( .settings(
commons, commons,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"one.fluence" %%% "codec-bits" % CodecV, "org.scodec" %%% "scodec-core" % "1.11.3",
"org.typelevel" %%% "cats-core" % CatsV,
"org.scalatest" %%% "scalatest" % ScalatestV % Test "org.scalatest" %%% "scalatest" % ScalatestV % Test
) )
) )
@ -62,30 +63,6 @@ lazy val `crypto-core` = crossProject(JVMPlatform, JSPlatform)
lazy val `crypto-core-js` = `crypto-core`.js lazy val `crypto-core-js` = `crypto-core`.js
lazy val `crypto-core-jvm` = `crypto-core`.jvm lazy val `crypto-core-jvm` = `crypto-core`.jvm
lazy val `crypto-keystore` = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(FluenceCrossType)
.in(file("keystore"))
.settings(
commons,
libraryDependencies ++= Seq(
"one.fluence" %%% "codec-circe" % CodecV,
"biz.enef" %%% "slogging" % SloggingV,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
)
)
.jsSettings(
fork in Test := false
)
.jvmSettings(
libraryDependencies += "org.typelevel" %% "cats-effect" % CatsEffectV,
)
.enablePlugins(AutomateHeaderPlugin)
.dependsOn(`crypto-core`)
lazy val `crypto-keystore-js` = `crypto-keystore`.js
lazy val `crypto-keystore-jvm` = `crypto-keystore`.jvm
lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform) lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform) .withoutSuffixFor(JVMPlatform)
.crossType(FluenceCrossType) .crossType(FluenceCrossType)
@ -93,7 +70,6 @@ lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
.settings( .settings(
commons, commons,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"biz.enef" %%% "slogging" % SloggingV,
"org.scalatest" %%% "scalatest" % ScalatestV % Test "org.scalatest" %%% "scalatest" % ScalatestV % Test
) )
) )
@ -104,8 +80,11 @@ lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
) )
) )
.jsSettings( .jsSettings(
libraryDependencies += "io.scalajs" %%% "nodejs" % "0.4.2",
npmDependencies in Compile ++= Seq( npmDependencies in Compile ++= Seq(
"elliptic" -> "6.4.0" "elliptic" -> "6.4.1",
"supercop.js" -> "2.0.1",
"hash.js" -> "1.1.7"
), ),
scalaJSModuleKind in Test := ModuleKind.CommonJSModule, scalaJSModuleKind in Test := ModuleKind.CommonJSModule,
//all JavaScript dependencies will be concatenated to a single file *-jsdeps.js //all JavaScript dependencies will be concatenated to a single file *-jsdeps.js
@ -113,7 +92,7 @@ lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
fork in Test := false fork in Test := false
) )
.enablePlugins(AutomateHeaderPlugin) .enablePlugins(AutomateHeaderPlugin)
.dependsOn(`crypto-core`, `crypto-keystore` % Test) .dependsOn(`crypto-core`)
lazy val `crypto-hashsign-js` = `crypto-hashsign`.js lazy val `crypto-hashsign-js` = `crypto-hashsign`.js
.enablePlugins(ScalaJSBundlerPlugin) .enablePlugins(ScalaJSBundlerPlugin)
@ -126,6 +105,7 @@ lazy val `crypto-cipher` = crossProject(JVMPlatform, JSPlatform)
.settings( .settings(
commons, commons,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"biz.enef" %%% "slogging" % SloggingV % Test,
"org.scalatest" %%% "scalatest" % ScalatestV % Test "org.scalatest" %%% "scalatest" % ScalatestV % Test
) )
) )
@ -150,24 +130,3 @@ lazy val `crypto-cipher` = crossProject(JVMPlatform, JSPlatform)
lazy val `crypto-cipher-js` = `crypto-cipher`.js lazy val `crypto-cipher-js` = `crypto-cipher`.js
.enablePlugins(ScalaJSBundlerPlugin) .enablePlugins(ScalaJSBundlerPlugin)
lazy val `crypto-cipher-jvm` = `crypto-cipher`.jvm lazy val `crypto-cipher-jvm` = `crypto-cipher`.jvm
lazy val `crypto-jwt` = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(FluenceCrossType)
.in(file("jwt"))
.settings(
commons,
libraryDependencies ++= Seq(
"one.fluence" %%% "codec-circe" % CodecV,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
)
)
.jsSettings(
fork in Test := false,
scalaJSModuleKind := ModuleKind.CommonJSModule
)
.enablePlugins(AutomateHeaderPlugin)
.dependsOn(`crypto-core`)
lazy val `crypto-jwt-js` = `crypto-jwt`.js
lazy val `crypto-jwt-jvm` = `crypto-jwt`.jvm

View File

@ -17,11 +17,6 @@
package fluence.crypto.aes package fluence.crypto.aes
import cats.Monad
import cats.data.EitherT
import cats.syntax.compose._
import fluence.codec.PureCodec
import fluence.crypto.CryptoError.nonFatalHandling
import fluence.crypto.facade.cryptojs.{CryptOptions, CryptoJS, Key, KeyOptions} import fluence.crypto.facade.cryptojs.{CryptOptions, CryptoJS, Key, KeyOptions}
import fluence.crypto.{Crypto, CryptoError} import fluence.crypto.{Crypto, CryptoError}
import scodec.bits.ByteVector import scodec.bits.ByteVector
@ -48,62 +43,41 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) {
private val mode = CryptoJS.mode.CBC private val mode = CryptoJS.mode.CBC
private val aes = CryptoJS.AES private val aes = CryptoJS.AES
val encrypt: Crypto.Func[Array[Byte], Array[Byte]] =
new Crypto.Func[Array[Byte], Array[Byte]] {
override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
for {
key initSecretKey()
encrypted encryptData(input, key)
} yield encrypted
}
val decrypt: Crypto.Func[Array[Byte], Array[Byte]] =
new Crypto.Func[Array[Byte], Array[Byte]] {
override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
for {
detachedData detachData(input)
(iv, base64) = detachedData
key initSecretKey()
decData decryptData(key, base64, iv)
_ EitherT.cond(decData.nonEmpty, decData, CryptoError("Cannot decrypt message with this password."))
} yield decData.toArray
}
/** /**
* Encrypt data. * Encrypt data.
* @param data Data to encrypt * data Data to encrypt
* @param key Salted and hashed password * key Salted and hashed password
* @return Encrypted data with IV * @return Encrypted data with IV
*/ */
private def encryptData[F[_]: Monad](data: Array[Byte], key: Key): EitherT[F, CryptoError, Array[Byte]] = { private val encryptData =
nonFatalHandling { Crypto.tryFn[(Array[Byte], Key), Array[Byte]] {
//transform data to JS type case (data: Array[Byte], key: Key)
val wordArray = CryptoJS.lib.WordArray.create(new Int8Array(data.toJSArray)) //transform data to JS type
val iv = if (withIV) Some(generateIV) else None val wordArray = CryptoJS.lib.WordArray.create(new Int8Array(data.toJSArray))
val cryptOptions = CryptOptions(iv = iv, padding = pad, mode = mode) val iv = if (withIV) Some(generateIV) else None
//encryption return base64 string, transform it to byte array val cryptOptions = CryptOptions(iv = iv, padding = pad, mode = mode)
val crypted = ByteVector.fromValidBase64(aes.encrypt(wordArray, key, cryptOptions).toString) //encryption return base64 string, transform it to byte array
//IV also needs to be transformed in byte array val crypted = ByteVector.fromValidBase64(aes.encrypt(wordArray, key, cryptOptions).toString)
val byteIv = iv.map(i ByteVector.fromValidHex(i.toString)) //IV also needs to be transformed in byte array
byteIv.map(_.toArray ++ crypted.toArray).getOrElse(crypted.toArray) val byteIv = iv.map(i ByteVector.fromValidHex(i.toString))
}("Cannot encrypt data.") byteIv.map(_.toArray ++ crypted.toArray).getOrElse(crypted.toArray)
} }("Cannot encrypt data")
private def decryptData[F[_]: Monad](key: Key, base64Data: String, iv: Option[String]) = { private val decryptData: Crypto.Func[(Key, String, Option[String]), ByteVector] =
nonFatalHandling { Crypto.tryFn[(Key, String, Option[String]), ByteVector] {
//parse IV to WordArray JS format case (key: Key, base64Data: String, iv: Option[String])
val cryptOptions = CryptOptions(iv = iv.map(i CryptoJS.enc.Hex.parse(i)), padding = pad, mode = mode) //parse IV to WordArray JS format
val dec = aes.decrypt(base64Data, key, cryptOptions) val cryptOptions = CryptOptions(iv = iv.map(i CryptoJS.enc.Hex.parse(i)), padding = pad, mode = mode)
ByteVector.fromValidHex(dec.toString) val dec = aes.decrypt(base64Data, key, cryptOptions)
}("Cannot decrypt data.") ByteVector.fromValidHex(dec.toString)
} }("Cannot decrypt data")
/** /**
* @param cipherText Encrypted data with IV * cipherText Encrypted data with IV
* @return IV in hex and data in base64 * @return IV in hex and data in base64
*/ */
private def detachData[F[_]: Monad](cipherText: Array[Byte]): EitherT[F, CryptoError, (Option[String], String)] = { private val detachData: Crypto.Func[Array[Byte], (Option[String], String)] =
nonFatalHandling { Crypto.tryFn { cipherText: Array[Byte]
val dataWithParams = if (withIV) { val dataWithParams = if (withIV) {
val ivDec = ByteVector(cipherText.slice(0, IV_SIZE)).toHex val ivDec = ByteVector(cipherText.slice(0, IV_SIZE)).toHex
val encMessage = cipherText.slice(IV_SIZE, cipherText.length) val encMessage = cipherText.slice(IV_SIZE, cipherText.length)
@ -112,36 +86,45 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) {
val (ivOp, data) = dataWithParams val (ivOp, data) = dataWithParams
val base64 = ByteVector(data).toBase64 val base64 = ByteVector(data).toBase64
(ivOp, base64) (ivOp, base64)
}("Cannot detach data and IV.") }("Cannot detach data and IV")
}
/** /**
* Hash password with salt `iterationCount` times * Hash password with salt `iterationCount` times
*/ */
private def initSecretKey[F[_]: Monad](): EitherT[F, CryptoError, Key] = { private val initSecretKey: Crypto.Func[Unit, Key] =
nonFatalHandling { Crypto.tryFn { _: Unit
// get raw key from password and salt // get raw key from password and salt
val keyOption = KeyOptions(BITS, iterations = iterationCount, hasher = CryptoJS.algo.SHA256) val keyOption = KeyOptions(BITS, iterations = iterationCount, hasher = CryptoJS.algo.SHA256)
CryptoJS.PBKDF2(new String(password), salt, keyOption) CryptoJS.PBKDF2(new String(password), salt, keyOption)
}("Cannot init secret key.") }("Cannot init secret key")
}
val decrypt: Crypto.Func[Array[Byte], Array[Byte]] =
Crypto { input
for {
detachedData detachData(input)
(iv, base64) = detachedData
key initSecretKey(())
decData decryptData((key, base64, iv))
_ Crypto[Boolean, Unit](Either.cond(_, (), CryptoError("Cannot decrypt message with this password.")))(
decData.nonEmpty
)
} yield decData.toArray
}
val encrypt: Crypto.Func[Array[Byte], Array[Byte]] =
Crypto[Array[Byte], Array[Byte]] { input
for {
key initSecretKey(())
encrypted encryptData(input -> key)
} yield encrypted
}
} }
object AesCrypt extends slogging.LazyLogging { object AesCrypt {
def build(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[Array[Byte]] = { def build(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[Array[Byte]] = {
val aes = new AesCrypt(password.toHex.toCharArray, withIV, config) val aes = new AesCrypt(password.toHex.toCharArray, withIV, config)
Crypto.Bijection(aes.encrypt, aes.decrypt) Crypto.Cipher(aes.encrypt, aes.decrypt)
} }
def forString(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[String] = {
implicit val codec: PureCodec[String, Array[Byte]] =
PureCodec.build(_.getBytes, bytes new String(bytes))
apply[String](password, withIV, config)
}
def apply[T](password: ByteVector, withIV: Boolean, config: AesConfig)(
implicit codec: PureCodec[T, Array[Byte]]
): Crypto.Cipher[T] =
Crypto.codec[T, Array[Byte]] andThen build(password, withIV, config)
} }

View File

@ -17,7 +17,6 @@
package fluence.crypto package fluence.crypto
import cats.instances.try_._
import fluence.crypto.aes.{AesConfig, AesCrypt} import fluence.crypto.aes.{AesConfig, AesCrypt}
import org.scalactic.source.Position import org.scalactic.source.Position
import org.scalatest.{Assertion, Matchers, WordSpec} import org.scalatest.{Assertion, Matchers, WordSpec}
@ -35,51 +34,52 @@ class AesJSSpec extends WordSpec with Matchers with slogging.LazyLogging {
"aes crypto" should { "aes crypto" should {
"work with IV" in { "work with IV" in {
val pass = ByteVector("pass".getBytes()) val pass = ByteVector("pass".getBytes())
val crypt = AesCrypt.forString(pass, withIV = true, config = conf) val crypt = AesCrypt.build(pass, withIV = true, config = conf)
val str = rndString(200) val str = rndString(200).getBytes()
val crypted = crypt.direct.unsafe(str) val crypted = crypt.encrypt(str).right.get
crypt.inverse.unsafe(crypted) shouldBe str crypt.decrypt(crypted).right.get shouldBe str
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = true, config = conf) val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = true, config = conf)
checkCryptoError(fakeAes.inverse.runF[Try](crypted), str) checkCryptoError(fakeAes.decrypt(crypted), str)
//we cannot check if first bytes is iv or already data, but encryption goes wrong //we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithoutIV = AesCrypt.forString(pass, withIV = false, config = conf) val aesWithoutIV = AesCrypt.build(pass, withIV = false, config = conf)
aesWithoutIV.inverse.unsafe(crypted) shouldNot be(str) aesWithoutIV.decrypt(crypted).right.get shouldNot be(str)
val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10))) val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted), str) checkCryptoError(aesWrongSalt.decrypt(crypted), str)
} }
"work without IV" in { "work without IV" in {
val pass = ByteVector("pass".getBytes()) val pass = ByteVector("pass".getBytes())
val crypt = AesCrypt.forString(pass, withIV = false, config = conf) val crypt = AesCrypt.build(pass, withIV = false, config = conf)
val str = rndString(200) val str = rndString(200).getBytes()
val crypted = crypt.direct.unsafe(str) val crypted = crypt.encrypt(str).right.get
crypt.inverse.unsafe(crypted) shouldBe str crypt.decrypt(crypted).right.get shouldBe str
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = false, config = conf) val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = false, config = conf)
checkCryptoError(fakeAes.inverse.runF[Try](crypted), str) checkCryptoError(fakeAes.decrypt(crypted), str)
//we cannot check if first bytes is iv or already data, but encryption goes wrong //we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithIV = AesCrypt.forString(pass, withIV = true, config = conf) val aesWithIV = AesCrypt.build(pass, withIV = true, config = conf)
aesWithIV.inverse.unsafe(crypted) shouldNot be(str) aesWithIV.decrypt(crypted).right.get shouldNot be(str)
val aesWrongSalt = AesCrypt.forString(pass, withIV = false, config = conf.copy(salt = rndString(10))) val aesWrongSalt = AesCrypt.build(pass, withIV = false, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted), str) checkCryptoError(aesWrongSalt.decrypt(crypted), str)
} }
def checkCryptoError(tr: Try[String], msg: String)(implicit pos: Position): Assertion = { /**
* Checks if there is a crypto error or result is not equal with source result.
*/
def checkCryptoError(tr: Crypto.Result[Array[Byte]], msg: Array[Byte])(implicit pos: Position): Assertion = {
tr.map { r tr.map { r
r != msg !(r sameElements msg)
}.recover { }.fold(
case e: CryptoError true _ true,
case e res => res
logger.error("Unexpected error", e) ) shouldBe true
false
}.get shouldBe true
} }
} }
} }

View File

@ -17,11 +17,8 @@
package fluence.crypto.aes package fluence.crypto.aes
import cats.Monad import cats.instances.either._
import cats.data.EitherT import fluence.crypto.{Crypto, JavaAlgorithm}
import cats.syntax.compose._
import fluence.codec.PureCodec
import fluence.crypto.{Crypto, CryptoError, JavaAlgorithm}
import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.engines.AESEngine import org.bouncycastle.crypto.engines.AESEngine
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator
@ -50,7 +47,6 @@ case class DataWithParams(data: Array[Byte], params: CipherParameters)
* message * message
*/ */
class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extends JavaAlgorithm { class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extends JavaAlgorithm {
import CryptoError.nonFatalHandling
private val rnd = Random private val rnd = Random
private val salt = config.salt.getBytes() private val salt = config.salt.getBytes()
@ -66,175 +62,150 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extend
iv iv
} }
val encrypt: Crypto.Func[Array[Byte], Array[Byte]] =
new Crypto.Func[Array[Byte], Array[Byte]] {
override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
for {
key initSecretKey(password, salt)
extDataWithParams extDataWithParams(key)
encData processData(DataWithParams(input, extDataWithParams._2), extDataWithParams._1, encrypt = true)
} yield encData
}
val decrypt: Crypto.Func[Array[Byte], Array[Byte]] =
new Crypto.Func[Array[Byte], Array[Byte]] {
override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
for {
dataWithParams detachDataAndGetParams(input, password, salt, withIV)
decData processData(dataWithParams, None, encrypt = false)
} yield decData
}
/**
* Generate key parameters with IV if it is necessary
* @param key Password
* @return Optional IV and cipher parameters
*/
def extDataWithParams[F[_]: Monad](
key: Array[Byte]
): EitherT[F, CryptoError, (Option[Array[Byte]], CipherParameters)] = {
if (withIV) {
val ivData = generateIV
// setup cipher parameters with key and IV
paramsWithIV(key, ivData).map(k (Some(ivData), k))
} else {
params(key).map(k (None, k))
}
}
/** /**
* Key spec initialization * Key spec initialization
*/ */
private def initSecretKey[F[_]: Monad]( private val initSecretKey: Crypto.Func[( /*password*/ Array[Char], /*salt*/ Array[Byte]), Array[Byte]] =
password: Array[Char], Crypto.tryFn[(Array[Char], Array[Byte]), Array[Byte]] {
salt: Array[Byte] case (password, salt)
): EitherT[F, CryptoError, Array[Byte]] = PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password)
nonFatalHandling { }("Cannot init secret key")
PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password)
}("Cannot init secret key.")
/** /**
* Setup AES CBC cipher * Setup AES CBC cipher
* @param encrypt True for encryption and false for decryption * encrypt: True for encryption and false for decryption
*
* @return cipher * @return cipher
*/ */
private def setupAesCipher[F[_]: Monad]( private val setupAesCipher: Crypto.Func[(CipherParameters, Boolean), PaddedBufferedBlockCipher] =
params: CipherParameters, Crypto.tryFn[(CipherParameters, Boolean), PaddedBufferedBlockCipher] {
encrypt: Boolean case (params, encrypt)
): EitherT[F, CryptoError, PaddedBufferedBlockCipher] = { // setup AES cipher in CBC mode with PKCS7 padding
nonFatalHandling { val padding = new PKCS7Padding
// setup AES cipher in CBC mode with PKCS7 padding val cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine), padding)
val padding = new PKCS7Padding cipher.reset()
val cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine), padding) cipher.init(encrypt, params)
cipher.reset()
cipher.init(encrypt, params)
cipher cipher
}("Cannot setup aes cipher.") }("Cannot setup aes cipher")
}
private def cipherBytes[F[_]: Monad]( private val cipherBytes: Crypto.Func[(Array[Byte], PaddedBufferedBlockCipher), Array[Byte]] =
data: Array[Byte], Crypto.tryFn[(Array[Byte], PaddedBufferedBlockCipher), Array[Byte]] {
cipher: PaddedBufferedBlockCipher case (data, cipher)
): EitherT[F, CryptoError, Array[Byte]] = { // create a temporary buffer to decode into (it'll include padding)
nonFatalHandling { val buf = new Array[Byte](cipher.getOutputSize(data.length))
// create a temporary buffer to decode into (it'll include padding) val outputLength = cipher.processBytes(data, 0, data.length, buf, 0)
val buf = new Array[Byte](cipher.getOutputSize(data.length)) val lastBlockLength = cipher.doFinal(buf, outputLength)
val outputLength = cipher.processBytes(data, 0, data.length, buf, 0) //remove padding
val lastBlockLength = cipher.doFinal(buf, outputLength) buf.slice(0, outputLength + lastBlockLength)
//remove padding }("Error in cipher processing")
buf.slice(0, outputLength + lastBlockLength)
}("Error in cipher processing.")
}
/** /**
* *
* @param dataWithParams Cata with cipher parameters * dataWithParams Cata with cipher parameters
* @param addData Additional data (nonce) * addData Additional data (nonce)
* @param encrypt True for encryption and false for decryption * encrypt True for encryption and false for decryption
* @return Crypted bytes * @return Crypted bytes
*/ */
private def processData[F[_]: Monad]( private val processData: Crypto.Func[(DataWithParams, Option[Array[Byte]], Boolean), Array[Byte]] =
dataWithParams: DataWithParams, Crypto {
addData: Option[Array[Byte]], case (dataWithParams, addData, encrypt)
encrypt: Boolean for {
): EitherT[F, CryptoError, Array[Byte]] = { cipher setupAesCipher(dataWithParams.params -> encrypt)
for { buf cipherBytes(dataWithParams.data, cipher)
cipher setupAesCipher(dataWithParams.params, encrypt = encrypt) } yield addData.map(_ ++ buf).getOrElse(buf)
buf cipherBytes(dataWithParams.data, cipher) }
encryptedData = addData.map(_ ++ buf).getOrElse(buf)
} yield encryptedData
}
/** /**
* encrypted data = initialization vector + data * encrypted data = initialization vector + data
*/ */
private def detachIV[F[_]: Monad](data: Array[Byte], ivSize: Int): EitherT[F, CryptoError, DetachedData] = { private val detachIV: Crypto.Func[(Array[Byte], Int), DetachedData] =
nonFatalHandling { Crypto.tryFn[(Array[Byte], Int), DetachedData] {
val ivData = data.slice(0, ivSize) case (data, ivSize)
val encData = data.slice(ivSize, data.length) val ivData = data.slice(0, ivSize)
DetachedData(ivData, encData) val encData = data.slice(ivSize, data.length)
}("Cannot detach data and IV.") DetachedData(ivData, encData)
} }("Cannot detach data and IV")
private def paramsWithIV[F[_]: Monad]( private val params: Crypto.Func[Array[Byte], KeyParameter] =
key: Array[Byte], Crypto.tryFn { key: Array[Byte]
iv: Array[Byte]
): EitherT[F, CryptoError, ParametersWithIV] = {
params(key).flatMap { keyParam
nonFatalHandling(new ParametersWithIV(keyParam, iv))("Cannot generate key parameters with IV")
}
}
private def params[F[_]: Monad](key: Array[Byte]): EitherT[F, CryptoError, KeyParameter] = {
nonFatalHandling {
val pGen = new PKCS5S2ParametersGenerator(new SHA256Digest) val pGen = new PKCS5S2ParametersGenerator(new SHA256Digest)
pGen.init(key, salt, iterationCount) pGen.init(key, salt, iterationCount)
pGen.generateDerivedParameters(BITS).asInstanceOf[KeyParameter] pGen.generateDerivedParameters(BITS).asInstanceOf[KeyParameter]
}("Cannot generate key parameters") }("Cannot generate key parameters")
}
private def detachDataAndGetParams[F[_]: Monad]( private val paramsWithIV: Crypto.Func[(Array[Byte], Array[Byte]), ParametersWithIV] =
data: Array[Byte], Crypto {
password: Array[Char], case (key: Array[Byte], iv: Array[Byte])
salt: Array[Byte], params
withIV: Boolean .andThen(
): EitherT[F, CryptoError, DataWithParams] = { Crypto.tryFn((kp: KeyParameter) new ParametersWithIV(kp, iv))("Cannot generate key parameters with IV")
if (withIV) { )
for { .run(key)
ivDataWithEncData detachIV(data, IV_SIZE) }
key initSecretKey(password, salt)
// setup cipher parameters with key and IV /**
paramsWithIV paramsWithIV(key, ivDataWithEncData.ivData) * Generate key parameters with IV if it is necessary
} yield DataWithParams(ivDataWithEncData.encData, paramsWithIV) * key Password
} else { * @return Optional IV and cipher parameters
for { */
key initSecretKey(password, salt) val extDataWithParams: Crypto.Func[Array[Byte], (Option[Array[Byte]], CipherParameters)] =
// setup cipher parameters with key Crypto(
params params(key) key
} yield DataWithParams(data, params) if (withIV) {
val ivData = generateIV
// setup cipher parameters with key and IV
paramsWithIV(key, ivData).map(k (Some(ivData), k))
} else {
params(key).map(k (None, k))
}
)
private val detachDataAndGetParams: Crypto.Func[(Array[Byte], Array[Char], Array[Byte], Boolean), DataWithParams] =
Crypto {
case (data, password, salt, withIV)
if (withIV) {
for {
ivDataWithEncData detachIV(data -> IV_SIZE)
key initSecretKey(password -> salt)
// setup cipher parameters with key and IV
paramsWithIV paramsWithIV(key, ivDataWithEncData.ivData)
} yield DataWithParams(ivDataWithEncData.encData, paramsWithIV)
} else {
for {
key initSecretKey(password -> salt)
// setup cipher parameters with key
params params(key)
} yield DataWithParams(data, params)
}
}
val decrypt: Crypto.Func[Array[Byte], Array[Byte]] =
Crypto[Array[Byte], Array[Byte]] { input: Array[Byte]
for {
dataWithParams detachDataAndGetParams((input, password, salt, withIV))
decData processData((dataWithParams, None, /*encrypt =*/ false))
} yield decData
}
val encrypt: Crypto.Func[Array[Byte], Array[Byte]] =
Crypto { input: Array[Byte]
for {
key initSecretKey(password -> salt)
extDataWithParams extDataWithParams(key)
encData processData((DataWithParams(input, extDataWithParams._2), extDataWithParams._1, /*encrypt =*/ true))
} yield encData
} }
}
} }
object AesCrypt extends slogging.LazyLogging { object AesCrypt {
def build(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[Array[Byte]] = { def build(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[Array[Byte]] = {
val aes = new AesCrypt(password.toHex.toCharArray, withIV, config) val aes = new AesCrypt(password.toHex.toCharArray, withIV, config)
Crypto.Bijection(aes.encrypt, aes.decrypt) Crypto.Cipher(aes.encrypt, aes.decrypt)
} }
def forString(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[String] = {
implicit val codec: PureCodec[String, Array[Byte]] =
PureCodec.build(_.getBytes, bytes new String(bytes))
apply[String](password, withIV, config)
}
def apply[T](password: ByteVector, withIV: Boolean, config: AesConfig)(
implicit codec: PureCodec[T, Array[Byte]]
): Crypto.Cipher[T] =
Crypto.codec[T, Array[Byte]] andThen build(password, withIV, config)
} }

View File

@ -17,7 +17,6 @@
package fluence.crypto package fluence.crypto
import cats.instances.try_._
import fluence.crypto.aes.{AesConfig, AesCrypt} import fluence.crypto.aes.{AesConfig, AesCrypt}
import org.scalactic.source.Position import org.scalactic.source.Position
import org.scalatest.{Assertion, Matchers, WordSpec} import org.scalatest.{Assertion, Matchers, WordSpec}
@ -34,51 +33,44 @@ class AesSpec extends WordSpec with Matchers with slogging.LazyLogging {
"work with IV" in { "work with IV" in {
val pass = ByteVector("pass".getBytes()) val pass = ByteVector("pass".getBytes())
val crypt = AesCrypt.forString(pass, withIV = true, config = conf) val crypt = AesCrypt.build(pass, withIV = true, config = conf)
val str = rndString(200) val str = rndString(200).getBytes
val crypted = crypt.direct.unsafe(str) val crypted = crypt.encrypt(str).right.get
crypt.inverse.unsafe(crypted) shouldBe str crypt.decrypt(crypted).right.get shouldBe str
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = true, config = conf) val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = true, config = conf)
checkCryptoError(fakeAes.inverse.runF[Try](crypted)) checkCryptoError(fakeAes.decrypt(crypted))
//we cannot check if first bytes is iv or already data, but encryption goes wrong //we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithoutIV = AesCrypt.forString(pass, withIV = false, config = conf) val aesWithoutIV = AesCrypt.build(pass, withIV = false, config = conf)
aesWithoutIV.inverse.unsafe(crypted) shouldNot be(str) aesWithoutIV.decrypt(crypted).right.get shouldNot be(str)
val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10))) val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted)) checkCryptoError(aesWrongSalt.decrypt(crypted))
} }
"work without IV" in { "work without IV" in {
val pass = ByteVector("pass".getBytes()) val pass = ByteVector("pass".getBytes())
val crypt = AesCrypt.forString(pass, withIV = false, config = conf) val crypt = AesCrypt.build(pass, withIV = false, config = conf)
val str = rndString(200) val str = rndString(200).getBytes()
val crypted = crypt.direct.unsafe(str) val crypted = crypt.encrypt(str).right.get
crypt.inverse.unsafe(crypted) shouldBe str crypt.decrypt(crypted).right.get shouldBe str
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = false, config = conf) val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = false, config = conf)
checkCryptoError(fakeAes.inverse.runF[Try](crypted)) checkCryptoError(fakeAes.decrypt(crypted))
//we cannot check if first bytes is iv or already data, but encryption goes wrong //we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithIV = AesCrypt.forString(pass, withIV = true, config = conf) val aesWithIV = AesCrypt.build(pass, withIV = true, config = conf)
aesWithIV.inverse.unsafe(crypted) shouldNot be(str) aesWithIV.decrypt(crypted).right.get shouldNot be(str)
val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10))) val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted)) checkCryptoError(aesWrongSalt.decrypt(crypted))
} }
} }
def checkCryptoError(tr: Try[String])(implicit pos: Position): Assertion = { def checkCryptoError(tr: Crypto.Result[Array[Byte]])(implicit pos: Position): Assertion = {
tr.map(_ false) tr.isLeft shouldBe true
.recover {
case e: CryptoError true
case e
logger.error("Unexpected error", e)
false
}
.get shouldBe true
} }
} }

View File

@ -17,18 +17,32 @@
package fluence.crypto package fluence.crypto
import fluence.codec.{CodecError, MonadicalEitherArrow, PureCodec} import cats.data.Kleisli
object Crypto extends MonadicalEitherArrow[CryptoError] { import scala.util.Try
type Hasher[A, B] = Func[A, B]
type Cipher[A] = Bijection[A, Array[Byte]] object Crypto {
type Result[T] = Either[CryptoError, T]
type KeyPairGenerator = Func[Option[Array[Byte]], KeyPair] type Hasher[A, B] = Kleisli[Result, A, B]
// TODO: move it to MonadicalEitherArrow, make liftTry with try-catch, and easy conversions for other funcs and bijections type Func[A, B] = Kleisli[Result, A, B]
implicit val liftCodecErrorToCrypto: CodecError CryptoError = err CryptoError("Codec error", Some(err))
implicit def codec[A, B](implicit codec: PureCodec[A, B]): Bijection[A, B] = case class Cipher[A](
Bijection(fromOtherFunc(codec.direct), fromOtherFunc(codec.inverse)) encrypt: Kleisli[Result, A, Array[Byte]],
decrypt: Kleisli[Result, Array[Byte], A]
)
type KeyPairGenerator = Kleisli[Result, Option[Array[Byte]], KeyPair]
def apply[A, B](fn: A Result[B]): Func[A, B] = Kleisli[Result, A, B](fn)
def tryFn[A, B](fn: A B)(errorText: String): Crypto.Func[A, B] =
Crypto(a tryUnit(fn(a))(errorText))
def tryUnit[B](fn: B)(errorText: String): Result[B] =
Try(fn).toEither.left.map(t CryptoError(errorText, Some(t)))
def cond[B](ifTrue: B, errorText: String): Crypto.Func[Boolean, B] =
Crypto(Either.cond(_, ifTrue, CryptoError(errorText)))
} }

View File

@ -17,23 +17,11 @@
package fluence.crypto package fluence.crypto
import cats.Applicative import scala.language.higherKinds
import cats.data.EitherT import scala.util.control.NoStackTrace
import scala.util.control.{NoStackTrace, NonFatal}
case class CryptoError(message: String, causedBy: Option[Throwable] = None) extends NoStackTrace { case class CryptoError(message: String, causedBy: Option[Throwable] = None) extends NoStackTrace {
override def getMessage: String = message override def getMessage: String = message
override def getCause: Throwable = causedBy getOrElse super.getCause override def getCause: Throwable = causedBy getOrElse super.getCause
} }
object CryptoError {
// TODO: there's a common `catchNonFatal` pattern, we should refactor this metod onto it
def nonFatalHandling[F[_]: Applicative, A](a: A)(errorText: String): EitherT[F, CryptoError, A] =
try EitherT.pure(a)
catch {
case NonFatal(e) EitherT.leftT(CryptoError(errorText + ": " + e.getLocalizedMessage, Some(e)))
}
}

View File

@ -19,9 +19,9 @@ package fluence.crypto
import java.security.SecureRandom import java.security.SecureRandom
import cats.Monad import cats.data.Kleisli
import cats.data.EitherT
import fluence.crypto.signature.{SignAlgo, Signature, SignatureChecker, Signer} import fluence.crypto.signature.{SignAlgo, Signature, SignatureChecker, Signer}
import cats.syntax.either._
import scodec.bits.ByteVector import scodec.bits.ByteVector
import scala.language.higherKinds import scala.language.higherKinds
@ -31,26 +31,34 @@ object DumbCrypto {
lazy val signAlgo: SignAlgo = lazy val signAlgo: SignAlgo =
SignAlgo( SignAlgo(
"dumb", "dumb",
Crypto.liftFunc { seedOpt Kleisli[Crypto.Result, Option[Array[Byte]], KeyPair] { seedOpt
val seed = seedOpt.getOrElse { val seed = seedOpt.getOrElse {
new SecureRandom().generateSeed(32) new SecureRandom().generateSeed(32)
} }
KeyPair.fromBytes(seed, seed) KeyPair.fromBytes(seed, seed).asRight
}, },
keyPair Signer(keyPair.publicKey, Crypto.liftFunc(plain Signature(plain.reverse))), keyPair
Signer(
keyPair.publicKey,
Kleisli[Crypto.Result, ByteVector, Signature](plain Signature(plain.reverse).asRight)
),
publicKey publicKey
new SignatureChecker { SignatureChecker(
override def check[F[_]: Monad](signature: Signature, plain: ByteVector): EitherT[F, CryptoError, Unit] = Kleisli {
EitherT.cond[F](signature.sign == plain.reverse, (), CryptoError("Signatures mismatch")) case (sgn, msg) Either.cond(sgn.sign == msg.reverse, (), CryptoError("Signatures mismatch"))
} }
)
) )
lazy val cipherString: Crypto.Cipher[String] = lazy val cipherString: Crypto.Cipher[String] =
Crypto.liftB(_.getBytes, bytes new String(bytes)) Crypto.Cipher(
Kleisli[Crypto.Result, String, Array[Byte]](_.getBytes.asRight[CryptoError]),
Kleisli[Crypto.Result, Array[Byte], String](bytes new String(bytes).asRight[CryptoError])
)
lazy val noOpHasher: Crypto.Hasher[Array[Byte], Array[Byte]] = lazy val noOpHasher: Crypto.Hasher[Array[Byte], Array[Byte]] =
Crypto.identityFunc[Array[Byte]] Kleisli[Crypto.Result, Array[Byte], Array[Byte]](_.asRight)
lazy val testHasher: Crypto.Hasher[Array[Byte], Array[Byte]] = lazy val testHasher: Crypto.Hasher[Array[Byte], Array[Byte]] =
Crypto.liftFunc(bytes ("H<" + new String(bytes) + ">").getBytes()) Kleisli[Crypto.Result, Array[Byte], Array[Byte]](bytes ("H<" + new String(bytes) + ">").getBytes().asRight)
} }

View File

@ -18,8 +18,10 @@
package fluence.crypto.cipher package fluence.crypto.cipher
import cats.Monad import cats.Monad
import cats.data.EitherT import cats.data.Kleisli
import fluence.crypto.{Crypto, CryptoError} import fluence.crypto.Crypto
import cats.instances.either._
import cats.syntax.either._
import scala.collection.Searching.{Found, InsertionPoint, SearchResult} import scala.collection.Searching.{Found, InsertionPoint, SearchResult}
import scala.language.higherKinds import scala.language.higherKinds
@ -41,13 +43,10 @@ object CipherSearch {
def binarySearch[A, B](coll: IndexedSeq[A], decrypt: Crypto.Func[A, B])( def binarySearch[A, B](coll: IndexedSeq[A], decrypt: Crypto.Func[A, B])(
implicit ordering: Ordering[B] implicit ordering: Ordering[B]
): Crypto.Func[B, SearchResult] = ): Crypto.Func[B, SearchResult] =
new Crypto.Func[B, SearchResult] { Kleisli { input
override def apply[F[_]](input: B)( {
implicit F: Monad[F] implicitly[Monad[Crypto.Result]].tailRecM((0, coll.length)) {
): EitherT[F, CryptoError, SearchResult] = { case (from, to) if from == to Right(InsertionPoint(from)).asRight
type M[X] = EitherT[F, CryptoError, X]
implicitly[Monad[M]].tailRecM((0, coll.length)) {
case (from, to) if from == to EitherT.rightT(Right(InsertionPoint(from)))
case (from, to) case (from, to)
val idx = from + (to - from - 1) / 2 val idx = from + (to - from - 1) / 2
decrypt(coll(idx)).map { d decrypt(coll(idx)).map { d

View File

@ -17,11 +17,8 @@
package fluence.crypto.signature package fluence.crypto.signature
import cats.Monad import cats.data.Kleisli
import cats.data.EitherT import fluence.crypto.{Crypto, KeyPair}
import cats.syntax.strong._
import cats.syntax.compose._
import fluence.crypto.{Crypto, CryptoError, KeyPair}
import scodec.bits.ByteVector import scodec.bits.ByteVector
import scala.language.higherKinds import scala.language.higherKinds
@ -38,7 +35,7 @@ case class SignAlgo(
name: String, name: String,
generateKeyPair: Crypto.KeyPairGenerator, generateKeyPair: Crypto.KeyPairGenerator,
signer: SignAlgo.SignerFn, signer: SignAlgo.SignerFn,
implicit val checker: SignAlgo.CheckerFn, implicit val checker: SignAlgo.CheckerFn
) )
object SignAlgo { object SignAlgo {
@ -46,27 +43,13 @@ object SignAlgo {
type CheckerFn = KeyPair.Public SignatureChecker type CheckerFn = KeyPair.Public SignatureChecker
/**
* Take checker, signature, and plain data, and apply checker, returning Unit on success, or left side error.
*/
private val fullChecker: Crypto.Func[((SignatureChecker, Signature), ByteVector), Unit] =
new Crypto.Func[((SignatureChecker, Signature), ByteVector), Unit] {
override def apply[F[_]: Monad](
input: ((SignatureChecker, Signature), ByteVector)
): EitherT[F, CryptoError, Unit] = {
val ((signatureChecker, signature), plainData) = input
signatureChecker.check(signature, plainData)
}
}
/** /**
* For CheckerFn, builds a function that takes PubKeyAndSignature along with plain data, and checks the signature. * For CheckerFn, builds a function that takes PubKeyAndSignature along with plain data, and checks the signature.
*/ */
def checkerFunc(fn: CheckerFn): Crypto.Func[(PubKeyAndSignature, ByteVector), Unit] = def checkerFunc(fn: CheckerFn): Crypto.Func[(PubKeyAndSignature, ByteVector), Unit] =
Crypto Kleisli {
.liftFunc[PubKeyAndSignature, (SignatureChecker, Signature)] { case (pks, msg)
case PubKeyAndSignature(pk, signature) fn(pk) -> signature fn(pks.publicKey).check.run(pks.signature -> msg)
} }
.first[ByteVector] andThen fullChecker
} }

View File

@ -17,13 +17,10 @@
package fluence.crypto.signature package fluence.crypto.signature
import cats.Monad import cats.data.Kleisli
import cats.data.EitherT import fluence.crypto.Crypto
import fluence.crypto.CryptoError
import scodec.bits.ByteVector import scodec.bits.ByteVector
import scala.language.higherKinds import scala.language.higherKinds
trait SignatureChecker { case class SignatureChecker(check: Kleisli[Crypto.Result, (Signature, ByteVector), Unit])
def check[F[_]: Monad](signature: Signature, plain: ByteVector): EitherT[F, CryptoError, Unit]
}

View File

@ -17,10 +17,10 @@
package fluence.crypto.signature package fluence.crypto.signature
import cats.syntax.profunctor._ import cats.instances.either._
import fluence.crypto.{Crypto, KeyPair} import fluence.crypto.{Crypto, KeyPair}
import scodec.bits.ByteVector import scodec.bits.ByteVector
case class Signer(publicKey: KeyPair.Public, sign: Crypto.Func[ByteVector, Signature]) { case class Signer(publicKey: KeyPair.Public, sign: Crypto.Func[ByteVector, Signature]) {
lazy val signWithPK: Crypto.Func[ByteVector, PubKeyAndSignature] = sign.rmap(PubKeyAndSignature(publicKey, _)) lazy val signWithPK: Crypto.Func[ByteVector, PubKeyAndSignature] = sign.map(PubKeyAndSignature(publicKey, _))
} }

View File

@ -30,17 +30,17 @@ class CryptoSearchingSpec extends WordSpec with Matchers {
val crypt: Crypto.Cipher[String] = DumbCrypto.cipherString val crypt: Crypto.Cipher[String] = DumbCrypto.cipherString
val plainTextElements = Array("A", "B", "C", "D", "E") val plainTextElements = Array("A", "B", "C", "D", "E")
val encryptedElements = plainTextElements.map(t crypt.direct.unsafe(t)) val encryptedElements = plainTextElements.map(t crypt.encrypt.run(t).right.get)
val search = CipherSearch.binarySearch(encryptedElements, crypt.inverse) val search = CipherSearch.binarySearch(encryptedElements, crypt.decrypt)
search.unsafe("B") shouldBe Found(1) search("B").right.get shouldBe Found(1)
search.unsafe("D") shouldBe Found(3) search("D").right.get shouldBe Found(3)
search.unsafe("E") shouldBe Found(4) search("E").right.get shouldBe Found(4)
search.unsafe("0") shouldBe InsertionPoint(0) search("0").right.get shouldBe InsertionPoint(0)
search.unsafe("BB") shouldBe InsertionPoint(2) search("BB").right.get shouldBe InsertionPoint(2)
search.unsafe("ZZ") shouldBe InsertionPoint(5) search("ZZ").right.get shouldBe InsertionPoint(5)
} }
} }

View File

@ -27,11 +27,11 @@ class NoOpCryptSpec extends WordSpec with Matchers {
val noOpCrypt = DumbCrypto.cipherString val noOpCrypt = DumbCrypto.cipherString
val emptyString = "" val emptyString = ""
noOpCrypt.inverse.unsafe(noOpCrypt.direct.unsafe(emptyString)) shouldBe emptyString noOpCrypt.decrypt(noOpCrypt.encrypt(emptyString).right.get).right.get shouldBe emptyString
val nonEmptyString = "some text here" val nonEmptyString = "some text here"
noOpCrypt.inverse.unsafe(noOpCrypt.direct.unsafe(nonEmptyString)) shouldBe nonEmptyString noOpCrypt.decrypt(noOpCrypt.encrypt(nonEmptyString).right.get).right.get shouldBe nonEmptyString
val byteArray = Array(1.toByte, 23.toByte, 45.toByte) val byteArray = Array(1.toByte, 23.toByte, 45.toByte)
noOpCrypt.direct.unsafe(noOpCrypt.inverse.unsafe(byteArray)) shouldBe byteArray noOpCrypt.encrypt(noOpCrypt.decrypt(byteArray).right.get).right.get shouldBe byteArray
} }
} }
} }

View File

@ -0,0 +1,29 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto
import io.scalajs.nodejs.buffer.Buffer
import scodec.bits.ByteVector
import scala.language.higherKinds
object CryptoJsHelpers {
implicit class ByteVectorOp(bv: ByteVector) {
def toJsBuffer: Buffer = Buffer.from(ByteVector(bv.toArray).toHex, "hex")
}
}

View File

@ -17,8 +17,6 @@
package fluence.crypto.ecdsa package fluence.crypto.ecdsa
import cats.Monad
import cats.data.EitherT
import fluence.crypto._ import fluence.crypto._
import fluence.crypto.facade.ecdsa.EC import fluence.crypto.facade.ecdsa.EC
import fluence.crypto.hash.JsCryptoHasher import fluence.crypto.hash.JsCryptoHasher
@ -35,54 +33,62 @@ import scala.scalajs.js.typedarray.Uint8Array
* @param ec implementation of ecdsa logic for different curves * @param ec implementation of ecdsa logic for different curves
*/ */
class Ecdsa(ec: EC, hasher: Option[Crypto.Hasher[Array[Byte], Array[Byte]]]) { class Ecdsa(ec: EC, hasher: Option[Crypto.Hasher[Array[Byte], Array[Byte]]]) {
import CryptoError.nonFatalHandling
/**
* Restores key pair by secret key.
*
*/
val restoreKeyPair: Crypto.Func[KeyPair.Secret, KeyPair] =
Crypto.tryFn[KeyPair.Secret, KeyPair] { secretKey
val key = ec.keyFromPrivate(secretKey.value.toHex, "hex")
val publicHex = key.getPublic(compact = true, "hex")
val secretHex = key.getPrivate("hex")
val public = ByteVector.fromValidHex(publicHex)
val secret = ByteVector.fromValidHex(secretHex)
KeyPair.fromByteVectors(public, secret)
}("Incorrect secret key format")
val generateKeyPair: Crypto.KeyPairGenerator = val generateKeyPair: Crypto.KeyPairGenerator =
new Crypto.Func[Option[Array[Byte]], KeyPair] { Crypto.tryFn[Option[Array[Byte]], KeyPair] { input
override def apply[F[_]](input: Option[Array[Byte]])(implicit F: Monad[F]): EitherT[F, CryptoError, KeyPair] = val seedJs = input.map(bs js.Dynamic.literal(entropy = bs.toJSArray))
nonFatalHandling { val key = ec.genKeyPair(seedJs)
val seedJs = input.map(bs js.Dynamic.literal(entropy = bs.toJSArray)) val publicHex = key.getPublic(compact = true, "hex")
val key = ec.genKeyPair(seedJs) val secretHex = key.getPrivate("hex")
val publicHex = key.getPublic(true, "hex") val public = ByteVector.fromValidHex(publicHex)
val secretHex = key.getPrivate("hex") val secret = ByteVector.fromValidHex(secretHex)
val public = ByteVector.fromValidHex(publicHex) KeyPair.fromByteVectors(public, secret)
val secret = ByteVector.fromValidHex(secretHex) }("Failed to generate key pair")
KeyPair.fromByteVectors(public, secret)
}("Failed to generate key pair.") val sign: Crypto.Func[(KeyPair, ByteVector), Signature] =
Crypto {
case (keyPair: KeyPair, message: ByteVector)
for {
secret Crypto.tryUnit {
ec.keyFromPrivate(keyPair.secretKey.value.toHex, "hex")
}("Cannot get private key from key pair")
hash JsCryptoHasher.hashJs(message, hasher)
signHex Crypto.tryUnit(secret.sign(new Uint8Array(hash)).toDER("hex"))("Cannot sign message")
} yield Signature(ByteVector.fromValidHex(signHex))
} }
def sign[F[_]: Monad](keyPair: KeyPair, message: ByteVector): EitherT[F, CryptoError, Signature] = val verify: Crypto.Func[(KeyPair.Public, Signature, ByteVector), Unit] =
for { Crypto {
secret nonFatalHandling { case (
ec.keyFromPrivate(keyPair.secretKey.value.toHex, "hex") pubKey,
}("Cannot get private key from key pair.") signature,
hash hash(message) message
signHex nonFatalHandling(secret.sign(new Uint8Array(hash)).toDER("hex"))("Cannot sign message") )
} yield Signature(ByteVector.fromValidHex(signHex)) for {
public Crypto.tryUnit {
val hex = pubKey.value.toHex
ec.keyFromPublic(hex, "hex")
}("Incorrect public key format.")
hash JsCryptoHasher.hashJs(message, hasher)
verify Crypto.tryUnit(public.verify(new Uint8Array(hash), signature.sign.toHex))("Cannot verify message")
_ Either.cond(verify, (), CryptoError("Signature is not verified"))
} yield ()
}
def hash[F[_]: Monad](message: ByteVector): EitherT[F, CryptoError, js.Array[Byte]] = {
val arr = message.toArray
hasher
.fold(EitherT.pure[F, CryptoError](arr)) { h
h[F](arr)
}
.map(_.toJSArray)
}
def verify[F[_]: Monad](
pubKey: KeyPair.Public,
signature: Signature,
message: ByteVector
): EitherT[F, CryptoError, Unit] =
for {
public nonFatalHandling {
val hex = pubKey.value.toHex
ec.keyFromPublic(hex, "hex")
}("Incorrect public key format.")
hash hash(message)
verify nonFatalHandling(public.verify(new Uint8Array(hash), signature.sign.toHex))("Cannot verify message.")
_ EitherT.cond[F](verify, (), CryptoError("Signature is not verified"))
} yield ()
} }
object Ecdsa { object Ecdsa {
@ -94,20 +100,13 @@ object Ecdsa {
signer = kp signer = kp
Signer( Signer(
kp.publicKey, kp.publicKey,
new Crypto.Func[ByteVector, signature.Signature] { ecdsa_secp256k1_sha256.sign.local(kp -> _)
override def apply[F[_]]( ),
input: ByteVector
)(implicit F: Monad[F]): EitherT[F, CryptoError, signature.Signature] =
ecdsa_secp256k1_sha256.sign(kp, input)
}
),
checker = pk checker = pk
new SignatureChecker { SignatureChecker(
override def check[F[_]: Monad]( ecdsa_secp256k1_sha256.verify.local {
signature: fluence.crypto.signature.Signature, case (signature, plain) (pk, signature, plain)
plain: ByteVector }
): EitherT[F, CryptoError, Unit] = )
ecdsa_secp256k1_sha256.verify(pk, signature, plain)
}
) )
} }

View File

@ -0,0 +1,101 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.eddsa
import fluence.crypto.facade.ed25519.Supercop
import fluence.crypto.hash.JsCryptoHasher
import fluence.crypto.{Crypto, CryptoError, CryptoJsHelpers, KeyPair}
import fluence.crypto.signature.{SignAlgo, Signature, SignatureChecker, Signer}
import scodec.bits.ByteVector
import scala.language.higherKinds
class Ed25519(hasher: Option[Crypto.Hasher[Array[Byte], Array[Byte]]]) {
import CryptoJsHelpers._
val sign: Crypto.Func[(KeyPair, ByteVector), Signature] =
Crypto {
case (keyPair, message)
for {
hash JsCryptoHasher.hash(message, hasher)
sign Crypto.tryUnit {
Supercop.sign(
ByteVector(hash).toJsBuffer,
keyPair.publicKey.value.toJsBuffer,
keyPair.secretKey.value.toJsBuffer
)
}("Error on signing message by js/ed25519 signature")
} yield Signature(ByteVector.fromValidHex(sign.toString("hex")))
}
val verify: Crypto.Func[(KeyPair.Public, Signature, ByteVector), Unit] =
Crypto {
case (
pubKey,
signature,
message
)
for {
hash JsCryptoHasher.hash(message, hasher)
verify Crypto.tryUnit(
Supercop.verify(signature.sign.toJsBuffer, ByteVector(hash).toJsBuffer, pubKey.value.toJsBuffer)
)("Cannot verify message")
_ Either.cond(verify, (), CryptoError("Signature is not verified"))
} yield ()
}
val generateKeyPair: Crypto.KeyPairGenerator =
Crypto[Option[Array[Byte]], KeyPair] { input
for {
seed Crypto.tryUnit(input.map(ByteVector(_).toJsBuffer).getOrElse(Supercop.createSeed()))(
"Error on seed creation"
)
jsKeyPair Crypto.tryUnit(Supercop.createKeyPair(seed))("Error on key pair generation.")
keyPair Crypto.tryUnit(
KeyPair.fromByteVectors(
ByteVector.fromValidHex(jsKeyPair.publicKey.toString("hex")),
ByteVector.fromValidHex(jsKeyPair.secretKey.toString("hex"))
)
)("Error on decoding public and secret keys")
} yield keyPair
}
}
object Ed25519 {
val ed25519: Ed25519 = new Ed25519(None)
val signAlgo: SignAlgo =
SignAlgo(
name = "ed25519",
generateKeyPair = ed25519.generateKeyPair,
signer = kp
Signer(
kp.publicKey,
ed25519.sign.local(kp -> _)
),
checker = pk
SignatureChecker(
ed25519.verify.local {
case (signature, plain) (pk, signature, plain)
}
)
)
}

View File

@ -0,0 +1,27 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.facade
import scala.scalajs.js
import scala.scalajs.js.annotation.JSGlobal
@js.native
@JSGlobal("Buffer")
class Buffer(arr: js.Array[Byte]) extends js.Object {
def toString(enc: String): String = js.native
}

View File

@ -0,0 +1,38 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.facade.ed25519
import io.scalajs.nodejs.buffer.Buffer
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
@js.native
trait KeyPair extends js.Object {
val publicKey: Buffer
val secretKey: Buffer
}
@js.native
@JSImport("supercop.js", JSImport.Namespace)
object Supercop extends js.Object {
def verify(sig: Buffer, msg: Buffer, pubKey: Buffer): Boolean = js.native
def sign(msg: Buffer, pubKey: Buffer, privKey: Buffer): Buffer = js.native
def createKeyPair(seed: Buffer): KeyPair = js.native
def createSeed(): Buffer = js.native
}

View File

@ -17,31 +17,54 @@
package fluence.crypto.hash package fluence.crypto.hash
import cats.instances.either._
import cats.syntax.either._
import fluence.crypto.{Crypto, CryptoError} import fluence.crypto.{Crypto, CryptoError}
import fluence.crypto.facade.ecdsa.{SHA1, SHA256} import fluence.crypto.facade.ecdsa.{SHA1, SHA256}
import scodec.bits.ByteVector import scodec.bits.ByteVector
import scala.language.higherKinds
import scala.scalajs.js
import scala.scalajs.js.JSConverters._ import scala.scalajs.js.JSConverters._
import scala.scalajs.js.typedarray.Uint8Array import scala.scalajs.js.typedarray.Uint8Array
import scala.util.Try
object JsCryptoHasher { object JsCryptoHasher {
lazy val Sha256: Crypto.Hasher[Array[Byte], Array[Byte]] = lazy val Sha256: Crypto.Hasher[Array[Byte], Array[Byte]] =
Crypto.liftFuncEither[Array[Byte], Array[Byte]] { msg Crypto.tryFn[Array[Byte], Array[Byte]] { msg
Try { val sha256 = new SHA256()
val sha256 = new SHA256() sha256.update(new Uint8Array(msg.toJSArray))
sha256.update(new Uint8Array(msg.toJSArray)) ByteVector.fromValidHex(sha256.digest("hex")).toArray
ByteVector.fromValidHex(sha256.digest("hex")).toArray }("Cannot calculate Sha256 hash")
}.toEither.left.map(err CryptoError("Cannot calculate Sha256 hash", Some(err)))
}
lazy val Sha1: Crypto.Hasher[Array[Byte], Array[Byte]] = lazy val Sha1: Crypto.Hasher[Array[Byte], Array[Byte]] =
Crypto.liftFuncEither[Array[Byte], Array[Byte]] { msg Crypto.tryFn[Array[Byte], Array[Byte]] { msg
Try { val sha1 = new SHA1()
val sha1 = new SHA1() sha1.update(new Uint8Array(msg.toJSArray))
sha1.update(new Uint8Array(msg.toJSArray)) ByteVector.fromValidHex(sha1.digest("hex")).toArray
ByteVector.fromValidHex(sha1.digest("hex")).toArray }("Cannot calculate Sha256 hash")
}.toEither.left.map(err CryptoError("Cannot calculate Sha256 hash", Some(err)))
/**
* Calculates hash of message.
*
* @return hash in Scala array
*/
val hash: Crypto.Func[(ByteVector, Option[Crypto.Hasher[Array[Byte], Array[Byte]]]), Array[Byte]] =
Crypto {
case (message, hasher)
val arr = message.toArray
hasher
.fold(arr.asRight[CryptoError]) { h
h(arr)
}
} }
/**
* Calculates hash of message.
*
* @return hash in JS array
*/
val hashJs: Crypto.Func[(ByteVector, Option[Crypto.Hasher[Array[Byte], Array[Byte]]]), js.Array[Byte]] =
hash
.map(_.toJSArray)
} }

View File

@ -1,99 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package flyence.crypto
import cats.data.EitherT
import cats.instances.try_._
import fluence.crypto.ecdsa.Ecdsa
import fluence.crypto.signature.Signature
import fluence.crypto.{CryptoError, KeyPair}
import org.scalatest.{Matchers, WordSpec}
import scodec.bits.ByteVector
import scala.util.{Random, Try}
class EcdsaSpec extends WordSpec with Matchers {
def rndBytes(size: Int) = 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)
}
"ecdsa algorithm" should {
"correct sign and verify data" in {
val algorithm = Ecdsa.ecdsa_secp256k1_sha256
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 = Ecdsa.signAlgo
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 = Ecdsa.signAlgo
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
}
}
}

View File

@ -1,64 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package flyence.crypto
import fluence.crypto.facade.ecdsa.{SHA1, SHA256}
import org.scalatest.{Matchers, WordSpec}
import scodec.bits.{Bases, ByteVector}
import scala.scalajs.js.JSConverters._
import scala.scalajs.js.typedarray.Uint8Array
class JSHashSpec extends WordSpec with Matchers {
"js hasher" should {
//test values get from third-party hash services
"work with sha256" in {
val str = "sha256Tester"
val sha256TesterHex = "513c17f8cf6ba96ce412cc2ae82f68821e9a2c6ae7a2fb1f5e46d08c387c8e65"
val hasher = new SHA256()
hasher.update(new Uint8Array(str.getBytes().toJSArray))
val hex = hasher.digest("hex")
hex shouldBe sha256TesterHex
}
"work with sha1" in {
val str = "sha1Tester"
val sha1TesterHex = "879db20eabcecea7d4736a8bae5bc64564b76b2f"
val hasher = new SHA1()
hasher.update(new Uint8Array(str.getBytes().toJSArray))
val hex = hasher.digest("hex")
hex shouldBe sha1TesterHex
}
"check unsigned array with sha1" in {
val arr = Array[Byte](3, -9, -31, 48, 10, 125, 51, -39, -20, -125, 123, 61, -36, 49, 76, 90, -16, 54, -61, 62, 50,
-116, -37, -88, -125, -32, -105, 120, 118, 13, -37, 33, -36)
val base64Check = "9keNwsj08vKTlwIpHAEYvsfpdP4="
val hasher = new SHA1()
hasher.update(new Uint8Array(arr.toJSArray))
val hex = hasher.digest("hex")
ByteVector.fromValidHex(hex, Bases.Alphabets.HexLowercase).toBase64 shouldBe base64Check
}
}
}

View File

@ -21,8 +21,8 @@ import java.math.BigInteger
import java.security._ import java.security._
import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPrivateKey
import cats.Monad import cats.instances.either._
import cats.data.EitherT import cats.syntax.either._
import fluence.crypto.KeyPair.Secret import fluence.crypto.KeyPair.Secret
import fluence.crypto.{KeyPair, _} import fluence.crypto.{KeyPair, _}
import fluence.crypto.hash.JdkCryptoHasher import fluence.crypto.hash.JdkCryptoHasher
@ -43,130 +43,158 @@ import scala.language.higherKinds
class Ecdsa(curveType: String, scheme: String, hasher: Option[Crypto.Hasher[Array[Byte], Array[Byte]]]) class Ecdsa(curveType: String, scheme: String, hasher: Option[Crypto.Hasher[Array[Byte], Array[Byte]]])
extends JavaAlgorithm { extends JavaAlgorithm {
import CryptoError.nonFatalHandling
import Ecdsa._ import Ecdsa._
val HEXradix = 16 val HEXradix = 16
val generateKeyPair: Crypto.KeyPairGenerator = /**
new Crypto.Func[Option[Array[Byte]], KeyPair] { * Restores pair of keys from the known secret key.
override def apply[F[_]]( * The public key will be the same each method call with the same secret key.
input: Option[Array[Byte]] * sk secret key
)(implicit F: Monad[F]): EitherT[F, CryptoError, fluence.crypto.KeyPair] = * @return key pair
*/
val restorePairFromSecret: Crypto.Func[Secret, KeyPair] =
Crypto(
sk
for { for {
ecSpec EitherT.fromOption( ecSpec Either.fromOption(
Option(ECNamedCurveTable.getParameterSpec(curveType)), Option(ECNamedCurveTable.getParameterSpec(curveType)),
CryptoError("Parameter spec for the curve is not available.") CryptoError("Parameter spec for the curve is not available.")
) )
g getKeyPairGenerator keyPair Crypto.tryUnit {
_ nonFatalHandling { val hex = sk.value.toHex
g.initialize(ecSpec, input.map(new SecureRandom(_)).getOrElse(new SecureRandom())) val d = new BigInteger(hex, HEXradix)
}(s"Could not initialize KeyPairGenerator") // to re-create public key from private we need to multiply known from curve point G with D (private key)
p EitherT.fromOption(Option(g.generateKeyPair()), CryptoError("Could not generate KeyPair. Unexpected.")) // result will be point Q (public key)
keyPair nonFatalHandling { // https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
//store S number for private key and compressed Q point on curve for public key val g = ecSpec.getG
val pk = ByteVector(p.getPublic.asInstanceOf[ECPublicKey].getQ.getEncoded(true)) val q = g.multiply(d)
val bg = p.getPrivate.asInstanceOf[ECPrivateKey].getS val pk = ByteVector(q.getEncoded(true))
val sk = ByteVector.fromValidHex(bg.toString(HEXradix)) KeyPair.fromByteVectors(pk, sk.value)
KeyPair.fromByteVectors(pk, sk) }("Could not generate KeyPair from private key. Unexpected.")
}("Could not generate KeyPair. Unexpected.")
} yield keyPair } yield keyPair
)
private def curveSpec: Crypto.Result[ECParameterSpec] =
Crypto.tryUnit(ECNamedCurveTable.getParameterSpec(curveType).asInstanceOf[ECParameterSpec])(
"Cannot get curve parameters"
)
private def getKeyPairGenerator =
Crypto.tryUnit(KeyPairGenerator.getInstance(ECDSA, BouncyCastleProvider.PROVIDER_NAME))(
"Cannot get key pair generator"
)
val generateKeyPair: Crypto.KeyPairGenerator =
Crypto[Option[Array[Byte]], KeyPair] { input
for {
ecSpec Either.fromOption(
Option(ECNamedCurveTable.getParameterSpec(curveType)),
CryptoError("Parameter spec for the curve is not available.")
)
g getKeyPairGenerator
_ Crypto.tryUnit {
g.initialize(ecSpec, input.map(new SecureRandom(_)).getOrElse(new SecureRandom()))
}(s"Could not initialize KeyPairGenerator")
p Either.fromOption(Option(g.generateKeyPair()), CryptoError("Generated key pair is null"))
keyPair Crypto.tryUnit {
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")
} yield keyPair
} }
/** private def getKeyFactory =
* Restores pair of keys from the known secret key. Crypto.tryUnit(KeyFactory.getInstance(ECDSA, BouncyCastleProvider.PROVIDER_NAME))(
* The public key will be the same each method call with the same secret key. "Cannot get key factory instance"
* @param sk secret key )
* @return key pair
*/
def restorePairFromSecret[F[_]: Monad](sk: Secret): EitherT[F, CryptoError, KeyPair] =
for {
ecSpec EitherT.fromOption(
Option(ECNamedCurveTable.getParameterSpec(curveType)),
CryptoError("Parameter spec for the curve is not available.")
)
keyPair nonFatalHandling {
val hex = sk.value.toHex
val d = new BigInteger(hex, HEXradix)
// to re-create public key from private we need to multiply known from curve point G with D (private key)
// result will be point Q (public key)
// https://en.wikipedia.org/wiki/Elliptic_Curve_Digital_Signature_Algorithm
val g = ecSpec.getG
val q = g.multiply(d)
val pk = ByteVector(q.getEncoded(true))
KeyPair.fromByteVectors(pk, sk.value)
}("Could not generate KeyPair from private key. Unexpected.")
} yield keyPair
def sign[F[_]: Monad]( private def getSignatureProvider =
keyPair: KeyPair, Crypto.tryUnit(Signature.getInstance(scheme, BouncyCastleProvider.PROVIDER_NAME))(
message: ByteVector "Cannot get signature instance"
): EitherT[F, CryptoError, signature.Signature] = )
signMessage(new BigInteger(keyPair.secretKey.value.toHex, HEXradix), message.toArray)
private val signMessage: Crypto.Func[(BigInteger, Array[Byte]), Array[Byte]] =
Crypto {
case (
privateKey,
message
)
for {
ec curveSpec
keySpec Crypto.tryUnit(new ECPrivateKeySpec(privateKey, ec))("Cannot read private key.")
keyFactory getKeyFactory
signProvider getSignatureProvider
_ Crypto.tryUnit(signProvider.initSign(keyFactory.generatePrivate(keySpec)))("Cannot initSign")
hash hasher.fold(
message.asRight[CryptoError]
)(_.apply(message))
sign Crypto.tryUnit {
signProvider.update(hash)
signProvider.sign()
}("Cannot sign message.")
} yield sign
}
val sign: Crypto.Func[(KeyPair, ByteVector), signature.Signature] =
signMessage
.map(bb fluence.crypto.signature.Signature(ByteVector(bb))) .map(bb fluence.crypto.signature.Signature(ByteVector(bb)))
.local {
case (keyPair, message) (new BigInteger(keyPair.secretKey.value.toHex, HEXradix), message.toArray)
}
def verify[F[_]: Monad]( private val verifySign: Crypto.Func[(Array[Byte], Array[Byte], Array[Byte]), Unit] =
publicKey: KeyPair.Public, Crypto {
signature: fluence.crypto.signature.Signature, case (
message: ByteVector publicKey,
): EitherT[F, CryptoError, Unit] = signature,
verifySign(publicKey.bytes, signature.bytes, message.toArray) message
)
for {
ec curveSpec
keySpec Crypto.tryUnit(new ECPublicKeySpec(ec.getCurve.decodePoint(publicKey), ec))(
"Cannot read public key"
)
keyFactory getKeyFactory
signProvider getSignatureProvider
_ Crypto.tryUnit(
signProvider.initVerify(keyFactory.generatePublic(keySpec))
)("Cannot initVerify message")
private def signMessage[F[_]: Monad]( hash hasher.fold(
privateKey: BigInteger, message.asRight[CryptoError]
message: Array[Byte] )(_.apply(message))
): EitherT[F, CryptoError, Array[Byte]] =
for {
ec curveSpec
keySpec nonFatalHandling(new ECPrivateKeySpec(privateKey, ec))("Cannot read private key.")
keyFactory getKeyFactory
signProvider getSignatureProvider
sign nonFatalHandling {
signProvider.initSign(keyFactory.generatePrivate(keySpec))
signProvider.update(hasher.map(_.unsafe(message)).getOrElse(message))
signProvider.sign()
}("Cannot sign message.")
} yield sign _ Crypto.tryUnit(signProvider.update(hash))("Cannot update message")
private def verifySign[F[_]: Monad]( verify Crypto.tryUnit(signProvider.verify(signature))("Cannot verify message")
publicKey: Array[Byte],
signature: Array[Byte],
message: Array[Byte],
): EitherT[F, CryptoError, Unit] =
for {
ec curveSpec
keySpec nonFatalHandling(new ECPublicKeySpec(ec.getCurve.decodePoint(publicKey), ec))("Cannot read public key.")
keyFactory getKeyFactory
signProvider getSignatureProvider
verify nonFatalHandling {
signProvider.initVerify(keyFactory.generatePublic(keySpec))
signProvider.update(hasher.map(_.unsafe(message)).getOrElse(message))
signProvider.verify(signature)
}("Cannot verify message.")
_ EitherT.cond[F](verify, (), CryptoError("Signature is not verified")) _ Either.cond(verify, (), CryptoError("Signature is not verified"))
} yield () } yield ()
}
private def curveSpec[F[_]: Monad] = val verify: Crypto.Func[(KeyPair.Public, signature.Signature, ByteVector), Unit] =
nonFatalHandling(ECNamedCurveTable.getParameterSpec(curveType).asInstanceOf[ECParameterSpec])( verifySign.local {
"Cannot get curve parameters." case (
) publicKey,
signature,
private def getKeyPairGenerator[F[_]: Monad] = message
nonFatalHandling(KeyPairGenerator.getInstance(ECDSA, BouncyCastleProvider.PROVIDER_NAME))( )
"Cannot get key pair generator." (publicKey.bytes, signature.bytes, message.toArray)
) }
private def getKeyFactory[F[_]: Monad] =
nonFatalHandling(KeyFactory.getInstance(ECDSA, BouncyCastleProvider.PROVIDER_NAME))(
"Cannot get key factory instance."
)
private def getSignatureProvider[F[_]: Monad] =
nonFatalHandling(Signature.getInstance(scheme, BouncyCastleProvider.PROVIDER_NAME))(
"Cannot get signature instance."
)
} }
object Ecdsa { object Ecdsa {
@ -187,20 +215,19 @@ object Ecdsa {
signer = kp signer = kp
Signer( Signer(
kp.publicKey, kp.publicKey,
new Crypto.Func[ByteVector, signature.Signature] { Crypto { input
override def apply[F[_]]( ecdsa_secp256k1_sha256.sign(kp -> input)
input: ByteVector
)(implicit F: Monad[F]): EitherT[F, CryptoError, signature.Signature] =
ecdsa_secp256k1_sha256.sign(kp, input)
} }
), ),
checker = pk checker = pk
new SignatureChecker { SignatureChecker(
override def check[F[_]: Monad]( Crypto {
signature: fluence.crypto.signature.Signature, case (
plain: ByteVector signature: fluence.crypto.signature.Signature,
): EitherT[F, CryptoError, Unit] = plain: ByteVector
ecdsa_secp256k1_sha256.verify(pk, signature, plain) )
} ecdsa_secp256k1_sha256.verify(pk, signature, plain)
}
)
) )
} }

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.eddsa
import java.security._
import cats.syntax.either._
import cats.instances.either._
import cats.syntax.functor._
import fluence.crypto.KeyPair.Secret
import fluence.crypto.signature.{SignAlgo, SignatureChecker, Signer}
import fluence.crypto.{KeyPair, _}
import org.bouncycastle.crypto.{AsymmetricCipherKeyPair, 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 {
/**
* Restores pair of keys from the known secret key.
* The public key will be the same each method call with the same secret key.
* sk secret key
* @return key pair
*/
val restorePairFromSecret: Crypto.Func[Secret, KeyPair] =
Crypto.tryFn[Secret, KeyPair] { sk
val secret = new Ed25519PrivateKeyParameters(sk.bytes, 0)
KeyPair.fromBytes(secret.generatePublicKey().getEncoded, sk.bytes)
}("Could not generate KeyPair from private key")
private val signMessage: Crypto.Func[(Array[Byte], Array[Byte]), Array[Byte]] =
Crypto.tryFn[(Array[Byte], Array[Byte]), Array[Byte]] {
case (
privateKey,
message
)
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")
val sign: Crypto.Func[(KeyPair, ByteVector), signature.Signature] =
signMessage
.map(bb fluence.crypto.signature.Signature(ByteVector(bb)))
.local {
case (keyPair, message)
keyPair.secretKey.bytes -> message.toArray
}
private val verifySign: Crypto.Func[(Array[Byte], Array[Byte], Array[Byte]), Unit] =
Crypto.tryFn[(Array[Byte], Array[Byte], Array[Byte]), Boolean] {
case (
publicKey,
signature,
message
)
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") andThen Crypto.cond((), "Signature is not verified")
val verify: Crypto.Func[(KeyPair.Public, signature.Signature, ByteVector), Unit] =
verifySign.local {
case (
publicKey,
signature,
message
)
(publicKey.bytes, signature.bytes, message.toArray)
}
private def getKeyPairGenerator =
Crypto.tryUnit(
new Ed25519KeyPairGenerator()
)(
"Cannot get key pair generator"
)
val generateKeyPair: Crypto.KeyPairGenerator =
Crypto[Option[Array[Byte]], KeyPair] { input
getKeyPairGenerator.flatMap { g
val random = input.map(new SecureRandom(_)).getOrElse(new SecureRandom())
val keyParameters = new KeyGenerationParameters(random, strength)
g.init(keyParameters)
Either.fromOption(Option(g.generateKeyPair()), CryptoError("Generated keypair is null"))
}.flatMap(
(p: AsymmetricCipherKeyPair)
Crypto.tryUnit {
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")
)
}
}
object Ed25519 {
/**
* Keys in tendermint are generating with a random seed of 32 bytes
*/
val ed25519 = new Ed25519(256)
val signAlgo: SignAlgo = signAlgoInit(256)
/**
*
* @param strength the size, in bits, of the keys we want to produce
*/
def ed25519Init(strength: Int) = new Ed25519(strength)
/**
*
* @param strength the size, in bits, of the keys we want to produce
*/
def signAlgoInit(strength: Int): SignAlgo = {
val algo = ed25519Init(strength)
SignAlgo(
name = "ed25519",
generateKeyPair = algo.generateKeyPair,
signer = kp
Signer(
kp.publicKey,
Crypto[ByteVector, signature.Signature] { input
algo.sign(kp -> input)
}
),
checker = pk
SignatureChecker(
Crypto {
case (signature, plain)
algo.verify(pk, signature, plain)
}
)
)
}
}

View File

@ -35,7 +35,7 @@ object JdkCryptoHasher {
* [[https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest]] * [[https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest]]
*/ */
def apply(algorithm: String): Crypto.Hasher[Array[Byte], Array[Byte]] = def apply(algorithm: String): Crypto.Hasher[Array[Byte], Array[Byte]] =
Crypto.liftFuncEither( Crypto(
bytes bytes
Try(MessageDigest.getInstance(algorithm).digest(bytes)).toEither.left Try(MessageDigest.getInstance(algorithm).digest(bytes)).toEither.left
.map(err CryptoError(s"Cannot get $algorithm hash", Some(err))) .map(err CryptoError(s"Cannot get $algorithm hash", Some(err)))

View File

@ -0,0 +1,45 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto
import fluence.crypto.ecdsa.Ecdsa
import org.scalatest.{Matchers, WordSpec}
import scodec.bits.ByteVector
import scala.util.Random
class JvmEcdsaSpec extends WordSpec with Matchers {
def rndBytes(size: Int): Array[Byte] = Random.nextString(10).getBytes
def rndByteVector(size: Int) = ByteVector(rndBytes(size))
"jvm ecdsa algorithm" should {
"restore key pair from secret key" in {
val algo = Ecdsa.signAlgo
val testKeys = algo.generateKeyPair(None).right.get
val ecdsa = Ecdsa.ecdsa_secp256k1_sha256
val newKeys = ecdsa.restorePairFromSecret(testKeys.secretKey).right.get
testKeys shouldBe newKeys
}
}
}

View File

@ -1,141 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto
import java.io.File
import java.math.BigInteger
import cats.data.EitherT
import cats.instances.try_._
import fluence.crypto.ecdsa.Ecdsa
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 SignatureSpec 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)
}
"ecdsa algorithm" should {
"correct sign and verify data" in {
val algorithm = Ecdsa.ecdsa_secp256k1_sha256
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 = Ecdsa.signAlgo
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 = Ecdsa.signAlgo
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 = Ecdsa.signAlgo
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 = Ecdsa.signAlgo
val testKeys = algo.generateKeyPair.unsafe(None)
val ecdsa = Ecdsa.ecdsa_secp256k1_sha256
val newKeys = ecdsa.restorePairFromSecret(testKeys.secretKey).extract
testKeys shouldBe newKeys
}
}
}

View File

@ -0,0 +1,88 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto
import fluence.crypto.ecdsa.Ecdsa
import fluence.crypto.signature.{SignAlgo, Signature}
import org.scalatest.{Matchers, WordSpec}
import scodec.bits.ByteVector
import scala.util.{Random, Try}
class EcdsaSpec extends WordSpec with Matchers {
def rndBytes(size: Int): Array[Byte] = Random.nextString(10).getBytes
def rndByteVector(size: Int) = ByteVector(rndBytes(size))
"ecdsa algorithm" should {
"correct sign and verify data" in {
val algorithm: SignAlgo = Ecdsa.signAlgo
val keys = algorithm.generateKeyPair(None).right.get
val pubKey = keys.publicKey
val data = rndByteVector(10)
val sign = algorithm.signer(keys).sign(data).right.get
algorithm.checker(pubKey).check((sign, data)).isRight shouldBe true
val randomData = rndByteVector(10)
val randomSign = algorithm.signer(keys).sign(randomData).right.get
algorithm.checker(pubKey).check(randomSign -> data).isRight shouldBe false
algorithm.checker(pubKey).check(sign -> randomData).isRight shouldBe false
}
"correctly work with signer and checker" in {
val algo: SignAlgo = Ecdsa.signAlgo
val keys = algo.generateKeyPair(None).right.get
val signer = algo.signer(keys)
val checker = algo.checker(keys.publicKey)
val data = rndByteVector(10)
val sign = signer.sign(data).right.get
checker.check(sign -> data).isRight shouldBe true
val randomSign = signer.sign(rndByteVector(10)).right.get
checker.check(randomSign -> data).isRight shouldBe false
}
"throw an errors on invalid data" in {
val algo: SignAlgo = Ecdsa.signAlgo
val keys = algo.generateKeyPair(None).right.get
val signer = algo.signer(keys)
val checker = algo.checker(keys.publicKey)
val data = rndByteVector(10)
val sign = signer.sign(data).right.get
the[CryptoError] thrownBy {
checker.check(Signature(rndByteVector(10)) -> data).toTry.get
}
val invalidChecker = algo.checker(KeyPair.fromByteVectors(rndByteVector(10), rndByteVector(10)).publicKey)
the[CryptoError] thrownBy {
invalidChecker
.check(sign -> data)
.toTry
.get
}
}
}
}

View File

@ -0,0 +1,88 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto
import fluence.crypto.eddsa.Ed25519
import fluence.crypto.signature.{SignAlgo, 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))
"ed25519 algorithm" should {
"correct sign and verify data" in {
val algorithm: SignAlgo = Ed25519.signAlgo
val keys = algorithm.generateKeyPair(None).right.get
val pubKey = keys.publicKey
val data = rndByteVector(10)
val sign = algorithm.signer(keys).sign(data).right.get
algorithm.checker(pubKey).check(sign -> data).isRight shouldBe true
val randomData = rndByteVector(10)
val randomSign = algorithm.signer(keys).sign(randomData).right.get
algorithm.checker(pubKey).check(randomSign -> data).contains(true) shouldBe false
algorithm.checker(pubKey).check(sign -> randomData).contains(true) shouldBe false
}
"correctly work with signer and checker" in {
val algo: SignAlgo = Ed25519.signAlgo
val keys = algo.generateKeyPair(None).right.get
val signer = algo.signer(keys)
val checker = algo.checker(keys.publicKey)
val data = rndByteVector(10)
val sign = signer.sign(data).right.get
checker.check(sign -> data).isRight shouldBe true
val randomSign = signer.sign(rndByteVector(10)).right.get
checker.check(randomSign, data).contains(true) shouldBe false
}
"throw an errors on invalid data" in {
val algo: SignAlgo = Ed25519.signAlgo
val keys = algo.generateKeyPair(None).right.get
val signer = algo.signer(keys)
val checker = algo.checker(keys.publicKey)
val data = rndByteVector(10)
val sign = signer.sign(data).right.get
the[CryptoError] thrownBy {
checker.check(Signature(rndByteVector(10)), data).toTry.get
}
val invalidChecker = algo.checker(KeyPair.fromByteVectors(rndByteVector(10), rndByteVector(10)).publicKey)
the[CryptoError] thrownBy {
invalidChecker
.check(sign, data)
.toTry
.get
}
}
}
}

View File

@ -17,27 +17,27 @@
package fluence.crypto package fluence.crypto
import fluence.crypto.hash.JdkCryptoHasher import fluence.crypto.hash.CryptoHashers
import org.scalatest.{Matchers, WordSpec} import org.scalatest.{Matchers, WordSpec}
import scodec.bits.ByteVector import scodec.bits.ByteVector
class JvmHashSpec extends WordSpec with Matchers { class HashSpec extends WordSpec with Matchers {
"jvm hasher" should { "jvm hasher" should {
//test values get from third-party hash services //test values get from third-party hash services
"work with sha256" in { "work with sha256" in {
val str = "sha256Tester" val str = "sha256Tester"
val sha256TesterHex = "513c17f8cf6ba96ce412cc2ae82f68821e9a2c6ae7a2fb1f5e46d08c387c8e65" val sha256TesterHex = "513c17f8cf6ba96ce412cc2ae82f68821e9a2c6ae7a2fb1f5e46d08c387c8e65"
val hasher = JdkCryptoHasher.Sha256 val hasher = CryptoHashers.Sha256
ByteVector(hasher.unsafe(str.getBytes())).toHex shouldBe sha256TesterHex ByteVector(hasher(str.getBytes()).right.get).toHex shouldBe sha256TesterHex
} }
"work with sha1" in { "work with sha1" in {
val str = "sha1Tester" val str = "sha1Tester"
val sha1TesterHex = "879db20eabcecea7d4736a8bae5bc64564b76b2f" val sha1TesterHex = "879db20eabcecea7d4736a8bae5bc64564b76b2f"
val hasher = JdkCryptoHasher.Sha1 val hasher = CryptoHashers.Sha1
ByteVector(hasher.unsafe(str.getBytes())).toHex shouldBe sha1TesterHex ByteVector(hasher(str.getBytes()).right.get).toHex shouldBe sha1TesterHex
} }
"check unsigned array with sha1" in { "check unsigned array with sha1" in {
@ -47,9 +47,9 @@ class JvmHashSpec extends WordSpec with Matchers {
val base64Check = "9keNwsj08vKTlwIpHAEYvsfpdP4=" val base64Check = "9keNwsj08vKTlwIpHAEYvsfpdP4="
val hasher = JdkCryptoHasher.Sha1 val hasher = CryptoHashers.Sha1
ByteVector(hasher.unsafe(arr)).toBase64 shouldBe base64Check ByteVector(hasher(arr).right.get).toBase64 shouldBe base64Check
} }
} }
} }

View File

@ -1,117 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.jwt
import cats.syntax.compose._
import cats.syntax.flatMap._
import cats.syntax.functor._
import fluence.codec.bits.BitsCodecs
import fluence.codec.bits.BitsCodecs._
import fluence.codec.{CodecError, PureCodec}
import fluence.codec.circe.CirceCodecs
import fluence.crypto.{Crypto, CryptoError, KeyPair}
import fluence.crypto.signature.SignAlgo.CheckerFn
import fluence.crypto.signature.{PubKeyAndSignature, SignAlgo, Signature, Signer}
import io.circe.{Decoder, Encoder}
import scodec.bits.{Bases, ByteVector}
/**
* Primitivized version of JWT.
*
* @param readPubKey Gets public key from decoded Header and Claim
* @tparam H Header type
* @tparam C Claim type
*/
class CryptoJwt[H: Encoder: Decoder, C: Encoder: Decoder](
readPubKey: PureCodec.Func[(H, C), KeyPair.Public]
) {
// Public Key reader, with errors lifted to Crypto
private val readPK = Crypto.fromOtherFunc(readPubKey)(Crypto.liftCodecErrorToCrypto)
private val headerAndClaimCodec = Crypto.codec(CryptoJwt.headerClaimCodec[H, C])
private val signatureCodec = Crypto.codec(CryptoJwt.signatureCodec)
private val stringTripleCodec = Crypto.codec(CryptoJwt.stringTripleCodec)
// Take a JWT string, parse and deserialize it, check signature, return Header and Claim on success
def reader(checkerFn: CheckerFn): Crypto.Func[String, (H, C)] =
Crypto.liftFuncPoint[String, (H, C)](
jwtToken
for {
triple stringTripleCodec.inverse.pointAt(jwtToken)
(hc @ (h, c), s) = triple
headerAndClaim headerAndClaimCodec.inverse.pointAt(hc)
pk readPK.pointAt(headerAndClaim)
signature signatureCodec.inverse.pointAt(s)
plainData = ByteVector((h + c).getBytes())
_ SignAlgo.checkerFunc(checkerFn).pointAt(PubKeyAndSignature(pk, signature) plainData)
} yield headerAndClaim
)
// With the given Signer, serialize Header and Claim into JWT string, signing it on the way
def writer(signer: Signer): Crypto.Func[(H, C), String] =
Crypto.liftFuncPoint[(H, C), String](
headerAndClaim
for {
pk readPK.pointAt(headerAndClaim)
_ Crypto
.liftFuncEither[Boolean, Unit](
Either.cond(_, (), CryptoError("JWT encoded PublicKey doesn't match with signer's PublicKey"))
)
.pointAt(pk == signer.publicKey)
hc headerAndClaimCodec.direct.pointAt(headerAndClaim)
plainData = ByteVector((hc._1 + hc._2).getBytes())
signature signer.sign.pointAt(plainData)
s signatureCodec.direct.pointAt(signature)
jwtToken stringTripleCodec.direct.pointAt((hc, s))
} yield jwtToken
)
}
object CryptoJwt {
private val alphabet = Bases.Alphabets.Base64Url
private val strVec = BitsCodecs.base64AlphabetToVector(alphabet).swap
val signatureCodec: PureCodec[Signature, String] =
PureCodec.liftB[Signature, ByteVector](_.sign, Signature(_)) andThen strVec
private def jsonCodec[T: Encoder: Decoder]: PureCodec[T, String] =
CirceCodecs.circeJsonCodec[T] andThen
CirceCodecs.circeJsonParseCodec andThen
PureCodec.liftB[String, Array[Byte]](_.getBytes(), new String(_)) andThen
PureCodec[Array[Byte], ByteVector] andThen
strVec
val stringTripleCodec: PureCodec[((String, String), String), String] =
PureCodec.liftEitherB(
{
case ((a, b), c) Right(s"$a.$b.$c")
},
s
s.split('.').toList match {
case a :: b :: c :: Nil Right(((a, b), c))
case l Left(CodecError("Wrong number of dot-divided parts, expected: 3, actual: " + l.size))
}
)
def headerClaimCodec[H: Encoder: Decoder, C: Encoder: Decoder]: PureCodec[(H, C), (String, String)] =
jsonCodec[H] split jsonCodec[C]
}

View File

@ -1,82 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.jwt
import cats.{Id, Monad}
import cats.data.EitherT
import fluence.codec.PureCodec
import fluence.crypto.signature.{SignAlgo, Signature, SignatureChecker, Signer}
import fluence.crypto.{Crypto, CryptoError, KeyPair}
import io.circe.{Json, JsonNumber, JsonObject}
import org.scalatest.{Matchers, WordSpec}
import scodec.bits.ByteVector
import scala.language.higherKinds
class CryptoJwtSpec extends WordSpec with Matchers {
"CryptoJwt" should {
val keys: Seq[KeyPair] =
Stream.from(0).map(ByteVector.fromInt(_)).map(i KeyPair.fromByteVectors(i, i))
val cryptoJwt =
new CryptoJwt[JsonNumber, JsonObject](
PureCodec.liftFunc(n KeyPair.Public(ByteVector.fromInt(n._1.toInt.get)))
)
implicit val checker: SignAlgo.CheckerFn =
publicKey
new SignatureChecker {
override def check[F[_]: Monad](signature: Signature, plain: ByteVector): EitherT[F, CryptoError, Unit] =
EitherT.cond[F](signature.sign == plain.reverse, (), CryptoError("Signatures mismatch"))
}
val signer: SignAlgo.SignerFn =
keyPair Signer(keyPair.publicKey, Crypto.liftFunc(plain Signature(plain.reverse)))
"be a total bijection for valid JWT" in {
val kp = keys.head
val str = cryptoJwt
.writer(signer(kp))
.unsafe((JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" Json.fromString("value. of test"))))
str.count(_ == '.') shouldBe 2
cryptoJwt.reader(checker).unsafe(str)._2.kleisli.run("test").get shouldBe Json.fromString("value. of test")
}
"fail with wrong signer" in {
val kp = keys.tail.head
val str = cryptoJwt
.writer(signer(kp))
.runEither[Id](
(JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" Json.fromString("value of test")))
)
str.isLeft shouldBe true
}
"fail with wrong signature" in {
val kp = keys.head
val str = cryptoJwt
.writer(signer(kp))
.unsafe((JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" Json.fromString("value of test"))))
cryptoJwt.reader(checker).runEither[Id](str + "m").isLeft shouldBe true
}
}
}

View File

@ -1,83 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.keystore
import java.io.File
import java.nio.file.Files
import cats.syntax.applicativeError._
import cats.effect.IO
import fluence.codec.PureCodec
import fluence.crypto.{signature, KeyPair}
import scala.language.higherKinds
import scala.util.control.NonFatal
/**
* File based storage for crypto keys.
*
* @param file Path to keys in file system
*/
class FileKeyStorage(file: File) extends slogging.LazyLogging {
import KeyStore._
private val codec = PureCodec[KeyPair, String]
private val readFile: IO[String] =
IO(Files.readAllBytes(file.toPath)).map(new String(_))
val readKeyPair: IO[KeyPair] = readFile.flatMap(codec.inverse.runF[IO])
private def writeFile(data: String): IO[Unit] = IO {
logger.info("Storing secret key to file: " + file)
if (!file.getParentFile.exists()) {
logger.info(s"Parent directory does not exist: ${file.getParentFile}, trying to create")
Files.createDirectories(file.getParentFile.toPath)
}
if (!file.exists()) file.createNewFile() else throw new RuntimeException(file.getAbsolutePath + " already exists")
Files.write(file.toPath, data.getBytes)
}
def storeKeyPair(keyPair: KeyPair): IO[Unit] =
codec.direct.runF[IO](keyPair).flatMap(writeFile)
def readOrCreateKeyPair(createKey: IO[KeyPair]): IO[KeyPair] =
readKeyPair.recoverWith {
case NonFatal(e)
logger.debug(s"KeyPair can't be loaded from $file, going to generate new keys", e)
for {
ks createKey
_ storeKeyPair(ks)
} yield ks
}
}
object FileKeyStorage {
/**
* Generates or loads keypair
*
* @param keyPath Path to store keys in
* @param algo Sign algo
* @return Keypair, either loaded or freshly generated
*/
def getKeyPair(keyPath: String, algo: signature.SignAlgo): IO[KeyPair] =
IO(new FileKeyStorage(new File(keyPath)))
.flatMap(_.readOrCreateKeyPair(algo.generateKeyPair.runF[IO](None)))
}

View File

@ -1,84 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.keystore
import cats.syntax.compose._
import fluence.codec.PureCodec
import fluence.codec.bits.BitsCodecs
import fluence.codec.circe.CirceCodecs
import fluence.crypto.KeyPair
import io.circe.{HCursor, Json}
import scodec.bits.{Bases, ByteVector}
import scala.language.higherKinds
/**
* Json example:
* {
* "keystore" : {
* "secret" : "SFcDtZClfcxx75w9xJpQgBm09d6h9tVmVUEgHYxlews=",
* "public" : "AlTBivFrIYe++9Me4gr4R11BtRzjZ2WXZGDNWD/bEPka"
* }
* }
*/
object KeyStore {
private val alphabet = Bases.Alphabets.Base64Url
private object Field {
val Keystore = "keystore"
val Secret = "secret"
val Public = "public"
}
// Codec for a tuple of already serialized public and secret keys to json
private val pubSecJsonCodec: PureCodec[(String, String), Json] =
CirceCodecs.circeJsonCodec(
{
case (pub, sec)
Json.obj(
(
Field.Keystore,
Json.obj(
(Field.Secret, Json.fromString(sec)),
(Field.Public, Json.fromString(pub))
)
)
)
},
(c: HCursor)
for {
sec c.downField(Field.Keystore).downField(Field.Secret).as[String]
pub c.downField(Field.Keystore).downField(Field.Public).as[String]
} yield (pub, sec)
)
// ByteVector to/from String, with the chosen alphabet
private val vecToStr = BitsCodecs.base64AlphabetToVector(alphabet).swap
implicit val keyPairJsonCodec: PureCodec[KeyPair, Json] =
PureCodec.liftB[KeyPair, (ByteVector, ByteVector)](
kp (kp.publicKey.value, kp.secretKey.value), {
case (pub, sec) KeyPair(KeyPair.Public(pub), KeyPair.Secret(sec))
}
) andThen (vecToStr split vecToStr) andThen pubSecJsonCodec
implicit val keyPairJsonStringCodec: PureCodec[KeyPair, String] =
keyPairJsonCodec andThen CirceCodecs.circeJsonParseCodec
}

View File

@ -1,49 +0,0 @@
/*
* 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 <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.keystore
import fluence.crypto.KeyPair
import org.scalatest.{Matchers, WordSpec}
class KeyStoreSpec extends WordSpec with Matchers {
private val keyPair = KeyPair.fromBytes("pubKey".getBytes, "secKey".getBytes)
private val jsonString = """{"keystore":{"secret":"c2VjS2V5","public":"cHViS2V5"}}"""
"KeyStore.encodeKeyStorage" should {
"transform KeyStore to json" in {
val result = KeyStore.keyPairJsonStringCodec.direct.unsafe(keyPair)
result shouldBe jsonString
}
}
"KeyStore.decodeKeyStorage" should {
"transform KeyStore to json" in {
val result = KeyStore.keyPairJsonStringCodec.inverse.unsafe(jsonString)
result shouldBe keyPair
}
}
"KeyStore" should {
"transform KeyStore to json and back" in {
val result = KeyStore.keyPairJsonCodec.inverse.unsafe(KeyStore.keyPairJsonCodec.direct.unsafe(keyPair))
result shouldBe keyPair
}
}
}

View File

@ -26,4 +26,3 @@ object FluenceCrossType extends sbtcrossproject.CrossType {
override def sharedSrcDir(projectBase: File, conf: String) = override def sharedSrcDir(projectBase: File, conf: String) =
Some(shared(projectBase, conf)) Some(shared(projectBase, conf))
} }

View File

@ -1 +1 @@
sbt.version = 1.2.1 sbt.version = 1.2.8

View File

@ -1,15 +1,15 @@
addSbtPlugin("com.geirsson" % "sbt-scalafmt" % "1.4.0") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.1")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.2") addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.20")
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "4.1.0") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.2.0")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.22") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "0.3.1") addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "0.6.0")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.3.1") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0")
addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.10.0") addSbtPlugin("ch.epfl.scala" % "sbt-scalajs-bundler" % "0.14.0")
addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.0") addSbtPlugin("net.virtual-void" % "sbt-dependency-graph" % "0.9.2")
addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4") addSbtPlugin("org.foundweekends" % "sbt-bintray" % "0.5.4")