Compare commits

..

No commits in common. "master" and "0.0.4" have entirely different histories.

39 changed files with 1353 additions and 1152 deletions

View File

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

View File

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

View File

@ -10,11 +10,11 @@ javaOptions in Test ++= Seq("-ea")
skip in publish := true // Skip root project skip in publish := true // Skip root project
val scalaV = scalaVersion := "2.12.9" val scalaV = scalaVersion := "2.12.8"
val commons = Seq( val commons = Seq(
scalaV, scalaV,
version := "0.1.0", version := "0.0.4",
fork in Test := true, fork in Test := true,
parallelExecution in Test := false, parallelExecution in Test := false,
organization := "one.fluence", organization := "one.fluence",
@ -25,15 +25,15 @@ val commons = Seq(
headerLicense := Some(License.AGPLv3("2017", organizationName.value)), headerLicense := Some(License.AGPLv3("2017", organizationName.value)),
bintrayOrganization := Some("fluencelabs"), bintrayOrganization := Some("fluencelabs"),
publishMavenStyle := true, publishMavenStyle := true,
scalafmtOnCompile := true,
bintrayRepository := "releases", bintrayRepository := "releases",
resolvers ++= Seq(Resolver.bintrayRepo("fluencelabs", "releases"), Resolver.sonatypeRepo("releases")) resolvers += Resolver.bintrayRepo("fluencelabs", "releases")
) )
commons commons
val CatsV = "2.0.0" val CodecV = "0.0.5"
val CirceV = "0.12.1"
val CatsEffectV = "1.2.0"
val SloggingV = "0.6.1" val SloggingV = "0.6.1"
@ -50,8 +50,7 @@ lazy val `crypto-core` = crossProject(JVMPlatform, JSPlatform)
.settings( .settings(
commons, commons,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"org.scodec" %%% "scodec-core" % "1.11.3", "one.fluence" %%% "codec-bits" % CodecV,
"org.typelevel" %%% "cats-core" % CatsV,
"org.scalatest" %%% "scalatest" % ScalatestV % Test "org.scalatest" %%% "scalatest" % ScalatestV % Test
) )
) )
@ -63,6 +62,30 @@ lazy val `crypto-core` = crossProject(JVMPlatform, JSPlatform)
lazy val `crypto-core-js` = `crypto-core`.js lazy val `crypto-core-js` = `crypto-core`.js
lazy val `crypto-core-jvm` = `crypto-core`.jvm lazy val `crypto-core-jvm` = `crypto-core`.jvm
lazy val `crypto-keystore` = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(FluenceCrossType)
.in(file("keystore"))
.settings(
commons,
libraryDependencies ++= Seq(
"one.fluence" %%% "codec-circe" % CodecV,
"biz.enef" %%% "slogging" % SloggingV,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
)
)
.jsSettings(
fork in Test := false
)
.jvmSettings(
libraryDependencies += "org.typelevel" %% "cats-effect" % CatsEffectV,
)
.enablePlugins(AutomateHeaderPlugin)
.dependsOn(`crypto-core`)
lazy val `crypto-keystore-js` = `crypto-keystore`.js
lazy val `crypto-keystore-jvm` = `crypto-keystore`.jvm
lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform) lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform) .withoutSuffixFor(JVMPlatform)
.crossType(FluenceCrossType) .crossType(FluenceCrossType)
@ -80,11 +103,8 @@ lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
) )
) )
.jsSettings( .jsSettings(
libraryDependencies += "io.scalajs" %%% "nodejs" % "0.4.2",
npmDependencies in Compile ++= Seq( npmDependencies in Compile ++= Seq(
"elliptic" -> "6.4.1", "elliptic" -> "6.4.0"
"supercop.js" -> "2.0.1",
"hash.js" -> "1.1.7"
), ),
scalaJSModuleKind in Test := ModuleKind.CommonJSModule, scalaJSModuleKind in Test := ModuleKind.CommonJSModule,
//all JavaScript dependencies will be concatenated to a single file *-jsdeps.js //all JavaScript dependencies will be concatenated to a single file *-jsdeps.js
@ -92,7 +112,7 @@ lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
fork in Test := false fork in Test := false
) )
.enablePlugins(AutomateHeaderPlugin) .enablePlugins(AutomateHeaderPlugin)
.dependsOn(`crypto-core`) .dependsOn(`crypto-core`, `crypto-keystore` % Test)
lazy val `crypto-hashsign-js` = `crypto-hashsign`.js lazy val `crypto-hashsign-js` = `crypto-hashsign`.js
.enablePlugins(ScalaJSBundlerPlugin) .enablePlugins(ScalaJSBundlerPlugin)
@ -105,7 +125,7 @@ lazy val `crypto-cipher` = crossProject(JVMPlatform, JSPlatform)
.settings( .settings(
commons, commons,
libraryDependencies ++= Seq( libraryDependencies ++= Seq(
"biz.enef" %%% "slogging" % SloggingV % Test, "biz.enef" %%% "slogging" % SloggingV,
"org.scalatest" %%% "scalatest" % ScalatestV % Test "org.scalatest" %%% "scalatest" % ScalatestV % Test
) )
) )
@ -130,3 +150,24 @@ lazy val `crypto-cipher` = crossProject(JVMPlatform, JSPlatform)
lazy val `crypto-cipher-js` = `crypto-cipher`.js lazy val `crypto-cipher-js` = `crypto-cipher`.js
.enablePlugins(ScalaJSBundlerPlugin) .enablePlugins(ScalaJSBundlerPlugin)
lazy val `crypto-cipher-jvm` = `crypto-cipher`.jvm lazy val `crypto-cipher-jvm` = `crypto-cipher`.jvm
lazy val `crypto-jwt` = crossProject(JVMPlatform, JSPlatform)
.withoutSuffixFor(JVMPlatform)
.crossType(FluenceCrossType)
.in(file("jwt"))
.settings(
commons,
libraryDependencies ++= Seq(
"one.fluence" %%% "codec-circe" % CodecV,
"org.scalatest" %%% "scalatest" % ScalatestV % Test
)
)
.jsSettings(
fork in Test := false,
scalaJSModuleKind := ModuleKind.CommonJSModule
)
.enablePlugins(AutomateHeaderPlugin)
.dependsOn(`crypto-core`)
lazy val `crypto-jwt-js` = `crypto-jwt`.js
lazy val `crypto-jwt-jvm` = `crypto-jwt`.jvm

View File

@ -17,6 +17,11 @@
package fluence.crypto.aes package fluence.crypto.aes
import cats.Monad
import cats.data.EitherT
import cats.syntax.compose._
import fluence.codec.PureCodec
import fluence.crypto.CryptoError.nonFatalHandling
import fluence.crypto.facade.cryptojs.{CryptOptions, CryptoJS, Key, KeyOptions} import fluence.crypto.facade.cryptojs.{CryptOptions, CryptoJS, Key, KeyOptions}
import fluence.crypto.{Crypto, CryptoError} import fluence.crypto.{Crypto, CryptoError}
import scodec.bits.ByteVector import scodec.bits.ByteVector
@ -43,15 +48,35 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) {
private val mode = CryptoJS.mode.CBC private val mode = CryptoJS.mode.CBC
private val aes = CryptoJS.AES private val aes = CryptoJS.AES
val encrypt: Crypto.Func[Array[Byte], Array[Byte]] =
new Crypto.Func[Array[Byte], Array[Byte]] {
override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
for {
key initSecretKey()
encrypted encryptData(input, key)
} yield encrypted
}
val decrypt: Crypto.Func[Array[Byte], Array[Byte]] =
new Crypto.Func[Array[Byte], Array[Byte]] {
override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
for {
detachedData detachData(input)
(iv, base64) = detachedData
key initSecretKey()
decData decryptData(key, base64, iv)
_ EitherT.cond(decData.nonEmpty, decData, CryptoError("Cannot decrypt message with this password."))
} yield decData.toArray
}
/** /**
* Encrypt data. * Encrypt data.
* data Data to encrypt * @param data Data to encrypt
* key Salted and hashed password * @param key Salted and hashed password
* @return Encrypted data with IV * @return Encrypted data with IV
*/ */
private val encryptData = private def encryptData[F[_]: Monad](data: Array[Byte], key: Key): EitherT[F, CryptoError, Array[Byte]] = {
Crypto.tryFn[(Array[Byte], Key), Array[Byte]] { nonFatalHandling {
case (data: Array[Byte], key: Key)
//transform data to JS type //transform data to JS type
val wordArray = CryptoJS.lib.WordArray.create(new Int8Array(data.toJSArray)) val wordArray = CryptoJS.lib.WordArray.create(new Int8Array(data.toJSArray))
val iv = if (withIV) Some(generateIV) else None val iv = if (withIV) Some(generateIV) else None
@ -61,23 +86,24 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) {
//IV also needs to be transformed in byte array //IV also needs to be transformed in byte array
val byteIv = iv.map(i ByteVector.fromValidHex(i.toString)) val byteIv = iv.map(i ByteVector.fromValidHex(i.toString))
byteIv.map(_.toArray ++ crypted.toArray).getOrElse(crypted.toArray) byteIv.map(_.toArray ++ crypted.toArray).getOrElse(crypted.toArray)
}("Cannot encrypt data") }("Cannot encrypt data.")
}
private val decryptData: Crypto.Func[(Key, String, Option[String]), ByteVector] = private def decryptData[F[_]: Monad](key: Key, base64Data: String, iv: Option[String]) = {
Crypto.tryFn[(Key, String, Option[String]), ByteVector] { nonFatalHandling {
case (key: Key, base64Data: String, iv: Option[String])
//parse IV to WordArray JS format //parse IV to WordArray JS format
val cryptOptions = CryptOptions(iv = iv.map(i CryptoJS.enc.Hex.parse(i)), padding = pad, mode = mode) val cryptOptions = CryptOptions(iv = iv.map(i CryptoJS.enc.Hex.parse(i)), padding = pad, mode = mode)
val dec = aes.decrypt(base64Data, key, cryptOptions) val dec = aes.decrypt(base64Data, key, cryptOptions)
ByteVector.fromValidHex(dec.toString) ByteVector.fromValidHex(dec.toString)
}("Cannot decrypt data") }("Cannot decrypt data.")
}
/** /**
* cipherText Encrypted data with IV * @param cipherText Encrypted data with IV
* @return IV in hex and data in base64 * @return IV in hex and data in base64
*/ */
private val detachData: Crypto.Func[Array[Byte], (Option[String], String)] = private def detachData[F[_]: Monad](cipherText: Array[Byte]): EitherT[F, CryptoError, (Option[String], String)] = {
Crypto.tryFn { cipherText: Array[Byte] nonFatalHandling {
val dataWithParams = if (withIV) { val dataWithParams = if (withIV) {
val ivDec = ByteVector(cipherText.slice(0, IV_SIZE)).toHex val ivDec = ByteVector(cipherText.slice(0, IV_SIZE)).toHex
val encMessage = cipherText.slice(IV_SIZE, cipherText.length) val encMessage = cipherText.slice(IV_SIZE, cipherText.length)
@ -86,45 +112,36 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) {
val (ivOp, data) = dataWithParams val (ivOp, data) = dataWithParams
val base64 = ByteVector(data).toBase64 val base64 = ByteVector(data).toBase64
(ivOp, base64) (ivOp, base64)
}("Cannot detach data and IV") }("Cannot detach data and IV.")
}
/** /**
* Hash password with salt `iterationCount` times * Hash password with salt `iterationCount` times
*/ */
private val initSecretKey: Crypto.Func[Unit, Key] = private def initSecretKey[F[_]: Monad](): EitherT[F, CryptoError, Key] = {
Crypto.tryFn { _: Unit nonFatalHandling {
// get raw key from password and salt // get raw key from password and salt
val keyOption = KeyOptions(BITS, iterations = iterationCount, hasher = CryptoJS.algo.SHA256) val keyOption = KeyOptions(BITS, iterations = iterationCount, hasher = CryptoJS.algo.SHA256)
CryptoJS.PBKDF2(new String(password), salt, keyOption) CryptoJS.PBKDF2(new String(password), salt, keyOption)
}("Cannot init secret key") }("Cannot init secret key.")
val decrypt: Crypto.Func[Array[Byte], Array[Byte]] =
Crypto { input
for {
detachedData detachData(input)
(iv, base64) = detachedData
key initSecretKey(())
decData decryptData((key, base64, iv))
_ Crypto[Boolean, Unit](Either.cond(_, (), CryptoError("Cannot decrypt message with this password.")))(
decData.nonEmpty
)
} yield decData.toArray
}
val encrypt: Crypto.Func[Array[Byte], Array[Byte]] =
Crypto[Array[Byte], Array[Byte]] { input
for {
key initSecretKey(())
encrypted encryptData(input -> key)
} yield encrypted
} }
} }
object AesCrypt { object AesCrypt extends slogging.LazyLogging {
def build(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[Array[Byte]] = { def build(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[Array[Byte]] = {
val aes = new AesCrypt(password.toHex.toCharArray, withIV, config) val aes = new AesCrypt(password.toHex.toCharArray, withIV, config)
Crypto.Cipher(aes.encrypt, aes.decrypt) Crypto.Bijection(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,6 +17,7 @@
package fluence.crypto package fluence.crypto
import cats.instances.try_._
import fluence.crypto.aes.{AesConfig, AesCrypt} import fluence.crypto.aes.{AesConfig, AesCrypt}
import org.scalactic.source.Position import org.scalactic.source.Position
import org.scalatest.{Assertion, Matchers, WordSpec} import org.scalatest.{Assertion, Matchers, WordSpec}
@ -34,52 +35,51 @@ class AesJSSpec extends WordSpec with Matchers with slogging.LazyLogging {
"aes crypto" should { "aes crypto" should {
"work with IV" in { "work with IV" in {
val pass = ByteVector("pass".getBytes()) val pass = ByteVector("pass".getBytes())
val crypt = AesCrypt.build(pass, withIV = true, config = conf) val crypt = AesCrypt.forString(pass, withIV = true, config = conf)
val str = rndString(200).getBytes() val str = rndString(200)
val crypted = crypt.encrypt(str).right.get val crypted = crypt.direct.unsafe(str)
crypt.decrypt(crypted).right.get shouldBe str crypt.inverse.unsafe(crypted) shouldBe str
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = true, config = conf) val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = true, config = conf)
checkCryptoError(fakeAes.decrypt(crypted), str) checkCryptoError(fakeAes.inverse.runF[Try](crypted), str)
//we cannot check if first bytes is iv or already data, but encryption goes wrong //we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithoutIV = AesCrypt.build(pass, withIV = false, config = conf) val aesWithoutIV = AesCrypt.forString(pass, withIV = false, config = conf)
aesWithoutIV.decrypt(crypted).right.get shouldNot be(str) aesWithoutIV.inverse.unsafe(crypted) shouldNot be(str)
val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10))) val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.decrypt(crypted), str) checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted), str)
} }
"work without IV" in { "work without IV" in {
val pass = ByteVector("pass".getBytes()) val pass = ByteVector("pass".getBytes())
val crypt = AesCrypt.build(pass, withIV = false, config = conf) val crypt = AesCrypt.forString(pass, withIV = false, config = conf)
val str = rndString(200).getBytes() val str = rndString(200)
val crypted = crypt.encrypt(str).right.get val crypted = crypt.direct.unsafe(str)
crypt.decrypt(crypted).right.get shouldBe str crypt.inverse.unsafe(crypted) shouldBe str
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = false, config = conf) val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = false, config = conf)
checkCryptoError(fakeAes.decrypt(crypted), str) checkCryptoError(fakeAes.inverse.runF[Try](crypted), str)
//we cannot check if first bytes is iv or already data, but encryption goes wrong //we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithIV = AesCrypt.build(pass, withIV = true, config = conf) val aesWithIV = AesCrypt.forString(pass, withIV = true, config = conf)
aesWithIV.decrypt(crypted).right.get shouldNot be(str) aesWithIV.inverse.unsafe(crypted) shouldNot be(str)
val aesWrongSalt = AesCrypt.build(pass, withIV = false, config = conf.copy(salt = rndString(10))) val aesWrongSalt = AesCrypt.forString(pass, withIV = false, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.decrypt(crypted), str) checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted), str)
} }
/** def checkCryptoError(tr: Try[String], msg: String)(implicit pos: Position): Assertion = {
* Checks if there is a crypto error or result is not equal with source result.
*/
def checkCryptoError(tr: Crypto.Result[Array[Byte]], msg: Array[Byte])(implicit pos: Position): Assertion = {
tr.map { r tr.map { r
!(r sameElements msg) r != msg
}.fold( }.recover {
_ true, case e: CryptoError true
res => res case e
) shouldBe true logger.error("Unexpected error", e)
false
}.get shouldBe true
} }
} }
} }

View File

@ -17,8 +17,11 @@
package fluence.crypto.aes package fluence.crypto.aes
import cats.instances.either._ import cats.Monad
import fluence.crypto.{Crypto, JavaAlgorithm} import cats.data.EitherT
import cats.syntax.compose._
import fluence.codec.PureCodec
import fluence.crypto.{Crypto, CryptoError, JavaAlgorithm}
import org.bouncycastle.crypto.digests.SHA256Digest import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.engines.AESEngine import org.bouncycastle.crypto.engines.AESEngine
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator
@ -47,6 +50,7 @@ case class DataWithParams(data: Array[Byte], params: CipherParameters)
* message * message
*/ */
class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extends JavaAlgorithm { class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extends JavaAlgorithm {
import CryptoError.nonFatalHandling
private val rnd = Random private val rnd = Random
private val salt = config.salt.getBytes() private val salt = config.salt.getBytes()
@ -62,97 +66,34 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extend
iv iv
} }
/** val encrypt: Crypto.Func[Array[Byte], Array[Byte]] =
* Key spec initialization new Crypto.Func[Array[Byte], Array[Byte]] {
*/ override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
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
* encrypt: True for encryption and false for decryption
*
* @return cipher
*/
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")
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")
/**
*
* dataWithParams Cata with cipher parameters
* addData Additional data (nonce)
* encrypt True for encryption and false for decryption
* @return Crypted bytes
*/
private val processData: Crypto.Func[(DataWithParams, Option[Array[Byte]], Boolean), Array[Byte]] =
Crypto {
case (dataWithParams, addData, encrypt)
for { for {
cipher setupAesCipher(dataWithParams.params -> encrypt) key initSecretKey(password, salt)
buf cipherBytes(dataWithParams.data, cipher) extDataWithParams extDataWithParams(key)
} yield addData.map(_ ++ buf).getOrElse(buf) encData processData(DataWithParams(input, extDataWithParams._2), extDataWithParams._1, encrypt = true)
} yield encData
} }
/** val decrypt: Crypto.Func[Array[Byte], Array[Byte]] =
* encrypted data = initialization vector + data new Crypto.Func[Array[Byte], Array[Byte]] {
*/ override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
private val detachIV: Crypto.Func[(Array[Byte], Int), DetachedData] = for {
Crypto.tryFn[(Array[Byte], Int), DetachedData] { dataWithParams detachDataAndGetParams(input, password, salt, withIV)
case (data, ivSize) decData processData(dataWithParams, None, encrypt = false)
val ivData = data.slice(0, ivSize) } yield decData
val encData = data.slice(ivSize, data.length)
DetachedData(ivData, encData)
}("Cannot detach data and IV")
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 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 * Generate key parameters with IV if it is necessary
* key Password * @param key Password
* @return Optional IV and cipher parameters * @return Optional IV and cipher parameters
*/ */
val extDataWithParams: Crypto.Func[Array[Byte], (Option[Array[Byte]], CipherParameters)] = def extDataWithParams[F[_]: Monad](
Crypto( key: Array[Byte]
key ): EitherT[F, CryptoError, (Option[Array[Byte]], CipherParameters)] = {
if (withIV) { if (withIV) {
val ivData = generateIV val ivData = generateIV
@ -161,51 +102,139 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extend
} else { } else {
params(key).map(k (None, k)) params(key).map(k (None, k))
} }
) }
private val detachDataAndGetParams: Crypto.Func[(Array[Byte], Array[Char], Array[Byte], Boolean), DataWithParams] = /**
Crypto { * Key spec initialization
case (data, password, salt, withIV) */
private def initSecretKey[F[_]: Monad](
password: Array[Char],
salt: Array[Byte]
): EitherT[F, CryptoError, Array[Byte]] =
nonFatalHandling {
PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password)
}("Cannot init secret key.")
/**
* Setup AES CBC cipher
* @param 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)
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.")
}
/**
*
* @param dataWithParams Cata with cipher parameters
* @param addData Additional data (nonce)
* @param 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
}
/**
* 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 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 {
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) { if (withIV) {
for { for {
ivDataWithEncData detachIV(data -> IV_SIZE) ivDataWithEncData detachIV(data, IV_SIZE)
key initSecretKey(password -> salt) key initSecretKey(password, salt)
// setup cipher parameters with key and IV // setup cipher parameters with key and IV
paramsWithIV paramsWithIV(key, ivDataWithEncData.ivData) paramsWithIV paramsWithIV(key, ivDataWithEncData.ivData)
} yield DataWithParams(ivDataWithEncData.encData, paramsWithIV) } yield DataWithParams(ivDataWithEncData.encData, paramsWithIV)
} else { } else {
for { for {
key initSecretKey(password -> salt) key initSecretKey(password, salt)
// setup cipher parameters with key // setup cipher parameters with key
params params(key) params params(key)
} yield DataWithParams(data, params) } 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]] = object AesCrypt extends slogging.LazyLogging {
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 {
def build(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[Array[Byte]] = { def build(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[Array[Byte]] = {
val aes = new AesCrypt(password.toHex.toCharArray, withIV, config) val aes = new AesCrypt(password.toHex.toCharArray, withIV, config)
Crypto.Cipher(aes.encrypt, aes.decrypt) Crypto.Bijection(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,6 +17,7 @@
package fluence.crypto package fluence.crypto
import cats.instances.try_._
import fluence.crypto.aes.{AesConfig, AesCrypt} import fluence.crypto.aes.{AesConfig, AesCrypt}
import org.scalactic.source.Position import org.scalactic.source.Position
import org.scalatest.{Assertion, Matchers, WordSpec} import org.scalatest.{Assertion, Matchers, WordSpec}
@ -33,44 +34,51 @@ class AesSpec extends WordSpec with Matchers with slogging.LazyLogging {
"work with IV" in { "work with IV" in {
val pass = ByteVector("pass".getBytes()) val pass = ByteVector("pass".getBytes())
val crypt = AesCrypt.build(pass, withIV = true, config = conf) val crypt = AesCrypt.forString(pass, withIV = true, config = conf)
val str = rndString(200).getBytes val str = rndString(200)
val crypted = crypt.encrypt(str).right.get val crypted = crypt.direct.unsafe(str)
crypt.decrypt(crypted).right.get shouldBe str crypt.inverse.unsafe(crypted) shouldBe str
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = true, config = conf) val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = true, config = conf)
checkCryptoError(fakeAes.decrypt(crypted)) checkCryptoError(fakeAes.inverse.runF[Try](crypted))
//we cannot check if first bytes is iv or already data, but encryption goes wrong //we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithoutIV = AesCrypt.build(pass, withIV = false, config = conf) val aesWithoutIV = AesCrypt.forString(pass, withIV = false, config = conf)
aesWithoutIV.decrypt(crypted).right.get shouldNot be(str) aesWithoutIV.inverse.unsafe(crypted) shouldNot be(str)
val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10))) val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.decrypt(crypted)) checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted))
} }
"work without IV" in { "work without IV" in {
val pass = ByteVector("pass".getBytes()) val pass = ByteVector("pass".getBytes())
val crypt = AesCrypt.build(pass, withIV = false, config = conf) val crypt = AesCrypt.forString(pass, withIV = false, config = conf)
val str = rndString(200).getBytes() val str = rndString(200)
val crypted = crypt.encrypt(str).right.get val crypted = crypt.direct.unsafe(str)
crypt.decrypt(crypted).right.get shouldBe str crypt.inverse.unsafe(crypted) shouldBe str
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = false, config = conf) val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = false, config = conf)
checkCryptoError(fakeAes.decrypt(crypted)) checkCryptoError(fakeAes.inverse.runF[Try](crypted))
//we cannot check if first bytes is iv or already data, but encryption goes wrong //we cannot check if first bytes is iv or already data, but encryption goes wrong
val aesWithIV = AesCrypt.build(pass, withIV = true, config = conf) val aesWithIV = AesCrypt.forString(pass, withIV = true, config = conf)
aesWithIV.decrypt(crypted).right.get shouldNot be(str) aesWithIV.inverse.unsafe(crypted) shouldNot be(str)
val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10))) val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10)))
checkCryptoError(aesWrongSalt.decrypt(crypted)) checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted))
} }
} }
def checkCryptoError(tr: Crypto.Result[Array[Byte]])(implicit pos: Position): Assertion = { def checkCryptoError(tr: Try[String])(implicit pos: Position): Assertion = {
tr.isLeft shouldBe true tr.map(_ false)
.recover {
case e: CryptoError true
case e
logger.error("Unexpected error", e)
false
}
.get shouldBe true
} }
} }

View File

@ -17,32 +17,18 @@
package fluence.crypto package fluence.crypto
import cats.data.Kleisli import fluence.codec.{CodecError, MonadicalEitherArrow, PureCodec}
import scala.util.Try object Crypto extends MonadicalEitherArrow[CryptoError] {
type Hasher[A, B] = Func[A, B]
object Crypto { type Cipher[A] = Bijection[A, Array[Byte]]
type Result[T] = Either[CryptoError, T]
type Hasher[A, B] = Kleisli[Result, A, B] type KeyPairGenerator = Func[Option[Array[Byte]], KeyPair]
type Func[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))
case class Cipher[A]( implicit def codec[A, B](implicit codec: PureCodec[A, B]): Bijection[A, B] =
encrypt: Kleisli[Result, A, Array[Byte]], Bijection(fromOtherFunc(codec.direct), fromOtherFunc(codec.inverse))
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,11 +17,23 @@
package fluence.crypto package fluence.crypto
import scala.language.higherKinds import cats.Applicative
import scala.util.control.NoStackTrace import cats.data.EitherT
import scala.util.control.{NoStackTrace, NonFatal}
case class CryptoError(message: String, causedBy: Option[Throwable] = None) extends NoStackTrace { case class CryptoError(message: String, causedBy: Option[Throwable] = None) extends NoStackTrace {
override def getMessage: String = message override def getMessage: String = message
override def getCause: Throwable = causedBy getOrElse super.getCause override def getCause: Throwable = causedBy getOrElse super.getCause
} }
object CryptoError {
// TODO: there's a common `catchNonFatal` pattern, we should refactor this metod onto it
def nonFatalHandling[F[_]: Applicative, A](a: A)(errorText: String): EitherT[F, CryptoError, A] =
try EitherT.pure(a)
catch {
case NonFatal(e) EitherT.leftT(CryptoError(errorText + ": " + e.getLocalizedMessage, Some(e)))
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,101 +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.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

@ -1,27 +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.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

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

View File

@ -0,0 +1,99 @@
/*
* 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

@ -0,0 +1,64 @@
/*
* Copyright (C) 2017 Fluence Labs Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package flyence.crypto
import fluence.crypto.facade.ecdsa.{SHA1, SHA256}
import org.scalatest.{Matchers, WordSpec}
import scodec.bits.{Bases, ByteVector}
import scala.scalajs.js.JSConverters._
import scala.scalajs.js.typedarray.Uint8Array
class JSHashSpec extends WordSpec with Matchers {
"js hasher" should {
//test values get from third-party hash services
"work with sha256" in {
val str = "sha256Tester"
val sha256TesterHex = "513c17f8cf6ba96ce412cc2ae82f68821e9a2c6ae7a2fb1f5e46d08c387c8e65"
val hasher = new SHA256()
hasher.update(new Uint8Array(str.getBytes().toJSArray))
val hex = hasher.digest("hex")
hex shouldBe sha256TesterHex
}
"work with sha1" in {
val str = "sha1Tester"
val sha1TesterHex = "879db20eabcecea7d4736a8bae5bc64564b76b2f"
val hasher = new SHA1()
hasher.update(new Uint8Array(str.getBytes().toJSArray))
val hex = hasher.digest("hex")
hex shouldBe sha1TesterHex
}
"check unsigned array with sha1" in {
val arr = Array[Byte](3, -9, -31, 48, 10, 125, 51, -39, -20, -125, 123, 61, -36, 49, 76, 90, -16, 54, -61, 62, 50,
-116, -37, -88, -125, -32, -105, 120, 118, 13, -37, 33, -36)
val base64Check = "9keNwsj08vKTlwIpHAEYvsfpdP4="
val hasher = new SHA1()
hasher.update(new Uint8Array(arr.toJSArray))
val hex = hasher.digest("hex")
ByteVector.fromValidHex(hex, Bases.Alphabets.HexLowercase).toBase64 shouldBe base64Check
}
}
}

View File

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

View File

@ -1,170 +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.eddsa
import java.security._
import cats.syntax.either._
import cats.instances.either._
import cats.syntax.functor._
import fluence.crypto.KeyPair.Secret
import fluence.crypto.signature.{SignAlgo, SignatureChecker, Signer}
import fluence.crypto.{KeyPair, _}
import org.bouncycastle.crypto.{AsymmetricCipherKeyPair, KeyGenerationParameters}
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator
import org.bouncycastle.crypto.params.{Ed25519PrivateKeyParameters, Ed25519PublicKeyParameters}
import org.bouncycastle.crypto.signers.Ed25519Signer
import scodec.bits.ByteVector
import scala.language.higherKinds
/**
* Edwards-curve Digital Signature Algorithm (EdDSA)
*/
class Ed25519(strength: Int) extends JavaAlgorithm {
/**
* Restores pair of keys from the known secret key.
* The public key will be the same each method call with the same secret key.
* sk secret key
* @return key pair
*/
val restorePairFromSecret: Crypto.Func[Secret, KeyPair] =
Crypto.tryFn[Secret, KeyPair] { sk
val secret = new Ed25519PrivateKeyParameters(sk.bytes, 0)
KeyPair.fromBytes(secret.generatePublicKey().getEncoded, sk.bytes)
}("Could not generate KeyPair from private key")
private val signMessage: Crypto.Func[(Array[Byte], Array[Byte]), Array[Byte]] =
Crypto.tryFn[(Array[Byte], Array[Byte]), Array[Byte]] {
case (
privateKey,
message
)
val privKey = new Ed25519PrivateKeyParameters(privateKey, 0)
val signer = new Ed25519Signer
signer.init(true, privKey)
signer.update(message, 0, message.length)
signer.generateSignature()
}("Cannot sign message")
val sign: Crypto.Func[(KeyPair, ByteVector), signature.Signature] =
signMessage
.map(bb fluence.crypto.signature.Signature(ByteVector(bb)))
.local {
case (keyPair, message)
keyPair.secretKey.bytes -> message.toArray
}
private val verifySign: Crypto.Func[(Array[Byte], Array[Byte], Array[Byte]), Unit] =
Crypto.tryFn[(Array[Byte], Array[Byte], Array[Byte]), Boolean] {
case (
publicKey,
signature,
message
)
val pubKey = new Ed25519PublicKeyParameters(publicKey, 0)
val signer = new Ed25519Signer
signer.init(false, pubKey)
signer.update(message, 0, message.length)
signer.verifySignature(signature)
}("Cannot verify message") andThen Crypto.cond((), "Signature is not verified")
val verify: Crypto.Func[(KeyPair.Public, signature.Signature, ByteVector), Unit] =
verifySign.local {
case (
publicKey,
signature,
message
)
(publicKey.bytes, signature.bytes, message.toArray)
}
private def getKeyPairGenerator =
Crypto.tryUnit(
new Ed25519KeyPairGenerator()
)(
"Cannot get key pair generator"
)
val generateKeyPair: Crypto.KeyPairGenerator =
Crypto[Option[Array[Byte]], KeyPair] { input
getKeyPairGenerator.flatMap { g
val random = input.map(new SecureRandom(_)).getOrElse(new SecureRandom())
val keyParameters = new KeyGenerationParameters(random, strength)
g.init(keyParameters)
Either.fromOption(Option(g.generateKeyPair()), CryptoError("Generated keypair is null"))
}.flatMap(
(p: AsymmetricCipherKeyPair)
Crypto.tryUnit {
val pk = p.getPublic match {
case pk: Ed25519PublicKeyParameters => pk.getEncoded
case p =>
throw new ClassCastException(s"Cannot cast public key (${p.getClass}) to Ed25519PublicKeyParameters")
}
val sk = p.getPrivate match {
case sk: Ed25519PrivateKeyParameters => sk.getEncoded
case s =>
throw new ClassCastException(s"Cannot cast private key (${p.getClass}) to Ed25519PrivateKeyParameters")
}
KeyPair.fromBytes(pk, sk)
}("Could not generate KeyPair")
)
}
}
object Ed25519 {
/**
* Keys in tendermint are generating with a random seed of 32 bytes
*/
val ed25519 = new Ed25519(256)
val signAlgo: SignAlgo = signAlgoInit(256)
/**
*
* @param strength the size, in bits, of the keys we want to produce
*/
def ed25519Init(strength: Int) = new Ed25519(strength)
/**
*
* @param strength the size, in bits, of the keys we want to produce
*/
def signAlgoInit(strength: Int): SignAlgo = {
val algo = ed25519Init(strength)
SignAlgo(
name = "ed25519",
generateKeyPair = algo.generateKeyPair,
signer = kp
Signer(
kp.publicKey,
Crypto[ByteVector, signature.Signature] { input
algo.sign(kp -> input)
}
),
checker = pk
SignatureChecker(
Crypto {
case (signature, plain)
algo.verify(pk, signature, plain)
}
)
)
}
}

View File

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

View File

@ -1,45 +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 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

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

View File

@ -0,0 +1,141 @@
/*
* 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

@ -1,88 +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 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

@ -1,88 +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 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

@ -0,0 +1,117 @@
/*
* 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.Base64.alphabetToVector(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

@ -0,0 +1,82 @@
/*
* 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

@ -0,0 +1,83 @@
/*
* 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

@ -0,0 +1,84 @@
/*
* 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.Base64.alphabetToVector(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

@ -0,0 +1,49 @@
/*
* 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,3 +26,4 @@ object FluenceCrossType extends sbtcrossproject.CrossType {
override def sharedSrcDir(projectBase: File, conf: String) = override def sharedSrcDir(projectBase: File, conf: String) =
Some(shared(projectBase, conf)) Some(shared(projectBase, conf))
} }

View File

@ -1,10 +1,10 @@
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.1") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0")
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.20") addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.20")
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.2.0") addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.2.0")
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28") addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.27")
addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "0.6.0") addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "0.6.0")
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0") addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0")