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
maxColumn = 120
rewriteTokens {
"=>": "⇒"
"<-": "←"
}
align = none
align {
openParenCallSite = false
openParenDefnSite = false
tokens = [
"%", "%%", "%%%", ":=", "~="
]
}
align = some
align.tokens = [{code = "=>", owner = "Case"}, ":=", "%", "%%", "%%%"]
assumeStandardLibraryStripMargin = true
includeCurlyBraceInSelectChains = false
@ -62,3 +53,4 @@ rewrite {
SortImports
]
}

View File

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

View File

@ -1,5 +1,4 @@
[![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
@ -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
resolvers += Resolver.bintrayRepo("fluencelabs", "releases")
val cryptoV = "0.0.1"
val cryptoV = "0.0.3"
libraryDependencies ++= Seq(
"one.fluence" %%% "crypto-core" % cryptoV, // basic types and APIs

View File

@ -1,6 +1,6 @@
import de.heikoseeberger.sbtheader.License
import org.scalajs.sbtplugin.ScalaJSPlugin.autoImport._
import sbtcrossproject.crossProject
import sbtcrossproject.CrossPlugin.autoImport.crossProject
name := "crypto"
@ -10,11 +10,11 @@ javaOptions in Test ++= Seq("-ea")
skip in publish := true // Skip root project
val scalaV = scalaVersion := "2.12.5"
val scalaV = scalaVersion := "2.12.9"
val commons = Seq(
scalaV,
version := "0.0.2",
version := "0.1.0",
fork in Test := true,
parallelExecution in Test := false,
organization := "one.fluence",
@ -25,21 +25,21 @@ val commons = Seq(
headerLicense := Some(License.AGPLv3("2017", organizationName.value)),
bintrayOrganization := Some("fluencelabs"),
publishMavenStyle := true,
scalafmtOnCompile := true,
bintrayRepository := "releases",
resolvers += Resolver.bintrayRepo("fluencelabs", "releases")
resolvers ++= Seq(Resolver.bintrayRepo("fluencelabs", "releases"), Resolver.sonatypeRepo("releases"))
)
commons
val CodecV = "0.0.3"
val CatsEffectV = "1.0.0-RC3"
val CatsV = "2.0.0"
val CirceV = "0.12.1"
val SloggingV = "0.6.1"
val ScalatestV = "3.0.+"
val bouncyCastle = "org.bouncycastle" % "bcprov-jdk15on" % "1.59"
val bouncyCastle = "org.bouncycastle" % "bcprov-jdk15on" % "1.61"
enablePlugins(AutomateHeaderPlugin)
@ -50,7 +50,8 @@ lazy val `crypto-core` = crossProject(JVMPlatform, JSPlatform)
.settings(
commons,
libraryDependencies ++= Seq(
"one.fluence" %%% "codec-bits" % CodecV,
"org.scodec" %%% "scodec-core" % "1.11.3",
"org.typelevel" %%% "cats-core" % CatsV,
"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-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)
.withoutSuffixFor(JVMPlatform)
.crossType(FluenceCrossType)
@ -93,7 +70,6 @@ lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
.settings(
commons,
libraryDependencies ++= Seq(
"biz.enef" %%% "slogging" % SloggingV,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
)
)
@ -104,8 +80,11 @@ lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
)
)
.jsSettings(
libraryDependencies += "io.scalajs" %%% "nodejs" % "0.4.2",
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,
//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
)
.enablePlugins(AutomateHeaderPlugin)
.dependsOn(`crypto-core`, `crypto-keystore` % Test)
.dependsOn(`crypto-core`)
lazy val `crypto-hashsign-js` = `crypto-hashsign`.js
.enablePlugins(ScalaJSBundlerPlugin)
@ -126,6 +105,7 @@ lazy val `crypto-cipher` = crossProject(JVMPlatform, JSPlatform)
.settings(
commons,
libraryDependencies ++= Seq(
"biz.enef" %%% "slogging" % SloggingV % 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
.enablePlugins(ScalaJSBundlerPlugin)
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
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.{Crypto, CryptoError}
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 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.
* @param data Data to encrypt
* @param key Salted and hashed password
* data Data to encrypt
* key Salted and hashed password
* @return Encrypted data with IV
*/
private def encryptData[F[_]: Monad](data: Array[Byte], key: Key): EitherT[F, CryptoError, Array[Byte]] = {
nonFatalHandling {
//transform data to JS type
val wordArray = CryptoJS.lib.WordArray.create(new Int8Array(data.toJSArray))
val iv = if (withIV) Some(generateIV) else None
val cryptOptions = CryptOptions(iv = iv, padding = pad, mode = mode)
//encryption return base64 string, transform it to byte array
val crypted = ByteVector.fromValidBase64(aes.encrypt(wordArray, key, cryptOptions).toString)
//IV also needs to be transformed in byte array
val byteIv = iv.map(i ByteVector.fromValidHex(i.toString))
byteIv.map(_.toArray ++ crypted.toArray).getOrElse(crypted.toArray)
}("Cannot encrypt data.")
}
private val encryptData =
Crypto.tryFn[(Array[Byte], Key), Array[Byte]] {
case (data: Array[Byte], key: Key)
//transform data to JS type
val wordArray = CryptoJS.lib.WordArray.create(new Int8Array(data.toJSArray))
val iv = if (withIV) Some(generateIV) else None
val cryptOptions = CryptOptions(iv = iv, padding = pad, mode = mode)
//encryption return base64 string, transform it to byte array
val crypted = ByteVector.fromValidBase64(aes.encrypt(wordArray, key, cryptOptions).toString)
//IV also needs to be transformed in byte array
val byteIv = iv.map(i ByteVector.fromValidHex(i.toString))
byteIv.map(_.toArray ++ crypted.toArray).getOrElse(crypted.toArray)
}("Cannot encrypt data")
private def decryptData[F[_]: Monad](key: Key, base64Data: String, iv: Option[String]) = {
nonFatalHandling {
//parse IV to WordArray JS format
val cryptOptions = CryptOptions(iv = iv.map(i CryptoJS.enc.Hex.parse(i)), padding = pad, mode = mode)
val dec = aes.decrypt(base64Data, key, cryptOptions)
ByteVector.fromValidHex(dec.toString)
}("Cannot decrypt data.")
}
private val decryptData: Crypto.Func[(Key, String, Option[String]), ByteVector] =
Crypto.tryFn[(Key, String, Option[String]), ByteVector] {
case (key: Key, base64Data: String, iv: Option[String])
//parse IV to WordArray JS format
val cryptOptions = CryptOptions(iv = iv.map(i CryptoJS.enc.Hex.parse(i)), padding = pad, mode = mode)
val dec = aes.decrypt(base64Data, key, cryptOptions)
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
*/
private def detachData[F[_]: Monad](cipherText: Array[Byte]): EitherT[F, CryptoError, (Option[String], String)] = {
nonFatalHandling {
private val detachData: Crypto.Func[Array[Byte], (Option[String], String)] =
Crypto.tryFn { cipherText: Array[Byte]
val dataWithParams = if (withIV) {
val ivDec = ByteVector(cipherText.slice(0, IV_SIZE)).toHex
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 base64 = ByteVector(data).toBase64
(ivOp, base64)
}("Cannot detach data and IV.")
}
}("Cannot detach data and IV")
/**
* Hash password with salt `iterationCount` times
*/
private def initSecretKey[F[_]: Monad](): EitherT[F, CryptoError, Key] = {
nonFatalHandling {
private val initSecretKey: Crypto.Func[Unit, Key] =
Crypto.tryFn { _: Unit
// get raw key from password and salt
val keyOption = KeyOptions(BITS, iterations = iterationCount, hasher = CryptoJS.algo.SHA256)
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]] = {
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
import cats.instances.try_._
import fluence.crypto.aes.{AesConfig, AesCrypt}
import org.scalactic.source.Position
import org.scalatest.{Assertion, Matchers, WordSpec}
@ -35,51 +34,52 @@ class AesJSSpec extends WordSpec with Matchers with slogging.LazyLogging {
"aes crypto" should {
"work with IV" in {
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 crypted = crypt.direct.unsafe(str)
crypt.inverse.unsafe(crypted) shouldBe str
val str = rndString(200).getBytes()
val crypted = crypt.encrypt(str).right.get
crypt.decrypt(crypted).right.get shouldBe str
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = true, config = conf)
checkCryptoError(fakeAes.inverse.runF[Try](crypted), str)
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = true, config = conf)
checkCryptoError(fakeAes.decrypt(crypted), str)
//we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithoutIV = AesCrypt.forString(pass, withIV = false, config = conf)
aesWithoutIV.inverse.unsafe(crypted) shouldNot be(str)
val aesWithoutIV = AesCrypt.build(pass, withIV = false, config = conf)
aesWithoutIV.decrypt(crypted).right.get shouldNot be(str)
val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted), str)
val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.decrypt(crypted), str)
}
"work without IV" in {
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 crypted = crypt.direct.unsafe(str)
crypt.inverse.unsafe(crypted) shouldBe str
val str = rndString(200).getBytes()
val crypted = crypt.encrypt(str).right.get
crypt.decrypt(crypted).right.get shouldBe str
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = false, config = conf)
checkCryptoError(fakeAes.inverse.runF[Try](crypted), str)
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = false, config = conf)
checkCryptoError(fakeAes.decrypt(crypted), str)
//we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithIV = AesCrypt.forString(pass, withIV = true, config = conf)
aesWithIV.inverse.unsafe(crypted) shouldNot be(str)
val aesWithIV = AesCrypt.build(pass, withIV = true, config = conf)
aesWithIV.decrypt(crypted).right.get shouldNot be(str)
val aesWrongSalt = AesCrypt.forString(pass, withIV = false, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted), str)
val aesWrongSalt = AesCrypt.build(pass, withIV = false, config = conf.copy(salt = rndString(10)))
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
r != msg
}.recover {
case e: CryptoError true
case e
logger.error("Unexpected error", e)
false
}.get shouldBe true
!(r sameElements msg)
}.fold(
_ true,
res => res
) shouldBe true
}
}
}

View File

@ -17,11 +17,8 @@
package fluence.crypto.aes
import cats.Monad
import cats.data.EitherT
import cats.syntax.compose._
import fluence.codec.PureCodec
import fluence.crypto.{Crypto, CryptoError, JavaAlgorithm}
import cats.instances.either._
import fluence.crypto.{Crypto, JavaAlgorithm}
import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.engines.AESEngine
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator
@ -50,7 +47,6 @@ case class DataWithParams(data: Array[Byte], params: CipherParameters)
* message
*/
class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extends JavaAlgorithm {
import CryptoError.nonFatalHandling
private val rnd = Random
private val salt = config.salt.getBytes()
@ -66,175 +62,150 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extend
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
*/
private def initSecretKey[F[_]: Monad](
password: Array[Char],
salt: Array[Byte]
): EitherT[F, CryptoError, Array[Byte]] =
nonFatalHandling {
PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password)
}("Cannot init secret key.")
private val initSecretKey: Crypto.Func[( /*password*/ Array[Char], /*salt*/ Array[Byte]), Array[Byte]] =
Crypto.tryFn[(Array[Char], Array[Byte]), Array[Byte]] {
case (password, salt)
PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password)
}("Cannot init secret key")
/**
* Setup AES CBC cipher
* @param encrypt True for encryption and false for decryption
* encrypt: True for encryption and false for decryption
*
* @return cipher
*/
private def setupAesCipher[F[_]: Monad](
params: CipherParameters,
encrypt: Boolean
): EitherT[F, CryptoError, PaddedBufferedBlockCipher] = {
nonFatalHandling {
// setup AES cipher in CBC mode with PKCS7 padding
val padding = new PKCS7Padding
val cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine), padding)
cipher.reset()
cipher.init(encrypt, params)
private val setupAesCipher: Crypto.Func[(CipherParameters, Boolean), PaddedBufferedBlockCipher] =
Crypto.tryFn[(CipherParameters, Boolean), PaddedBufferedBlockCipher] {
case (params, encrypt)
// setup AES cipher in CBC mode with PKCS7 padding
val padding = new PKCS7Padding
val cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESEngine), padding)
cipher.reset()
cipher.init(encrypt, params)
cipher
}("Cannot setup aes cipher.")
}
cipher
}("Cannot setup aes cipher")
private def cipherBytes[F[_]: Monad](
data: Array[Byte],
cipher: PaddedBufferedBlockCipher
): EitherT[F, CryptoError, Array[Byte]] = {
nonFatalHandling {
// create a temporary buffer to decode into (it'll include padding)
val buf = new Array[Byte](cipher.getOutputSize(data.length))
val outputLength = cipher.processBytes(data, 0, data.length, buf, 0)
val lastBlockLength = cipher.doFinal(buf, outputLength)
//remove padding
buf.slice(0, outputLength + lastBlockLength)
}("Error in cipher processing.")
}
private val cipherBytes: Crypto.Func[(Array[Byte], PaddedBufferedBlockCipher), Array[Byte]] =
Crypto.tryFn[(Array[Byte], PaddedBufferedBlockCipher), Array[Byte]] {
case (data, cipher)
// create a temporary buffer to decode into (it'll include padding)
val buf = new Array[Byte](cipher.getOutputSize(data.length))
val outputLength = cipher.processBytes(data, 0, data.length, buf, 0)
val lastBlockLength = cipher.doFinal(buf, outputLength)
//remove padding
buf.slice(0, outputLength + lastBlockLength)
}("Error in cipher processing")
/**
*
* @param dataWithParams Cata with cipher parameters
* @param addData Additional data (nonce)
* @param encrypt True for encryption and false for decryption
* dataWithParams Cata with cipher parameters
* addData Additional data (nonce)
* encrypt True for encryption and false for decryption
* @return Crypted bytes
*/
private def processData[F[_]: Monad](
dataWithParams: DataWithParams,
addData: Option[Array[Byte]],
encrypt: Boolean
): EitherT[F, CryptoError, Array[Byte]] = {
for {
cipher setupAesCipher(dataWithParams.params, encrypt = encrypt)
buf cipherBytes(dataWithParams.data, cipher)
encryptedData = addData.map(_ ++ buf).getOrElse(buf)
} yield encryptedData
}
private val processData: Crypto.Func[(DataWithParams, Option[Array[Byte]], Boolean), Array[Byte]] =
Crypto {
case (dataWithParams, addData, encrypt)
for {
cipher setupAesCipher(dataWithParams.params -> encrypt)
buf cipherBytes(dataWithParams.data, cipher)
} yield addData.map(_ ++ buf).getOrElse(buf)
}
/**
* encrypted data = initialization vector + data
*/
private def detachIV[F[_]: Monad](data: Array[Byte], ivSize: Int): EitherT[F, CryptoError, DetachedData] = {
nonFatalHandling {
val ivData = data.slice(0, ivSize)
val encData = data.slice(ivSize, data.length)
DetachedData(ivData, encData)
}("Cannot detach data and IV.")
}
private val detachIV: Crypto.Func[(Array[Byte], Int), DetachedData] =
Crypto.tryFn[(Array[Byte], Int), DetachedData] {
case (data, ivSize)
val ivData = data.slice(0, ivSize)
val encData = data.slice(ivSize, data.length)
DetachedData(ivData, encData)
}("Cannot detach data and IV")
private def paramsWithIV[F[_]: Monad](
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 {
private val params: Crypto.Func[Array[Byte], KeyParameter] =
Crypto.tryFn { key: Array[Byte]
val pGen = new PKCS5S2ParametersGenerator(new SHA256Digest)
pGen.init(key, salt, iterationCount)
pGen.generateDerivedParameters(BITS).asInstanceOf[KeyParameter]
}("Cannot generate key parameters")
}
private def detachDataAndGetParams[F[_]: Monad](
data: Array[Byte],
password: Array[Char],
salt: Array[Byte],
withIV: Boolean
): EitherT[F, CryptoError, DataWithParams] = {
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)
private val paramsWithIV: Crypto.Func[(Array[Byte], Array[Byte]), ParametersWithIV] =
Crypto {
case (key: Array[Byte], iv: Array[Byte])
params
.andThen(
Crypto.tryFn((kp: KeyParameter) new ParametersWithIV(kp, iv))("Cannot generate key parameters with IV")
)
.run(key)
}
/**
* Generate key parameters with IV if it is necessary
* key Password
* @return Optional IV and cipher parameters
*/
val extDataWithParams: Crypto.Func[Array[Byte], (Option[Array[Byte]], CipherParameters)] =
Crypto(
key
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]] = {
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
import cats.instances.try_._
import fluence.crypto.aes.{AesConfig, AesCrypt}
import org.scalactic.source.Position
import org.scalatest.{Assertion, Matchers, WordSpec}
@ -34,51 +33,44 @@ class AesSpec extends WordSpec with Matchers with slogging.LazyLogging {
"work with IV" in {
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 crypted = crypt.direct.unsafe(str)
crypt.inverse.unsafe(crypted) shouldBe str
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = true, config = conf)
checkCryptoError(fakeAes.inverse.runF[Try](crypted))
val str = rndString(200).getBytes
val crypted = crypt.encrypt(str).right.get
crypt.decrypt(crypted).right.get shouldBe str
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = true, config = conf)
checkCryptoError(fakeAes.decrypt(crypted))
//we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithoutIV = AesCrypt.forString(pass, withIV = false, config = conf)
aesWithoutIV.inverse.unsafe(crypted) shouldNot be(str)
val aesWithoutIV = AesCrypt.build(pass, withIV = false, config = conf)
aesWithoutIV.decrypt(crypted).right.get shouldNot be(str)
val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted))
val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.decrypt(crypted))
}
"work without IV" in {
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 crypted = crypt.direct.unsafe(str)
crypt.inverse.unsafe(crypted) shouldBe str
val str = rndString(200).getBytes()
val crypted = crypt.encrypt(str).right.get
crypt.decrypt(crypted).right.get shouldBe str
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = false, config = conf)
checkCryptoError(fakeAes.inverse.runF[Try](crypted))
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = false, config = conf)
checkCryptoError(fakeAes.decrypt(crypted))
//we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithIV = AesCrypt.forString(pass, withIV = true, config = conf)
aesWithIV.inverse.unsafe(crypted) shouldNot be(str)
val aesWithIV = AesCrypt.build(pass, withIV = true, config = conf)
aesWithIV.decrypt(crypted).right.get shouldNot be(str)
val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted))
val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.decrypt(crypted))
}
}
def checkCryptoError(tr: Try[String])(implicit pos: Position): Assertion = {
tr.map(_ false)
.recover {
case e: CryptoError true
case e
logger.error("Unexpected error", e)
false
}
.get shouldBe true
def checkCryptoError(tr: Crypto.Result[Array[Byte]])(implicit pos: Position): Assertion = {
tr.isLeft shouldBe true
}
}

View File

@ -17,18 +17,32 @@
package fluence.crypto
import fluence.codec.{CodecError, MonadicalEitherArrow, PureCodec}
import cats.data.Kleisli
object Crypto extends MonadicalEitherArrow[CryptoError] {
type Hasher[A, B] = Func[A, B]
import scala.util.Try
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
implicit val liftCodecErrorToCrypto: CodecError CryptoError = err CryptoError("Codec error", Some(err))
type Func[A, B] = Kleisli[Result, A, B]
implicit def codec[A, B](implicit codec: PureCodec[A, B]): Bijection[A, B] =
Bijection(fromOtherFunc(codec.direct), fromOtherFunc(codec.inverse))
case class Cipher[A](
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
import cats.Applicative
import cats.data.EitherT
import scala.util.control.{NoStackTrace, NonFatal}
import scala.language.higherKinds
import scala.util.control.NoStackTrace
case class CryptoError(message: String, causedBy: Option[Throwable] = None) extends NoStackTrace {
override def getMessage: String = message
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 cats.Monad
import cats.data.EitherT
import cats.data.Kleisli
import fluence.crypto.signature.{SignAlgo, Signature, SignatureChecker, Signer}
import cats.syntax.either._
import scodec.bits.ByteVector
import scala.language.higherKinds
@ -31,26 +31,34 @@ object DumbCrypto {
lazy val signAlgo: SignAlgo =
SignAlgo(
"dumb",
Crypto.liftFunc { seedOpt
Kleisli[Crypto.Result, Option[Array[Byte]], KeyPair] { seedOpt
val seed = seedOpt.getOrElse {
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
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"))
}
SignatureChecker(
Kleisli {
case (sgn, msg) Either.cond(sgn.sign == msg.reverse, (), CryptoError("Signatures mismatch"))
}
)
)
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]] =
Crypto.identityFunc[Array[Byte]]
Kleisli[Crypto.Result, Array[Byte], Array[Byte]](_.asRight)
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
import cats.Monad
import cats.data.EitherT
import fluence.crypto.{Crypto, CryptoError}
import cats.data.Kleisli
import fluence.crypto.Crypto
import cats.instances.either._
import cats.syntax.either._
import scala.collection.Searching.{Found, InsertionPoint, SearchResult}
import scala.language.higherKinds
@ -41,13 +43,10 @@ object CipherSearch {
def binarySearch[A, B](coll: IndexedSeq[A], decrypt: Crypto.Func[A, B])(
implicit ordering: Ordering[B]
): Crypto.Func[B, SearchResult] =
new Crypto.Func[B, SearchResult] {
override def apply[F[_]](input: B)(
implicit F: Monad[F]
): EitherT[F, CryptoError, SearchResult] = {
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)))
Kleisli { input
{
implicitly[Monad[Crypto.Result]].tailRecM((0, coll.length)) {
case (from, to) if from == to Right(InsertionPoint(from)).asRight
case (from, to)
val idx = from + (to - from - 1) / 2
decrypt(coll(idx)).map { d

View File

@ -17,11 +17,8 @@
package fluence.crypto.signature
import cats.Monad
import cats.data.EitherT
import cats.syntax.strong._
import cats.syntax.compose._
import fluence.crypto.{Crypto, CryptoError, KeyPair}
import cats.data.Kleisli
import fluence.crypto.{Crypto, KeyPair}
import scodec.bits.ByteVector
import scala.language.higherKinds
@ -38,7 +35,7 @@ case class SignAlgo(
name: String,
generateKeyPair: Crypto.KeyPairGenerator,
signer: SignAlgo.SignerFn,
implicit val checker: SignAlgo.CheckerFn,
implicit val checker: SignAlgo.CheckerFn
)
object SignAlgo {
@ -46,27 +43,13 @@ object SignAlgo {
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.
*/
def checkerFunc(fn: CheckerFn): Crypto.Func[(PubKeyAndSignature, ByteVector), Unit] =
Crypto
.liftFunc[PubKeyAndSignature, (SignatureChecker, Signature)] {
case PubKeyAndSignature(pk, signature) fn(pk) -> signature
}
.first[ByteVector] andThen fullChecker
Kleisli {
case (pks, msg)
fn(pks.publicKey).check.run(pks.signature -> msg)
}
}

View File

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

View File

@ -17,10 +17,10 @@
package fluence.crypto.signature
import cats.syntax.profunctor._
import cats.instances.either._
import fluence.crypto.{Crypto, KeyPair}
import scodec.bits.ByteVector
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 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.unsafe("D") shouldBe Found(3)
search.unsafe("E") shouldBe Found(4)
search("B").right.get shouldBe Found(1)
search("D").right.get shouldBe Found(3)
search("E").right.get shouldBe Found(4)
search.unsafe("0") shouldBe InsertionPoint(0)
search.unsafe("BB") shouldBe InsertionPoint(2)
search.unsafe("ZZ") shouldBe InsertionPoint(5)
search("0").right.get shouldBe InsertionPoint(0)
search("BB").right.get shouldBe InsertionPoint(2)
search("ZZ").right.get shouldBe InsertionPoint(5)
}
}

View File

@ -27,11 +27,11 @@ class NoOpCryptSpec extends WordSpec with Matchers {
val noOpCrypt = DumbCrypto.cipherString
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"
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)
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
import cats.Monad
import cats.data.EitherT
import fluence.crypto._
import fluence.crypto.facade.ecdsa.EC
import fluence.crypto.hash.JsCryptoHasher
@ -35,54 +33,62 @@ import scala.scalajs.js.typedarray.Uint8Array
* @param ec implementation of ecdsa logic for different curves
*/
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 =
new Crypto.Func[Option[Array[Byte]], KeyPair] {
override def apply[F[_]](input: Option[Array[Byte]])(implicit F: Monad[F]): EitherT[F, CryptoError, KeyPair] =
nonFatalHandling {
val seedJs = input.map(bs js.Dynamic.literal(entropy = bs.toJSArray))
val key = ec.genKeyPair(seedJs)
val publicHex = key.getPublic(true, "hex")
val secretHex = key.getPrivate("hex")
val public = ByteVector.fromValidHex(publicHex)
val secret = ByteVector.fromValidHex(secretHex)
KeyPair.fromByteVectors(public, secret)
}("Failed to generate key pair.")
Crypto.tryFn[Option[Array[Byte]], KeyPair] { input
val seedJs = input.map(bs js.Dynamic.literal(entropy = bs.toJSArray))
val key = ec.genKeyPair(seedJs)
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)
}("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] =
for {
secret nonFatalHandling {
ec.keyFromPrivate(keyPair.secretKey.value.toHex, "hex")
}("Cannot get private key from key pair.")
hash hash(message)
signHex nonFatalHandling(secret.sign(new Uint8Array(hash)).toDER("hex"))("Cannot sign message")
} yield Signature(ByteVector.fromValidHex(signHex))
val verify: Crypto.Func[(KeyPair.Public, Signature, ByteVector), Unit] =
Crypto {
case (
pubKey,
signature,
message
)
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 {
@ -94,20 +100,13 @@ object Ecdsa {
signer = kp
Signer(
kp.publicKey,
new Crypto.Func[ByteVector, signature.Signature] {
override def apply[F[_]](
input: ByteVector
)(implicit F: Monad[F]): EitherT[F, CryptoError, signature.Signature] =
ecdsa_secp256k1_sha256.sign(kp, input)
}
),
ecdsa_secp256k1_sha256.sign.local(kp -> _)
),
checker = pk
new SignatureChecker {
override def check[F[_]: Monad](
signature: fluence.crypto.signature.Signature,
plain: ByteVector
): EitherT[F, CryptoError, Unit] =
ecdsa_secp256k1_sha256.verify(pk, signature, plain)
}
SignatureChecker(
ecdsa_secp256k1_sha256.verify.local {
case (signature, plain) (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
import cats.instances.either._
import cats.syntax.either._
import fluence.crypto.{Crypto, CryptoError}
import fluence.crypto.facade.ecdsa.{SHA1, SHA256}
import scodec.bits.ByteVector
import scala.language.higherKinds
import scala.scalajs.js
import scala.scalajs.js.JSConverters._
import scala.scalajs.js.typedarray.Uint8Array
import scala.util.Try
object JsCryptoHasher {
lazy val Sha256: Crypto.Hasher[Array[Byte], Array[Byte]] =
Crypto.liftFuncEither[Array[Byte], Array[Byte]] { msg
Try {
val sha256 = new SHA256()
sha256.update(new Uint8Array(msg.toJSArray))
ByteVector.fromValidHex(sha256.digest("hex")).toArray
}.toEither.left.map(err CryptoError("Cannot calculate Sha256 hash", Some(err)))
}
Crypto.tryFn[Array[Byte], Array[Byte]] { msg
val sha256 = new SHA256()
sha256.update(new Uint8Array(msg.toJSArray))
ByteVector.fromValidHex(sha256.digest("hex")).toArray
}("Cannot calculate Sha256 hash")
lazy val Sha1: Crypto.Hasher[Array[Byte], Array[Byte]] =
Crypto.liftFuncEither[Array[Byte], Array[Byte]] { msg
Try {
val sha1 = new SHA1()
sha1.update(new Uint8Array(msg.toJSArray))
ByteVector.fromValidHex(sha1.digest("hex")).toArray
}.toEither.left.map(err CryptoError("Cannot calculate Sha256 hash", Some(err)))
Crypto.tryFn[Array[Byte], Array[Byte]] { msg
val sha1 = new SHA1()
sha1.update(new Uint8Array(msg.toJSArray))
ByteVector.fromValidHex(sha1.digest("hex")).toArray
}("Cannot calculate Sha256 hash")
/**
* 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.interfaces.ECPrivateKey
import cats.Monad
import cats.data.EitherT
import cats.instances.either._
import cats.syntax.either._
import fluence.crypto.KeyPair.Secret
import fluence.crypto.{KeyPair, _}
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]]])
extends JavaAlgorithm {
import CryptoError.nonFatalHandling
import Ecdsa._
val HEXradix = 16
val generateKeyPair: Crypto.KeyPairGenerator =
new Crypto.Func[Option[Array[Byte]], KeyPair] {
override def apply[F[_]](
input: Option[Array[Byte]]
)(implicit F: Monad[F]): EitherT[F, CryptoError, fluence.crypto.KeyPair] =
/**
* 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(
sk
for {
ecSpec EitherT.fromOption(
ecSpec Either.fromOption(
Option(ECNamedCurveTable.getParameterSpec(curveType)),
CryptoError("Parameter spec for the curve is not available.")
)
g getKeyPairGenerator
_ nonFatalHandling {
g.initialize(ecSpec, input.map(new SecureRandom(_)).getOrElse(new SecureRandom()))
}(s"Could not initialize KeyPairGenerator")
p EitherT.fromOption(Option(g.generateKeyPair()), CryptoError("Could not generate KeyPair. Unexpected."))
keyPair nonFatalHandling {
//store S number for private key and compressed Q point on curve for public key
val pk = ByteVector(p.getPublic.asInstanceOf[ECPublicKey].getQ.getEncoded(true))
val bg = p.getPrivate.asInstanceOf[ECPrivateKey].getS
val sk = ByteVector.fromValidHex(bg.toString(HEXradix))
KeyPair.fromByteVectors(pk, sk)
}("Could not generate KeyPair. Unexpected.")
keyPair Crypto.tryUnit {
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
)
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
}
/**
* Restores pair of keys from the known secret key.
* The public key will be the same each method call with the same secret key.
* @param sk secret key
* @return key pair
*/
def restorePairFromSecret[F[_]: Monad](sk: Secret): EitherT[F, CryptoError, KeyPair] =
for {
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
private def getKeyFactory =
Crypto.tryUnit(KeyFactory.getInstance(ECDSA, BouncyCastleProvider.PROVIDER_NAME))(
"Cannot get key factory instance"
)
def sign[F[_]: Monad](
keyPair: KeyPair,
message: ByteVector
): EitherT[F, CryptoError, signature.Signature] =
signMessage(new BigInteger(keyPair.secretKey.value.toHex, HEXradix), message.toArray)
private def getSignatureProvider =
Crypto.tryUnit(Signature.getInstance(scheme, BouncyCastleProvider.PROVIDER_NAME))(
"Cannot get signature instance"
)
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)))
.local {
case (keyPair, message) (new BigInteger(keyPair.secretKey.value.toHex, HEXradix), message.toArray)
}
def verify[F[_]: Monad](
publicKey: KeyPair.Public,
signature: fluence.crypto.signature.Signature,
message: ByteVector
): EitherT[F, CryptoError, Unit] =
verifySign(publicKey.bytes, signature.bytes, message.toArray)
private val verifySign: Crypto.Func[(Array[Byte], Array[Byte], Array[Byte]), Unit] =
Crypto {
case (
publicKey,
signature,
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](
privateKey: BigInteger,
message: Array[Byte]
): 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.")
hash hasher.fold(
message.asRight[CryptoError]
)(_.apply(message))
} yield sign
_ Crypto.tryUnit(signProvider.update(hash))("Cannot update message")
private def verifySign[F[_]: Monad](
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.")
verify Crypto.tryUnit(signProvider.verify(signature))("Cannot verify message")
_ EitherT.cond[F](verify, (), CryptoError("Signature is not verified"))
} yield ()
_ Either.cond(verify, (), CryptoError("Signature is not verified"))
} yield ()
}
private def curveSpec[F[_]: Monad] =
nonFatalHandling(ECNamedCurveTable.getParameterSpec(curveType).asInstanceOf[ECParameterSpec])(
"Cannot get curve parameters."
)
private def getKeyPairGenerator[F[_]: Monad] =
nonFatalHandling(KeyPairGenerator.getInstance(ECDSA, BouncyCastleProvider.PROVIDER_NAME))(
"Cannot get key pair generator."
)
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."
)
val verify: Crypto.Func[(KeyPair.Public, signature.Signature, ByteVector), Unit] =
verifySign.local {
case (
publicKey,
signature,
message
)
(publicKey.bytes, signature.bytes, message.toArray)
}
}
object Ecdsa {
@ -187,20 +215,19 @@ object Ecdsa {
signer = kp
Signer(
kp.publicKey,
new Crypto.Func[ByteVector, signature.Signature] {
override def apply[F[_]](
input: ByteVector
)(implicit F: Monad[F]): EitherT[F, CryptoError, signature.Signature] =
ecdsa_secp256k1_sha256.sign(kp, input)
Crypto { input
ecdsa_secp256k1_sha256.sign(kp -> input)
}
),
),
checker = pk
new SignatureChecker {
override def check[F[_]: Monad](
signature: fluence.crypto.signature.Signature,
plain: ByteVector
): EitherT[F, CryptoError, Unit] =
ecdsa_secp256k1_sha256.verify(pk, signature, plain)
}
SignatureChecker(
Crypto {
case (
signature: fluence.crypto.signature.Signature,
plain: ByteVector
)
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]]
*/
def apply(algorithm: String): Crypto.Hasher[Array[Byte], Array[Byte]] =
Crypto.liftFuncEither(
Crypto(
bytes
Try(MessageDigest.getInstance(algorithm).digest(bytes)).toEither.left
.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
import fluence.crypto.hash.JdkCryptoHasher
import fluence.crypto.hash.CryptoHashers
import org.scalatest.{Matchers, WordSpec}
import scodec.bits.ByteVector
class JvmHashSpec extends WordSpec with Matchers {
class HashSpec extends WordSpec with Matchers {
"jvm hasher" should {
//test values get from third-party hash services
"work with sha256" in {
val str = "sha256Tester"
val sha256TesterHex = "513c17f8cf6ba96ce412cc2ae82f68821e9a2c6ae7a2fb1f5e46d08c387c8e65"
val hasher = JdkCryptoHasher.Sha256
ByteVector(hasher.unsafe(str.getBytes())).toHex shouldBe sha256TesterHex
val hasher = CryptoHashers.Sha256
ByteVector(hasher(str.getBytes()).right.get).toHex shouldBe sha256TesterHex
}
"work with sha1" in {
val str = "sha1Tester"
val sha1TesterHex = "879db20eabcecea7d4736a8bae5bc64564b76b2f"
val hasher = JdkCryptoHasher.Sha1
ByteVector(hasher.unsafe(str.getBytes())).toHex shouldBe sha1TesterHex
val hasher = CryptoHashers.Sha1
ByteVector(hasher(str.getBytes()).right.get).toHex shouldBe sha1TesterHex
}
"check unsigned array with sha1" in {
@ -47,9 +47,9 @@ class JvmHashSpec extends WordSpec with Matchers {
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) =
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.portable-scala" % "sbt-crossproject" % "0.3.1")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.3.1")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "0.6.0")
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")