mirror of
https://github.com/fluencelabs/crypto
synced 2025-04-25 06:42:19 +00:00
Compare commits
8 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
4951c4a2a8 | ||
|
64f12eb609 | ||
|
1502772f8e | ||
|
d63509f077 | ||
|
f7557a1d75 | ||
|
16f5a93562 | ||
|
38608df192 | ||
|
1a8313b321 |
@ -1,20 +1,11 @@
|
||||
version = 2.0.1
|
||||
|
||||
docstrings = JavaDoc
|
||||
|
||||
maxColumn = 120
|
||||
|
||||
rewriteTokens {
|
||||
"=>": "⇒"
|
||||
"<-": "←"
|
||||
}
|
||||
|
||||
align = none
|
||||
align {
|
||||
openParenCallSite = false
|
||||
openParenDefnSite = false
|
||||
tokens = [
|
||||
"%", "%%", "%%%", ":=", "~="
|
||||
]
|
||||
}
|
||||
align = some
|
||||
align.tokens = [{code = "=>", owner = "Case"}, ":=", "%", "%%", "%%%"]
|
||||
|
||||
assumeStandardLibraryStripMargin = true
|
||||
includeCurlyBraceInSelectChains = false
|
||||
@ -62,3 +53,4 @@ rewrite {
|
||||
SortImports
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -2,9 +2,7 @@ sudo: required
|
||||
|
||||
language: scala
|
||||
scala:
|
||||
- 2.12.5
|
||||
jdk:
|
||||
- oraclejdk8
|
||||
- 2.12.9
|
||||
|
||||
# These directories are cached to S3 at the end of the build
|
||||
cache:
|
||||
|
69
build.sbt
69
build.sbt
@ -10,11 +10,11 @@ javaOptions in Test ++= Seq("-ea")
|
||||
|
||||
skip in publish := true // Skip root project
|
||||
|
||||
val scalaV = scalaVersion := "2.12.8"
|
||||
val scalaV = scalaVersion := "2.12.9"
|
||||
|
||||
val commons = Seq(
|
||||
scalaV,
|
||||
version := "0.0.4",
|
||||
version := "0.1.0",
|
||||
fork in Test := true,
|
||||
parallelExecution in Test := false,
|
||||
organization := "one.fluence",
|
||||
@ -25,15 +25,15 @@ val commons = Seq(
|
||||
headerLicense := Some(License.AGPLv3("2017", organizationName.value)),
|
||||
bintrayOrganization := Some("fluencelabs"),
|
||||
publishMavenStyle := true,
|
||||
scalafmtOnCompile := true,
|
||||
bintrayRepository := "releases",
|
||||
resolvers += Resolver.bintrayRepo("fluencelabs", "releases")
|
||||
resolvers ++= Seq(Resolver.bintrayRepo("fluencelabs", "releases"), Resolver.sonatypeRepo("releases"))
|
||||
)
|
||||
|
||||
commons
|
||||
|
||||
val CodecV = "0.0.5"
|
||||
|
||||
val CatsEffectV = "1.2.0"
|
||||
val CatsV = "2.0.0"
|
||||
val CirceV = "0.12.1"
|
||||
|
||||
val SloggingV = "0.6.1"
|
||||
|
||||
@ -50,7 +50,8 @@ lazy val `crypto-core` = crossProject(JVMPlatform, JSPlatform)
|
||||
.settings(
|
||||
commons,
|
||||
libraryDependencies ++= Seq(
|
||||
"one.fluence" %%% "codec-bits" % CodecV,
|
||||
"org.scodec" %%% "scodec-core" % "1.11.3",
|
||||
"org.typelevel" %%% "cats-core" % CatsV,
|
||||
"org.scalatest" %%% "scalatest" % ScalatestV % Test
|
||||
)
|
||||
)
|
||||
@ -62,30 +63,6 @@ lazy val `crypto-core` = crossProject(JVMPlatform, JSPlatform)
|
||||
lazy val `crypto-core-js` = `crypto-core`.js
|
||||
lazy val `crypto-core-jvm` = `crypto-core`.jvm
|
||||
|
||||
lazy val `crypto-keystore` = crossProject(JVMPlatform, JSPlatform)
|
||||
.withoutSuffixFor(JVMPlatform)
|
||||
.crossType(FluenceCrossType)
|
||||
.in(file("keystore"))
|
||||
.settings(
|
||||
commons,
|
||||
libraryDependencies ++= Seq(
|
||||
"one.fluence" %%% "codec-circe" % CodecV,
|
||||
"biz.enef" %%% "slogging" % SloggingV,
|
||||
"org.scalatest" %%% "scalatest" % ScalatestV % Test
|
||||
)
|
||||
)
|
||||
.jsSettings(
|
||||
fork in Test := false
|
||||
)
|
||||
.jvmSettings(
|
||||
libraryDependencies += "org.typelevel" %% "cats-effect" % CatsEffectV,
|
||||
)
|
||||
.enablePlugins(AutomateHeaderPlugin)
|
||||
.dependsOn(`crypto-core`)
|
||||
|
||||
lazy val `crypto-keystore-js` = `crypto-keystore`.js
|
||||
lazy val `crypto-keystore-jvm` = `crypto-keystore`.jvm
|
||||
|
||||
lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
|
||||
.withoutSuffixFor(JVMPlatform)
|
||||
.crossType(FluenceCrossType)
|
||||
@ -103,8 +80,11 @@ lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
|
||||
)
|
||||
)
|
||||
.jsSettings(
|
||||
libraryDependencies += "io.scalajs" %%% "nodejs" % "0.4.2",
|
||||
npmDependencies in Compile ++= Seq(
|
||||
"elliptic" -> "6.4.0"
|
||||
"elliptic" -> "6.4.1",
|
||||
"supercop.js" -> "2.0.1",
|
||||
"hash.js" -> "1.1.7"
|
||||
),
|
||||
scalaJSModuleKind in Test := ModuleKind.CommonJSModule,
|
||||
//all JavaScript dependencies will be concatenated to a single file *-jsdeps.js
|
||||
@ -112,7 +92,7 @@ lazy val `crypto-hashsign` = crossProject(JVMPlatform, JSPlatform)
|
||||
fork in Test := false
|
||||
)
|
||||
.enablePlugins(AutomateHeaderPlugin)
|
||||
.dependsOn(`crypto-core`, `crypto-keystore` % Test)
|
||||
.dependsOn(`crypto-core`)
|
||||
|
||||
lazy val `crypto-hashsign-js` = `crypto-hashsign`.js
|
||||
.enablePlugins(ScalaJSBundlerPlugin)
|
||||
@ -125,7 +105,7 @@ lazy val `crypto-cipher` = crossProject(JVMPlatform, JSPlatform)
|
||||
.settings(
|
||||
commons,
|
||||
libraryDependencies ++= Seq(
|
||||
"biz.enef" %%% "slogging" % SloggingV,
|
||||
"biz.enef" %%% "slogging" % SloggingV % Test,
|
||||
"org.scalatest" %%% "scalatest" % ScalatestV % Test
|
||||
)
|
||||
)
|
||||
@ -150,24 +130,3 @@ lazy val `crypto-cipher` = crossProject(JVMPlatform, JSPlatform)
|
||||
lazy val `crypto-cipher-js` = `crypto-cipher`.js
|
||||
.enablePlugins(ScalaJSBundlerPlugin)
|
||||
lazy val `crypto-cipher-jvm` = `crypto-cipher`.jvm
|
||||
|
||||
lazy val `crypto-jwt` = crossProject(JVMPlatform, JSPlatform)
|
||||
.withoutSuffixFor(JVMPlatform)
|
||||
.crossType(FluenceCrossType)
|
||||
.in(file("jwt"))
|
||||
.settings(
|
||||
commons,
|
||||
libraryDependencies ++= Seq(
|
||||
"one.fluence" %%% "codec-circe" % CodecV,
|
||||
"org.scalatest" %%% "scalatest" % ScalatestV % Test
|
||||
)
|
||||
)
|
||||
.jsSettings(
|
||||
fork in Test := false,
|
||||
scalaJSModuleKind := ModuleKind.CommonJSModule
|
||||
)
|
||||
.enablePlugins(AutomateHeaderPlugin)
|
||||
.dependsOn(`crypto-core`)
|
||||
|
||||
lazy val `crypto-jwt-js` = `crypto-jwt`.js
|
||||
lazy val `crypto-jwt-jvm` = `crypto-jwt`.jvm
|
||||
|
@ -17,11 +17,6 @@
|
||||
|
||||
package fluence.crypto.aes
|
||||
|
||||
import cats.Monad
|
||||
import cats.data.EitherT
|
||||
import cats.syntax.compose._
|
||||
import fluence.codec.PureCodec
|
||||
import fluence.crypto.CryptoError.nonFatalHandling
|
||||
import fluence.crypto.facade.cryptojs.{CryptOptions, CryptoJS, Key, KeyOptions}
|
||||
import fluence.crypto.{Crypto, CryptoError}
|
||||
import scodec.bits.ByteVector
|
||||
@ -48,35 +43,15 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) {
|
||||
private val mode = CryptoJS.mode.CBC
|
||||
private val aes = CryptoJS.AES
|
||||
|
||||
val encrypt: Crypto.Func[Array[Byte], Array[Byte]] =
|
||||
new Crypto.Func[Array[Byte], Array[Byte]] {
|
||||
override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
|
||||
for {
|
||||
key ← initSecretKey()
|
||||
encrypted ← encryptData(input, key)
|
||||
} yield encrypted
|
||||
}
|
||||
|
||||
val decrypt: Crypto.Func[Array[Byte], Array[Byte]] =
|
||||
new Crypto.Func[Array[Byte], Array[Byte]] {
|
||||
override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
|
||||
for {
|
||||
detachedData ← detachData(input)
|
||||
(iv, base64) = detachedData
|
||||
key ← initSecretKey()
|
||||
decData ← decryptData(key, base64, iv)
|
||||
_ ← EitherT.cond(decData.nonEmpty, decData, CryptoError("Cannot decrypt message with this password."))
|
||||
} yield decData.toArray
|
||||
}
|
||||
|
||||
/**
|
||||
* Encrypt data.
|
||||
* @param data Data to encrypt
|
||||
* @param key Salted and hashed password
|
||||
* data Data to encrypt
|
||||
* key Salted and hashed password
|
||||
* @return Encrypted data with IV
|
||||
*/
|
||||
private def encryptData[F[_]: Monad](data: Array[Byte], key: Key): EitherT[F, CryptoError, Array[Byte]] = {
|
||||
nonFatalHandling {
|
||||
private val encryptData =
|
||||
Crypto.tryFn[(Array[Byte], Key), Array[Byte]] {
|
||||
case (data: Array[Byte], key: Key) ⇒
|
||||
//transform data to JS type
|
||||
val wordArray = CryptoJS.lib.WordArray.create(new Int8Array(data.toJSArray))
|
||||
val iv = if (withIV) Some(generateIV) else None
|
||||
@ -86,24 +61,23 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) {
|
||||
//IV also needs to be transformed in byte array
|
||||
val byteIv = iv.map(i ⇒ ByteVector.fromValidHex(i.toString))
|
||||
byteIv.map(_.toArray ++ crypted.toArray).getOrElse(crypted.toArray)
|
||||
}("Cannot encrypt data.")
|
||||
}
|
||||
}("Cannot encrypt data")
|
||||
|
||||
private def decryptData[F[_]: Monad](key: Key, base64Data: String, iv: Option[String]) = {
|
||||
nonFatalHandling {
|
||||
private val decryptData: Crypto.Func[(Key, String, Option[String]), ByteVector] =
|
||||
Crypto.tryFn[(Key, String, Option[String]), ByteVector] {
|
||||
case (key: Key, base64Data: String, iv: Option[String]) ⇒
|
||||
//parse IV to WordArray JS format
|
||||
val cryptOptions = CryptOptions(iv = iv.map(i ⇒ CryptoJS.enc.Hex.parse(i)), padding = pad, mode = mode)
|
||||
val dec = aes.decrypt(base64Data, key, cryptOptions)
|
||||
ByteVector.fromValidHex(dec.toString)
|
||||
}("Cannot decrypt data.")
|
||||
}
|
||||
}("Cannot decrypt data")
|
||||
|
||||
/**
|
||||
* @param cipherText Encrypted data with IV
|
||||
* cipherText Encrypted data with IV
|
||||
* @return IV in hex and data in base64
|
||||
*/
|
||||
private def detachData[F[_]: Monad](cipherText: Array[Byte]): EitherT[F, CryptoError, (Option[String], String)] = {
|
||||
nonFatalHandling {
|
||||
private val detachData: Crypto.Func[Array[Byte], (Option[String], String)] =
|
||||
Crypto.tryFn { cipherText: Array[Byte] ⇒
|
||||
val dataWithParams = if (withIV) {
|
||||
val ivDec = ByteVector(cipherText.slice(0, IV_SIZE)).toHex
|
||||
val encMessage = cipherText.slice(IV_SIZE, cipherText.length)
|
||||
@ -112,36 +86,45 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) {
|
||||
val (ivOp, data) = dataWithParams
|
||||
val base64 = ByteVector(data).toBase64
|
||||
(ivOp, base64)
|
||||
}("Cannot detach data and IV.")
|
||||
}
|
||||
}("Cannot detach data and IV")
|
||||
|
||||
/**
|
||||
* Hash password with salt `iterationCount` times
|
||||
*/
|
||||
private def initSecretKey[F[_]: Monad](): EitherT[F, CryptoError, Key] = {
|
||||
nonFatalHandling {
|
||||
private val initSecretKey: Crypto.Func[Unit, Key] =
|
||||
Crypto.tryFn { _: Unit ⇒
|
||||
// get raw key from password and salt
|
||||
val keyOption = KeyOptions(BITS, iterations = iterationCount, hasher = CryptoJS.algo.SHA256)
|
||||
CryptoJS.PBKDF2(new String(password), salt, keyOption)
|
||||
}("Cannot init secret key.")
|
||||
}("Cannot init secret key")
|
||||
|
||||
val decrypt: Crypto.Func[Array[Byte], Array[Byte]] =
|
||||
Crypto { input ⇒
|
||||
for {
|
||||
detachedData ← detachData(input)
|
||||
(iv, base64) = detachedData
|
||||
key ← initSecretKey(())
|
||||
decData ← decryptData((key, base64, iv))
|
||||
_ ← Crypto[Boolean, Unit](Either.cond(_, (), CryptoError("Cannot decrypt message with this password.")))(
|
||||
decData.nonEmpty
|
||||
)
|
||||
} yield decData.toArray
|
||||
}
|
||||
|
||||
val encrypt: Crypto.Func[Array[Byte], Array[Byte]] =
|
||||
Crypto[Array[Byte], Array[Byte]] { input ⇒
|
||||
for {
|
||||
key ← initSecretKey(())
|
||||
encrypted ← encryptData(input -> key)
|
||||
} yield encrypted
|
||||
}
|
||||
}
|
||||
|
||||
object AesCrypt extends slogging.LazyLogging {
|
||||
object AesCrypt {
|
||||
|
||||
def build(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[Array[Byte]] = {
|
||||
val aes = new AesCrypt(password.toHex.toCharArray, withIV, config)
|
||||
Crypto.Bijection(aes.encrypt, aes.decrypt)
|
||||
Crypto.Cipher(aes.encrypt, aes.decrypt)
|
||||
}
|
||||
|
||||
def forString(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[String] = {
|
||||
implicit val codec: PureCodec[String, Array[Byte]] =
|
||||
PureCodec.build(_.getBytes, bytes ⇒ new String(bytes))
|
||||
apply[String](password, withIV, config)
|
||||
}
|
||||
|
||||
def apply[T](password: ByteVector, withIV: Boolean, config: AesConfig)(
|
||||
implicit codec: PureCodec[T, Array[Byte]]
|
||||
): Crypto.Cipher[T] =
|
||||
Crypto.codec[T, Array[Byte]] andThen build(password, withIV, config)
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package fluence.crypto
|
||||
|
||||
import cats.instances.try_._
|
||||
import fluence.crypto.aes.{AesConfig, AesCrypt}
|
||||
import org.scalactic.source.Position
|
||||
import org.scalatest.{Assertion, Matchers, WordSpec}
|
||||
@ -35,51 +34,52 @@ class AesJSSpec extends WordSpec with Matchers with slogging.LazyLogging {
|
||||
"aes crypto" should {
|
||||
"work with IV" in {
|
||||
val pass = ByteVector("pass".getBytes())
|
||||
val crypt = AesCrypt.forString(pass, withIV = true, config = conf)
|
||||
val crypt = AesCrypt.build(pass, withIV = true, config = conf)
|
||||
|
||||
val str = rndString(200)
|
||||
val crypted = crypt.direct.unsafe(str)
|
||||
crypt.inverse.unsafe(crypted) shouldBe str
|
||||
val str = rndString(200).getBytes()
|
||||
val crypted = crypt.encrypt(str).right.get
|
||||
crypt.decrypt(crypted).right.get shouldBe str
|
||||
|
||||
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = true, config = conf)
|
||||
checkCryptoError(fakeAes.inverse.runF[Try](crypted), str)
|
||||
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = true, config = conf)
|
||||
checkCryptoError(fakeAes.decrypt(crypted), str)
|
||||
|
||||
//we cannot check if first bytes is iv or already data, but encryption goes wrong
|
||||
val aesWithoutIV = AesCrypt.forString(pass, withIV = false, config = conf)
|
||||
aesWithoutIV.inverse.unsafe(crypted) shouldNot be(str)
|
||||
val aesWithoutIV = AesCrypt.build(pass, withIV = false, config = conf)
|
||||
aesWithoutIV.decrypt(crypted).right.get shouldNot be(str)
|
||||
|
||||
val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10)))
|
||||
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted), str)
|
||||
val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10)))
|
||||
checkCryptoError(aesWrongSalt.decrypt(crypted), str)
|
||||
}
|
||||
|
||||
"work without IV" in {
|
||||
val pass = ByteVector("pass".getBytes())
|
||||
val crypt = AesCrypt.forString(pass, withIV = false, config = conf)
|
||||
val crypt = AesCrypt.build(pass, withIV = false, config = conf)
|
||||
|
||||
val str = rndString(200)
|
||||
val crypted = crypt.direct.unsafe(str)
|
||||
crypt.inverse.unsafe(crypted) shouldBe str
|
||||
val str = rndString(200).getBytes()
|
||||
val crypted = crypt.encrypt(str).right.get
|
||||
crypt.decrypt(crypted).right.get shouldBe str
|
||||
|
||||
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = false, config = conf)
|
||||
checkCryptoError(fakeAes.inverse.runF[Try](crypted), str)
|
||||
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = false, config = conf)
|
||||
checkCryptoError(fakeAes.decrypt(crypted), str)
|
||||
|
||||
//we cannot check if first bytes is iv or already data, but encryption goes wrong
|
||||
val aesWithIV = AesCrypt.forString(pass, withIV = true, config = conf)
|
||||
aesWithIV.inverse.unsafe(crypted) shouldNot be(str)
|
||||
val aesWithIV = AesCrypt.build(pass, withIV = true, config = conf)
|
||||
aesWithIV.decrypt(crypted).right.get shouldNot be(str)
|
||||
|
||||
val aesWrongSalt = AesCrypt.forString(pass, withIV = false, config = conf.copy(salt = rndString(10)))
|
||||
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted), str)
|
||||
val aesWrongSalt = AesCrypt.build(pass, withIV = false, config = conf.copy(salt = rndString(10)))
|
||||
checkCryptoError(aesWrongSalt.decrypt(crypted), str)
|
||||
}
|
||||
|
||||
def checkCryptoError(tr: Try[String], msg: String)(implicit pos: Position): Assertion = {
|
||||
/**
|
||||
* Checks if there is a crypto error or result is not equal with source result.
|
||||
*/
|
||||
def checkCryptoError(tr: Crypto.Result[Array[Byte]], msg: Array[Byte])(implicit pos: Position): Assertion = {
|
||||
tr.map { r ⇒
|
||||
r != msg
|
||||
}.recover {
|
||||
case e: CryptoError ⇒ true
|
||||
case e ⇒
|
||||
logger.error("Unexpected error", e)
|
||||
false
|
||||
}.get shouldBe true
|
||||
!(r sameElements msg)
|
||||
}.fold(
|
||||
_ ⇒ true,
|
||||
res => res
|
||||
) shouldBe true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,11 +17,8 @@
|
||||
|
||||
package fluence.crypto.aes
|
||||
|
||||
import cats.Monad
|
||||
import cats.data.EitherT
|
||||
import cats.syntax.compose._
|
||||
import fluence.codec.PureCodec
|
||||
import fluence.crypto.{Crypto, CryptoError, JavaAlgorithm}
|
||||
import cats.instances.either._
|
||||
import fluence.crypto.{Crypto, JavaAlgorithm}
|
||||
import org.bouncycastle.crypto.digests.SHA256Digest
|
||||
import org.bouncycastle.crypto.engines.AESEngine
|
||||
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator
|
||||
@ -50,7 +47,6 @@ case class DataWithParams(data: Array[Byte], params: CipherParameters)
|
||||
* message
|
||||
*/
|
||||
class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extends JavaAlgorithm {
|
||||
import CryptoError.nonFatalHandling
|
||||
|
||||
private val rnd = Random
|
||||
private val salt = config.salt.getBytes()
|
||||
@ -66,34 +62,97 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extend
|
||||
iv
|
||||
}
|
||||
|
||||
val encrypt: Crypto.Func[Array[Byte], Array[Byte]] =
|
||||
new Crypto.Func[Array[Byte], Array[Byte]] {
|
||||
override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
|
||||
for {
|
||||
key ← initSecretKey(password, salt)
|
||||
extDataWithParams ← extDataWithParams(key)
|
||||
encData ← processData(DataWithParams(input, extDataWithParams._2), extDataWithParams._1, encrypt = true)
|
||||
} yield encData
|
||||
/**
|
||||
* Key spec initialization
|
||||
*/
|
||||
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 {
|
||||
cipher ← setupAesCipher(dataWithParams.params -> encrypt)
|
||||
buf ← cipherBytes(dataWithParams.data, cipher)
|
||||
} yield addData.map(_ ++ buf).getOrElse(buf)
|
||||
}
|
||||
|
||||
val decrypt: Crypto.Func[Array[Byte], Array[Byte]] =
|
||||
new Crypto.Func[Array[Byte], Array[Byte]] {
|
||||
override def apply[F[_]: Monad](input: Array[Byte]): EitherT[F, CryptoError, Array[Byte]] =
|
||||
for {
|
||||
dataWithParams ← detachDataAndGetParams(input, password, salt, withIV)
|
||||
decData ← processData(dataWithParams, None, encrypt = false)
|
||||
} yield decData
|
||||
/**
|
||||
* encrypted data = initialization vector + data
|
||||
*/
|
||||
private val detachIV: Crypto.Func[(Array[Byte], Int), DetachedData] =
|
||||
Crypto.tryFn[(Array[Byte], Int), DetachedData] {
|
||||
case (data, ivSize) ⇒
|
||||
val ivData = data.slice(0, ivSize)
|
||||
val encData = data.slice(ivSize, data.length)
|
||||
DetachedData(ivData, encData)
|
||||
}("Cannot detach data and IV")
|
||||
|
||||
private 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
|
||||
* @param key Password
|
||||
* key Password
|
||||
* @return Optional IV and cipher parameters
|
||||
*/
|
||||
def extDataWithParams[F[_]: Monad](
|
||||
key: Array[Byte]
|
||||
): EitherT[F, CryptoError, (Option[Array[Byte]], CipherParameters)] = {
|
||||
val extDataWithParams: Crypto.Func[Array[Byte], (Option[Array[Byte]], CipherParameters)] =
|
||||
Crypto(
|
||||
key ⇒
|
||||
if (withIV) {
|
||||
val ivData = generateIV
|
||||
|
||||
@ -102,139 +161,51 @@ class AesCrypt(password: Array[Char], withIV: Boolean, config: AesConfig) extend
|
||||
} else {
|
||||
params(key).map(k ⇒ (None, k))
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
/**
|
||||
* Key spec initialization
|
||||
*/
|
||||
private def initSecretKey[F[_]: Monad](
|
||||
password: Array[Char],
|
||||
salt: Array[Byte]
|
||||
): EitherT[F, CryptoError, Array[Byte]] =
|
||||
nonFatalHandling {
|
||||
PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password)
|
||||
}("Cannot init secret key.")
|
||||
|
||||
/**
|
||||
* 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] = {
|
||||
private val detachDataAndGetParams: Crypto.Func[(Array[Byte], Array[Char], Array[Byte], Boolean), DataWithParams] =
|
||||
Crypto {
|
||||
case (data, password, salt, withIV) ⇒
|
||||
if (withIV) {
|
||||
for {
|
||||
ivDataWithEncData ← detachIV(data, IV_SIZE)
|
||||
key ← initSecretKey(password, salt)
|
||||
ivDataWithEncData ← detachIV(data -> IV_SIZE)
|
||||
key ← initSecretKey(password -> salt)
|
||||
// setup cipher parameters with key and IV
|
||||
paramsWithIV ← paramsWithIV(key, ivDataWithEncData.ivData)
|
||||
} yield DataWithParams(ivDataWithEncData.encData, paramsWithIV)
|
||||
} else {
|
||||
for {
|
||||
key ← initSecretKey(password, salt)
|
||||
key ← initSecretKey(password -> salt)
|
||||
// setup cipher parameters with key
|
||||
params ← params(key)
|
||||
} yield DataWithParams(data, params)
|
||||
}
|
||||
}
|
||||
|
||||
val decrypt: Crypto.Func[Array[Byte], Array[Byte]] =
|
||||
Crypto[Array[Byte], Array[Byte]] { input: Array[Byte] ⇒
|
||||
for {
|
||||
dataWithParams ← detachDataAndGetParams((input, password, salt, withIV))
|
||||
decData ← processData((dataWithParams, None, /*encrypt =*/ false))
|
||||
} yield decData
|
||||
}
|
||||
|
||||
val encrypt: Crypto.Func[Array[Byte], Array[Byte]] =
|
||||
Crypto { input: Array[Byte] ⇒
|
||||
for {
|
||||
key ← initSecretKey(password -> salt)
|
||||
extDataWithParams ← extDataWithParams(key)
|
||||
encData ← processData((DataWithParams(input, extDataWithParams._2), extDataWithParams._1, /*encrypt =*/ true))
|
||||
} yield encData
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
object AesCrypt extends slogging.LazyLogging {
|
||||
object AesCrypt {
|
||||
|
||||
def build(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[Array[Byte]] = {
|
||||
val aes = new AesCrypt(password.toHex.toCharArray, withIV, config)
|
||||
Crypto.Bijection(aes.encrypt, aes.decrypt)
|
||||
Crypto.Cipher(aes.encrypt, aes.decrypt)
|
||||
}
|
||||
|
||||
def forString(password: ByteVector, withIV: Boolean, config: AesConfig): Crypto.Cipher[String] = {
|
||||
implicit val codec: PureCodec[String, Array[Byte]] =
|
||||
PureCodec.build(_.getBytes, bytes ⇒ new String(bytes))
|
||||
apply[String](password, withIV, config)
|
||||
}
|
||||
|
||||
def apply[T](password: ByteVector, withIV: Boolean, config: AesConfig)(
|
||||
implicit codec: PureCodec[T, Array[Byte]]
|
||||
): Crypto.Cipher[T] =
|
||||
Crypto.codec[T, Array[Byte]] andThen build(password, withIV, config)
|
||||
}
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
package fluence.crypto
|
||||
|
||||
import cats.instances.try_._
|
||||
import fluence.crypto.aes.{AesConfig, AesCrypt}
|
||||
import org.scalactic.source.Position
|
||||
import org.scalatest.{Assertion, Matchers, WordSpec}
|
||||
@ -34,51 +33,44 @@ class AesSpec extends WordSpec with Matchers with slogging.LazyLogging {
|
||||
"work with IV" in {
|
||||
|
||||
val pass = ByteVector("pass".getBytes())
|
||||
val crypt = AesCrypt.forString(pass, withIV = true, config = conf)
|
||||
val crypt = AesCrypt.build(pass, withIV = true, config = conf)
|
||||
|
||||
val str = rndString(200)
|
||||
val crypted = crypt.direct.unsafe(str)
|
||||
crypt.inverse.unsafe(crypted) shouldBe str
|
||||
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = true, config = conf)
|
||||
checkCryptoError(fakeAes.inverse.runF[Try](crypted))
|
||||
val str = rndString(200).getBytes
|
||||
val crypted = crypt.encrypt(str).right.get
|
||||
crypt.decrypt(crypted).right.get shouldBe str
|
||||
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = true, config = conf)
|
||||
checkCryptoError(fakeAes.decrypt(crypted))
|
||||
|
||||
//we cannot check if first bytes is iv or already data, but encryption goes wrong
|
||||
val aesWithoutIV = AesCrypt.forString(pass, withIV = false, config = conf)
|
||||
aesWithoutIV.inverse.unsafe(crypted) shouldNot be(str)
|
||||
val aesWithoutIV = AesCrypt.build(pass, withIV = false, config = conf)
|
||||
aesWithoutIV.decrypt(crypted).right.get shouldNot be(str)
|
||||
|
||||
val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10)))
|
||||
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted))
|
||||
val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10)))
|
||||
checkCryptoError(aesWrongSalt.decrypt(crypted))
|
||||
}
|
||||
|
||||
"work without IV" in {
|
||||
val pass = ByteVector("pass".getBytes())
|
||||
val crypt = AesCrypt.forString(pass, withIV = false, config = conf)
|
||||
val crypt = AesCrypt.build(pass, withIV = false, config = conf)
|
||||
|
||||
val str = rndString(200)
|
||||
val crypted = crypt.direct.unsafe(str)
|
||||
crypt.inverse.unsafe(crypted) shouldBe str
|
||||
val str = rndString(200).getBytes()
|
||||
val crypted = crypt.encrypt(str).right.get
|
||||
crypt.decrypt(crypted).right.get shouldBe str
|
||||
|
||||
val fakeAes = AesCrypt.forString(ByteVector("wrong".getBytes()), withIV = false, config = conf)
|
||||
checkCryptoError(fakeAes.inverse.runF[Try](crypted))
|
||||
val fakeAes = AesCrypt.build(ByteVector("wrong".getBytes()), withIV = false, config = conf)
|
||||
checkCryptoError(fakeAes.decrypt(crypted))
|
||||
|
||||
//we cannot check if first bytes is iv or already data, but encryption goes wrong
|
||||
val aesWithIV = AesCrypt.forString(pass, withIV = true, config = conf)
|
||||
aesWithIV.inverse.unsafe(crypted) shouldNot be(str)
|
||||
val aesWithIV = AesCrypt.build(pass, withIV = true, config = conf)
|
||||
aesWithIV.decrypt(crypted).right.get shouldNot be(str)
|
||||
|
||||
val aesWrongSalt = AesCrypt.forString(pass, withIV = true, config = conf.copy(salt = rndString(10)))
|
||||
checkCryptoError(aesWrongSalt.inverse.runF[Try](crypted))
|
||||
val aesWrongSalt = AesCrypt.build(pass, withIV = true, config = conf.copy(salt = rndString(10)))
|
||||
checkCryptoError(aesWrongSalt.decrypt(crypted))
|
||||
}
|
||||
}
|
||||
|
||||
def checkCryptoError(tr: Try[String])(implicit pos: Position): Assertion = {
|
||||
tr.map(_ ⇒ false)
|
||||
.recover {
|
||||
case e: CryptoError ⇒ true
|
||||
case e ⇒
|
||||
logger.error("Unexpected error", e)
|
||||
false
|
||||
}
|
||||
.get shouldBe true
|
||||
def checkCryptoError(tr: Crypto.Result[Array[Byte]])(implicit pos: Position): Assertion = {
|
||||
tr.isLeft shouldBe true
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -17,18 +17,32 @@
|
||||
|
||||
package fluence.crypto
|
||||
|
||||
import fluence.codec.{CodecError, MonadicalEitherArrow, PureCodec}
|
||||
import cats.data.Kleisli
|
||||
|
||||
object Crypto extends MonadicalEitherArrow[CryptoError] {
|
||||
type Hasher[A, B] = Func[A, B]
|
||||
import scala.util.Try
|
||||
|
||||
type Cipher[A] = Bijection[A, Array[Byte]]
|
||||
object Crypto {
|
||||
type Result[T] = Either[CryptoError, T]
|
||||
|
||||
type KeyPairGenerator = Func[Option[Array[Byte]], KeyPair]
|
||||
type Hasher[A, B] = Kleisli[Result, A, B]
|
||||
|
||||
// TODO: move it to MonadicalEitherArrow, make liftTry with try-catch, and easy conversions for other funcs and bijections
|
||||
implicit val liftCodecErrorToCrypto: CodecError ⇒ CryptoError = err ⇒ CryptoError("Codec error", Some(err))
|
||||
type Func[A, B] = Kleisli[Result, A, B]
|
||||
|
||||
implicit def codec[A, B](implicit codec: PureCodec[A, B]): Bijection[A, B] =
|
||||
Bijection(fromOtherFunc(codec.direct), fromOtherFunc(codec.inverse))
|
||||
case class Cipher[A](
|
||||
encrypt: Kleisli[Result, A, Array[Byte]],
|
||||
decrypt: Kleisli[Result, Array[Byte], A]
|
||||
)
|
||||
|
||||
type KeyPairGenerator = Kleisli[Result, Option[Array[Byte]], KeyPair]
|
||||
|
||||
def apply[A, B](fn: A ⇒ Result[B]): Func[A, B] = Kleisli[Result, A, B](fn)
|
||||
|
||||
def tryFn[A, B](fn: A ⇒ B)(errorText: String): Crypto.Func[A, B] =
|
||||
Crypto(a ⇒ tryUnit(fn(a))(errorText))
|
||||
|
||||
def tryUnit[B](fn: ⇒ B)(errorText: String): Result[B] =
|
||||
Try(fn).toEither.left.map(t ⇒ CryptoError(errorText, Some(t)))
|
||||
|
||||
def cond[B](ifTrue: ⇒ B, errorText: ⇒ String): Crypto.Func[Boolean, B] =
|
||||
Crypto(Either.cond(_, ifTrue, CryptoError(errorText)))
|
||||
}
|
||||
|
@ -17,23 +17,11 @@
|
||||
|
||||
package fluence.crypto
|
||||
|
||||
import cats.Applicative
|
||||
import cats.data.EitherT
|
||||
|
||||
import scala.util.control.{NoStackTrace, NonFatal}
|
||||
import scala.language.higherKinds
|
||||
import scala.util.control.NoStackTrace
|
||||
|
||||
case class CryptoError(message: String, causedBy: Option[Throwable] = None) extends NoStackTrace {
|
||||
override def getMessage: String = message
|
||||
|
||||
override def getCause: Throwable = causedBy getOrElse super.getCause
|
||||
}
|
||||
|
||||
object CryptoError {
|
||||
|
||||
// TODO: there's a common `catchNonFatal` pattern, we should refactor this metod onto it
|
||||
def nonFatalHandling[F[_]: Applicative, A](a: ⇒ A)(errorText: String): EitherT[F, CryptoError, A] =
|
||||
try EitherT.pure(a)
|
||||
catch {
|
||||
case NonFatal(e) ⇒ EitherT.leftT(CryptoError(errorText + ": " + e.getLocalizedMessage, Some(e)))
|
||||
}
|
||||
}
|
||||
|
@ -19,9 +19,9 @@ package fluence.crypto
|
||||
|
||||
import java.security.SecureRandom
|
||||
|
||||
import cats.Monad
|
||||
import cats.data.EitherT
|
||||
import cats.data.Kleisli
|
||||
import fluence.crypto.signature.{SignAlgo, Signature, SignatureChecker, Signer}
|
||||
import cats.syntax.either._
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.language.higherKinds
|
||||
@ -31,26 +31,34 @@ object DumbCrypto {
|
||||
lazy val signAlgo: SignAlgo =
|
||||
SignAlgo(
|
||||
"dumb",
|
||||
Crypto.liftFunc { seedOpt ⇒
|
||||
Kleisli[Crypto.Result, Option[Array[Byte]], KeyPair] { seedOpt ⇒
|
||||
val seed = seedOpt.getOrElse {
|
||||
new SecureRandom().generateSeed(32)
|
||||
}
|
||||
KeyPair.fromBytes(seed, seed)
|
||||
KeyPair.fromBytes(seed, seed).asRight
|
||||
},
|
||||
keyPair ⇒ Signer(keyPair.publicKey, Crypto.liftFunc(plain ⇒ Signature(plain.reverse))),
|
||||
keyPair ⇒
|
||||
Signer(
|
||||
keyPair.publicKey,
|
||||
Kleisli[Crypto.Result, ByteVector, Signature](plain ⇒ Signature(plain.reverse).asRight)
|
||||
),
|
||||
publicKey ⇒
|
||||
new SignatureChecker {
|
||||
override def check[F[_]: Monad](signature: Signature, plain: ByteVector): EitherT[F, CryptoError, Unit] =
|
||||
EitherT.cond[F](signature.sign == plain.reverse, (), CryptoError("Signatures mismatch"))
|
||||
SignatureChecker(
|
||||
Kleisli {
|
||||
case (sgn, msg) ⇒ Either.cond(sgn.sign == msg.reverse, (), CryptoError("Signatures mismatch"))
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
lazy val cipherString: Crypto.Cipher[String] =
|
||||
Crypto.liftB(_.getBytes, bytes ⇒ new String(bytes))
|
||||
Crypto.Cipher(
|
||||
Kleisli[Crypto.Result, String, Array[Byte]](_.getBytes.asRight[CryptoError]),
|
||||
Kleisli[Crypto.Result, Array[Byte], String](bytes ⇒ new String(bytes).asRight[CryptoError])
|
||||
)
|
||||
|
||||
lazy val noOpHasher: Crypto.Hasher[Array[Byte], Array[Byte]] =
|
||||
Crypto.identityFunc[Array[Byte]]
|
||||
Kleisli[Crypto.Result, Array[Byte], Array[Byte]](_.asRight)
|
||||
|
||||
lazy val testHasher: Crypto.Hasher[Array[Byte], Array[Byte]] =
|
||||
Crypto.liftFunc(bytes ⇒ ("H<" + new String(bytes) + ">").getBytes())
|
||||
Kleisli[Crypto.Result, Array[Byte], Array[Byte]](bytes ⇒ ("H<" + new String(bytes) + ">").getBytes().asRight)
|
||||
}
|
||||
|
@ -18,8 +18,10 @@
|
||||
package fluence.crypto.cipher
|
||||
|
||||
import cats.Monad
|
||||
import cats.data.EitherT
|
||||
import fluence.crypto.{Crypto, CryptoError}
|
||||
import cats.data.Kleisli
|
||||
import fluence.crypto.Crypto
|
||||
import cats.instances.either._
|
||||
import cats.syntax.either._
|
||||
|
||||
import scala.collection.Searching.{Found, InsertionPoint, SearchResult}
|
||||
import scala.language.higherKinds
|
||||
@ -41,13 +43,10 @@ object CipherSearch {
|
||||
def binarySearch[A, B](coll: IndexedSeq[A], decrypt: Crypto.Func[A, B])(
|
||||
implicit ordering: Ordering[B]
|
||||
): Crypto.Func[B, SearchResult] =
|
||||
new Crypto.Func[B, SearchResult] {
|
||||
override def apply[F[_]](input: B)(
|
||||
implicit F: Monad[F]
|
||||
): EitherT[F, CryptoError, SearchResult] = {
|
||||
type M[X] = EitherT[F, CryptoError, X]
|
||||
implicitly[Monad[M]].tailRecM((0, coll.length)) {
|
||||
case (from, to) if from == to ⇒ EitherT.rightT(Right(InsertionPoint(from)))
|
||||
Kleisli { input ⇒
|
||||
{
|
||||
implicitly[Monad[Crypto.Result]].tailRecM((0, coll.length)) {
|
||||
case (from, to) if from == to ⇒ Right(InsertionPoint(from)).asRight
|
||||
case (from, to) ⇒
|
||||
val idx = from + (to - from - 1) / 2
|
||||
decrypt(coll(idx)).map { d ⇒
|
||||
|
@ -17,11 +17,8 @@
|
||||
|
||||
package fluence.crypto.signature
|
||||
|
||||
import cats.Monad
|
||||
import cats.data.EitherT
|
||||
import cats.syntax.strong._
|
||||
import cats.syntax.compose._
|
||||
import fluence.crypto.{Crypto, CryptoError, KeyPair}
|
||||
import cats.data.Kleisli
|
||||
import fluence.crypto.{Crypto, KeyPair}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.language.higherKinds
|
||||
@ -38,7 +35,7 @@ case class SignAlgo(
|
||||
name: String,
|
||||
generateKeyPair: Crypto.KeyPairGenerator,
|
||||
signer: SignAlgo.SignerFn,
|
||||
implicit val checker: SignAlgo.CheckerFn,
|
||||
implicit val checker: SignAlgo.CheckerFn
|
||||
)
|
||||
|
||||
object SignAlgo {
|
||||
@ -46,27 +43,13 @@ object SignAlgo {
|
||||
|
||||
type CheckerFn = KeyPair.Public ⇒ SignatureChecker
|
||||
|
||||
/**
|
||||
* Take checker, signature, and plain data, and apply checker, returning Unit on success, or left side error.
|
||||
*/
|
||||
private val fullChecker: Crypto.Func[((SignatureChecker, Signature), ByteVector), Unit] =
|
||||
new Crypto.Func[((SignatureChecker, Signature), ByteVector), Unit] {
|
||||
override def apply[F[_]: Monad](
|
||||
input: ((SignatureChecker, Signature), ByteVector)
|
||||
): EitherT[F, CryptoError, Unit] = {
|
||||
val ((signatureChecker, signature), plainData) = input
|
||||
signatureChecker.check(signature, plainData)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* For CheckerFn, builds a function that takes PubKeyAndSignature along with plain data, and checks the signature.
|
||||
*/
|
||||
def checkerFunc(fn: CheckerFn): Crypto.Func[(PubKeyAndSignature, ByteVector), Unit] =
|
||||
Crypto
|
||||
.liftFunc[PubKeyAndSignature, (SignatureChecker, Signature)] {
|
||||
case PubKeyAndSignature(pk, signature) ⇒ fn(pk) -> signature
|
||||
Kleisli {
|
||||
case (pks, msg) ⇒
|
||||
fn(pks.publicKey).check.run(pks.signature -> msg)
|
||||
}
|
||||
.first[ByteVector] andThen fullChecker
|
||||
|
||||
}
|
||||
|
@ -17,13 +17,10 @@
|
||||
|
||||
package fluence.crypto.signature
|
||||
|
||||
import cats.Monad
|
||||
import cats.data.EitherT
|
||||
import fluence.crypto.CryptoError
|
||||
import cats.data.Kleisli
|
||||
import fluence.crypto.Crypto
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
trait SignatureChecker {
|
||||
def check[F[_]: Monad](signature: Signature, plain: ByteVector): EitherT[F, CryptoError, Unit]
|
||||
}
|
||||
case class SignatureChecker(check: Kleisli[Crypto.Result, (Signature, ByteVector), Unit])
|
||||
|
@ -17,10 +17,10 @@
|
||||
|
||||
package fluence.crypto.signature
|
||||
|
||||
import cats.syntax.profunctor._
|
||||
import cats.instances.either._
|
||||
import fluence.crypto.{Crypto, KeyPair}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
case class Signer(publicKey: KeyPair.Public, sign: Crypto.Func[ByteVector, Signature]) {
|
||||
lazy val signWithPK: Crypto.Func[ByteVector, PubKeyAndSignature] = sign.rmap(PubKeyAndSignature(publicKey, _))
|
||||
lazy val signWithPK: Crypto.Func[ByteVector, PubKeyAndSignature] = sign.map(PubKeyAndSignature(publicKey, _))
|
||||
}
|
||||
|
@ -30,17 +30,17 @@ class CryptoSearchingSpec extends WordSpec with Matchers {
|
||||
val crypt: Crypto.Cipher[String] = DumbCrypto.cipherString
|
||||
|
||||
val plainTextElements = Array("A", "B", "C", "D", "E")
|
||||
val encryptedElements = plainTextElements.map(t ⇒ crypt.direct.unsafe(t))
|
||||
val encryptedElements = plainTextElements.map(t ⇒ crypt.encrypt.run(t).right.get)
|
||||
|
||||
val search = CipherSearch.binarySearch(encryptedElements, crypt.inverse)
|
||||
val search = CipherSearch.binarySearch(encryptedElements, crypt.decrypt)
|
||||
|
||||
search.unsafe("B") shouldBe Found(1)
|
||||
search.unsafe("D") shouldBe Found(3)
|
||||
search.unsafe("E") shouldBe Found(4)
|
||||
search("B").right.get shouldBe Found(1)
|
||||
search("D").right.get shouldBe Found(3)
|
||||
search("E").right.get shouldBe Found(4)
|
||||
|
||||
search.unsafe("0") shouldBe InsertionPoint(0)
|
||||
search.unsafe("BB") shouldBe InsertionPoint(2)
|
||||
search.unsafe("ZZ") shouldBe InsertionPoint(5)
|
||||
search("0").right.get shouldBe InsertionPoint(0)
|
||||
search("BB").right.get shouldBe InsertionPoint(2)
|
||||
search("ZZ").right.get shouldBe InsertionPoint(5)
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -27,11 +27,11 @@ class NoOpCryptSpec extends WordSpec with Matchers {
|
||||
val noOpCrypt = DumbCrypto.cipherString
|
||||
|
||||
val emptyString = ""
|
||||
noOpCrypt.inverse.unsafe(noOpCrypt.direct.unsafe(emptyString)) shouldBe emptyString
|
||||
noOpCrypt.decrypt(noOpCrypt.encrypt(emptyString).right.get).right.get shouldBe emptyString
|
||||
val nonEmptyString = "some text here"
|
||||
noOpCrypt.inverse.unsafe(noOpCrypt.direct.unsafe(nonEmptyString)) shouldBe nonEmptyString
|
||||
noOpCrypt.decrypt(noOpCrypt.encrypt(nonEmptyString).right.get).right.get shouldBe nonEmptyString
|
||||
val byteArray = Array(1.toByte, 23.toByte, 45.toByte)
|
||||
noOpCrypt.direct.unsafe(noOpCrypt.inverse.unsafe(byteArray)) shouldBe byteArray
|
||||
noOpCrypt.encrypt(noOpCrypt.decrypt(byteArray).right.get).right.get shouldBe byteArray
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto
|
||||
|
||||
import io.scalajs.nodejs.buffer.Buffer
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
object CryptoJsHelpers {
|
||||
implicit class ByteVectorOp(bv: ByteVector) {
|
||||
def toJsBuffer: Buffer = Buffer.from(ByteVector(bv.toArray).toHex, "hex")
|
||||
}
|
||||
}
|
@ -17,8 +17,6 @@
|
||||
|
||||
package fluence.crypto.ecdsa
|
||||
|
||||
import cats.Monad
|
||||
import cats.data.EitherT
|
||||
import fluence.crypto._
|
||||
import fluence.crypto.facade.ecdsa.EC
|
||||
import fluence.crypto.hash.JsCryptoHasher
|
||||
@ -35,54 +33,62 @@ import scala.scalajs.js.typedarray.Uint8Array
|
||||
* @param ec implementation of ecdsa logic for different curves
|
||||
*/
|
||||
class Ecdsa(ec: EC, hasher: Option[Crypto.Hasher[Array[Byte], Array[Byte]]]) {
|
||||
import CryptoError.nonFatalHandling
|
||||
|
||||
val generateKeyPair: Crypto.KeyPairGenerator =
|
||||
new Crypto.Func[Option[Array[Byte]], KeyPair] {
|
||||
override def apply[F[_]](input: Option[Array[Byte]])(implicit F: Monad[F]): EitherT[F, CryptoError, KeyPair] =
|
||||
nonFatalHandling {
|
||||
val seedJs = input.map(bs ⇒ js.Dynamic.literal(entropy = bs.toJSArray))
|
||||
val key = ec.genKeyPair(seedJs)
|
||||
val publicHex = key.getPublic(true, "hex")
|
||||
/**
|
||||
* 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)
|
||||
}("Failed to generate key pair.")
|
||||
}
|
||||
}("Incorrect secret key format")
|
||||
|
||||
def sign[F[_]: Monad](keyPair: KeyPair, message: ByteVector): EitherT[F, CryptoError, Signature] =
|
||||
val generateKeyPair: Crypto.KeyPairGenerator =
|
||||
Crypto.tryFn[Option[Array[Byte]], KeyPair] { input ⇒
|
||||
val seedJs = input.map(bs ⇒ js.Dynamic.literal(entropy = bs.toJSArray))
|
||||
val key = ec.genKeyPair(seedJs)
|
||||
val publicHex = key.getPublic(compact = true, "hex")
|
||||
val secretHex = key.getPrivate("hex")
|
||||
val public = ByteVector.fromValidHex(publicHex)
|
||||
val secret = ByteVector.fromValidHex(secretHex)
|
||||
KeyPair.fromByteVectors(public, secret)
|
||||
}("Failed to generate key pair")
|
||||
|
||||
val sign: Crypto.Func[(KeyPair, ByteVector), Signature] =
|
||||
Crypto {
|
||||
case (keyPair: KeyPair, message: ByteVector) ⇒
|
||||
for {
|
||||
secret ← nonFatalHandling {
|
||||
secret ← Crypto.tryUnit {
|
||||
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")
|
||||
}("Cannot get private key from key pair")
|
||||
hash ← JsCryptoHasher.hashJs(message, hasher)
|
||||
signHex ← Crypto.tryUnit(secret.sign(new Uint8Array(hash)).toDER("hex"))("Cannot sign message")
|
||||
} yield Signature(ByteVector.fromValidHex(signHex))
|
||||
|
||||
def 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] =
|
||||
val verify: Crypto.Func[(KeyPair.Public, Signature, ByteVector), Unit] =
|
||||
Crypto {
|
||||
case (
|
||||
pubKey,
|
||||
signature,
|
||||
message
|
||||
) ⇒
|
||||
for {
|
||||
public ← nonFatalHandling {
|
||||
public ← Crypto.tryUnit {
|
||||
val hex = pubKey.value.toHex
|
||||
ec.keyFromPublic(hex, "hex")
|
||||
}("Incorrect public key format.")
|
||||
hash ← hash(message)
|
||||
verify ← nonFatalHandling(public.verify(new Uint8Array(hash), signature.sign.toHex))("Cannot verify message.")
|
||||
_ ← EitherT.cond[F](verify, (), CryptoError("Signature is not verified"))
|
||||
hash ← JsCryptoHasher.hashJs(message, hasher)
|
||||
verify ← Crypto.tryUnit(public.verify(new Uint8Array(hash), signature.sign.toHex))("Cannot verify message")
|
||||
_ ← Either.cond(verify, (), CryptoError("Signature is not verified"))
|
||||
} yield ()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
object Ecdsa {
|
||||
@ -94,20 +100,13 @@ object Ecdsa {
|
||||
signer = kp ⇒
|
||||
Signer(
|
||||
kp.publicKey,
|
||||
new Crypto.Func[ByteVector, signature.Signature] {
|
||||
override def apply[F[_]](
|
||||
input: ByteVector
|
||||
)(implicit F: Monad[F]): EitherT[F, CryptoError, signature.Signature] =
|
||||
ecdsa_secp256k1_sha256.sign(kp, input)
|
||||
}
|
||||
ecdsa_secp256k1_sha256.sign.local(kp -> _)
|
||||
),
|
||||
checker = pk ⇒
|
||||
new SignatureChecker {
|
||||
override def check[F[_]: Monad](
|
||||
signature: fluence.crypto.signature.Signature,
|
||||
plain: ByteVector
|
||||
): EitherT[F, CryptoError, Unit] =
|
||||
ecdsa_secp256k1_sha256.verify(pk, signature, plain)
|
||||
SignatureChecker(
|
||||
ecdsa_secp256k1_sha256.verify.local {
|
||||
case (signature, plain) ⇒ (pk, signature, plain)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
101
hashsign/js/src/main/scala/fluence/crypto/eddsa/Ed25519.scala
Normal file
101
hashsign/js/src/main/scala/fluence/crypto/eddsa/Ed25519.scala
Normal file
@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto.eddsa
|
||||
|
||||
import fluence.crypto.facade.ed25519.Supercop
|
||||
import fluence.crypto.hash.JsCryptoHasher
|
||||
import fluence.crypto.{Crypto, CryptoError, CryptoJsHelpers, KeyPair}
|
||||
import fluence.crypto.signature.{SignAlgo, Signature, SignatureChecker, Signer}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
class Ed25519(hasher: Option[Crypto.Hasher[Array[Byte], Array[Byte]]]) {
|
||||
|
||||
import CryptoJsHelpers._
|
||||
|
||||
val sign: Crypto.Func[(KeyPair, ByteVector), Signature] =
|
||||
Crypto {
|
||||
case (keyPair, message) ⇒
|
||||
for {
|
||||
hash ← JsCryptoHasher.hash(message, hasher)
|
||||
sign ← Crypto.tryUnit {
|
||||
Supercop.sign(
|
||||
ByteVector(hash).toJsBuffer,
|
||||
keyPair.publicKey.value.toJsBuffer,
|
||||
keyPair.secretKey.value.toJsBuffer
|
||||
)
|
||||
}("Error on signing message by js/ed25519 signature")
|
||||
} yield Signature(ByteVector.fromValidHex(sign.toString("hex")))
|
||||
}
|
||||
|
||||
val verify: Crypto.Func[(KeyPair.Public, Signature, ByteVector), Unit] =
|
||||
Crypto {
|
||||
case (
|
||||
pubKey,
|
||||
signature,
|
||||
message
|
||||
) ⇒
|
||||
for {
|
||||
hash ← JsCryptoHasher.hash(message, hasher)
|
||||
verify ← Crypto.tryUnit(
|
||||
Supercop.verify(signature.sign.toJsBuffer, ByteVector(hash).toJsBuffer, pubKey.value.toJsBuffer)
|
||||
)("Cannot verify message")
|
||||
_ ← Either.cond(verify, (), CryptoError("Signature is not verified"))
|
||||
} yield ()
|
||||
}
|
||||
|
||||
val generateKeyPair: Crypto.KeyPairGenerator =
|
||||
Crypto[Option[Array[Byte]], KeyPair] { input ⇒
|
||||
for {
|
||||
seed ← Crypto.tryUnit(input.map(ByteVector(_).toJsBuffer).getOrElse(Supercop.createSeed()))(
|
||||
"Error on seed creation"
|
||||
)
|
||||
jsKeyPair ← Crypto.tryUnit(Supercop.createKeyPair(seed))("Error on key pair generation.")
|
||||
keyPair ← Crypto.tryUnit(
|
||||
KeyPair.fromByteVectors(
|
||||
ByteVector.fromValidHex(jsKeyPair.publicKey.toString("hex")),
|
||||
ByteVector.fromValidHex(jsKeyPair.secretKey.toString("hex"))
|
||||
)
|
||||
)("Error on decoding public and secret keys")
|
||||
} yield keyPair
|
||||
}
|
||||
}
|
||||
|
||||
object Ed25519 {
|
||||
|
||||
val ed25519: Ed25519 = new Ed25519(None)
|
||||
|
||||
val signAlgo: SignAlgo =
|
||||
SignAlgo(
|
||||
name = "ed25519",
|
||||
generateKeyPair = ed25519.generateKeyPair,
|
||||
signer = kp ⇒
|
||||
Signer(
|
||||
kp.publicKey,
|
||||
ed25519.sign.local(kp -> _)
|
||||
),
|
||||
checker = pk ⇒
|
||||
SignatureChecker(
|
||||
ed25519.verify.local {
|
||||
case (signature, plain) ⇒ (pk, signature, plain)
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
}
|
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto.facade
|
||||
|
||||
import scala.scalajs.js
|
||||
import scala.scalajs.js.annotation.JSGlobal
|
||||
|
||||
@js.native
|
||||
@JSGlobal("Buffer")
|
||||
class Buffer(arr: js.Array[Byte]) extends js.Object {
|
||||
def toString(enc: String): String = js.native
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto.facade.ed25519
|
||||
|
||||
import io.scalajs.nodejs.buffer.Buffer
|
||||
|
||||
import scala.scalajs.js
|
||||
import scala.scalajs.js.annotation.JSImport
|
||||
|
||||
@js.native
|
||||
trait KeyPair extends js.Object {
|
||||
val publicKey: Buffer
|
||||
val secretKey: Buffer
|
||||
}
|
||||
|
||||
@js.native
|
||||
@JSImport("supercop.js", JSImport.Namespace)
|
||||
object Supercop extends js.Object {
|
||||
def verify(sig: Buffer, msg: Buffer, pubKey: Buffer): Boolean = js.native
|
||||
def sign(msg: Buffer, pubKey: Buffer, privKey: Buffer): Buffer = js.native
|
||||
def createKeyPair(seed: Buffer): KeyPair = js.native
|
||||
def createSeed(): Buffer = js.native
|
||||
}
|
@ -17,31 +17,54 @@
|
||||
|
||||
package fluence.crypto.hash
|
||||
|
||||
import cats.instances.either._
|
||||
import cats.syntax.either._
|
||||
import fluence.crypto.{Crypto, CryptoError}
|
||||
import fluence.crypto.facade.ecdsa.{SHA1, SHA256}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.language.higherKinds
|
||||
import scala.scalajs.js
|
||||
import scala.scalajs.js.JSConverters._
|
||||
import scala.scalajs.js.typedarray.Uint8Array
|
||||
import scala.util.Try
|
||||
|
||||
object JsCryptoHasher {
|
||||
|
||||
lazy val Sha256: Crypto.Hasher[Array[Byte], Array[Byte]] =
|
||||
Crypto.liftFuncEither[Array[Byte], Array[Byte]] { msg ⇒
|
||||
Try {
|
||||
Crypto.tryFn[Array[Byte], Array[Byte]] { msg ⇒
|
||||
val sha256 = new SHA256()
|
||||
sha256.update(new Uint8Array(msg.toJSArray))
|
||||
ByteVector.fromValidHex(sha256.digest("hex")).toArray
|
||||
}.toEither.left.map(err ⇒ CryptoError("Cannot calculate Sha256 hash", Some(err)))
|
||||
}
|
||||
}("Cannot calculate Sha256 hash")
|
||||
|
||||
lazy val Sha1: Crypto.Hasher[Array[Byte], Array[Byte]] =
|
||||
Crypto.liftFuncEither[Array[Byte], Array[Byte]] { msg ⇒
|
||||
Try {
|
||||
Crypto.tryFn[Array[Byte], Array[Byte]] { msg ⇒
|
||||
val sha1 = new SHA1()
|
||||
sha1.update(new Uint8Array(msg.toJSArray))
|
||||
ByteVector.fromValidHex(sha1.digest("hex")).toArray
|
||||
}.toEither.left.map(err ⇒ CryptoError("Cannot calculate Sha256 hash", Some(err)))
|
||||
}("Cannot calculate Sha256 hash")
|
||||
|
||||
/**
|
||||
* Calculates hash of message.
|
||||
*
|
||||
* @return hash in Scala array
|
||||
*/
|
||||
val hash: Crypto.Func[(ByteVector, Option[Crypto.Hasher[Array[Byte], Array[Byte]]]), Array[Byte]] =
|
||||
Crypto {
|
||||
case (message, hasher) ⇒
|
||||
val arr = message.toArray
|
||||
hasher
|
||||
.fold(arr.asRight[CryptoError]) { h ⇒
|
||||
h(arr)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates hash of message.
|
||||
*
|
||||
* @return hash in JS array
|
||||
*/
|
||||
val hashJs: Crypto.Func[(ByteVector, Option[Crypto.Hasher[Array[Byte], Array[Byte]]]), js.Array[Byte]] =
|
||||
hash
|
||||
.map(_.toJSArray)
|
||||
}
|
||||
|
@ -1,99 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package flyence.crypto
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.instances.try_._
|
||||
import fluence.crypto.ecdsa.Ecdsa
|
||||
import fluence.crypto.signature.Signature
|
||||
import fluence.crypto.{CryptoError, KeyPair}
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.util.{Random, Try}
|
||||
|
||||
class EcdsaSpec extends WordSpec with Matchers {
|
||||
|
||||
def rndBytes(size: Int) = Random.nextString(10).getBytes
|
||||
|
||||
def rndByteVector(size: Int) = ByteVector(rndBytes(size))
|
||||
|
||||
private implicit class TryEitherTExtractor[A <: Throwable, B](et: EitherT[Try, A, B]) {
|
||||
|
||||
def extract: B =
|
||||
et.value.map {
|
||||
case Left(e) ⇒ fail(e) // for making test fail message more describable
|
||||
case Right(v) ⇒ v
|
||||
}.get
|
||||
|
||||
def isOk: Boolean = et.value.fold(_ ⇒ false, _.isRight)
|
||||
}
|
||||
|
||||
"ecdsa algorithm" should {
|
||||
"correct sign and verify data" in {
|
||||
val algorithm = Ecdsa.ecdsa_secp256k1_sha256
|
||||
|
||||
val keys = algorithm.generateKeyPair.unsafe(None)
|
||||
val pubKey = keys.publicKey
|
||||
val data = rndByteVector(10)
|
||||
val sign = algorithm.sign[Try](keys, data).extract
|
||||
|
||||
algorithm.verify[Try](pubKey, sign, data).isOk shouldBe true
|
||||
|
||||
val randomData = rndByteVector(10)
|
||||
val randomSign = algorithm.sign(keys, randomData).extract
|
||||
|
||||
algorithm.verify(pubKey, randomSign, data).isOk shouldBe false
|
||||
|
||||
algorithm.verify(pubKey, sign, randomData).isOk shouldBe false
|
||||
}
|
||||
|
||||
"correctly work with signer and checker" in {
|
||||
val algo = Ecdsa.signAlgo
|
||||
val keys = algo.generateKeyPair.unsafe(None)
|
||||
val signer = algo.signer(keys)
|
||||
val checker = algo.checker(keys.publicKey)
|
||||
|
||||
val data = rndByteVector(10)
|
||||
val sign = signer.sign(data).extract
|
||||
|
||||
checker.check(sign, data).isOk shouldBe true
|
||||
|
||||
val randomSign = signer.sign(rndByteVector(10)).extract
|
||||
checker.check(randomSign, data).isOk shouldBe false
|
||||
}
|
||||
|
||||
"throw an errors on invalid data" in {
|
||||
val algo = Ecdsa.signAlgo
|
||||
val keys = algo.generateKeyPair.unsafe(None)
|
||||
val signer = algo.signer(keys)
|
||||
val checker = algo.checker(keys.publicKey)
|
||||
val data = rndByteVector(10)
|
||||
|
||||
val sign = signer.sign(data).extract
|
||||
|
||||
the[CryptoError] thrownBy checker.check(Signature(rndByteVector(10)), data).value.flatMap(_.toTry).get
|
||||
val invalidChecker = algo.checker(KeyPair.fromByteVectors(rndByteVector(10), rndByteVector(10)).publicKey)
|
||||
the[CryptoError] thrownBy invalidChecker
|
||||
.check(sign, data)
|
||||
.value
|
||||
.flatMap(_.toTry)
|
||||
.get
|
||||
}
|
||||
}
|
||||
}
|
@ -1,64 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package flyence.crypto
|
||||
|
||||
import fluence.crypto.facade.ecdsa.{SHA1, SHA256}
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
import scodec.bits.{Bases, ByteVector}
|
||||
|
||||
import scala.scalajs.js.JSConverters._
|
||||
import scala.scalajs.js.typedarray.Uint8Array
|
||||
|
||||
class JSHashSpec extends WordSpec with Matchers {
|
||||
"js hasher" should {
|
||||
//test values get from third-party hash services
|
||||
"work with sha256" in {
|
||||
val str = "sha256Tester"
|
||||
val sha256TesterHex = "513c17f8cf6ba96ce412cc2ae82f68821e9a2c6ae7a2fb1f5e46d08c387c8e65"
|
||||
|
||||
val hasher = new SHA256()
|
||||
hasher.update(new Uint8Array(str.getBytes().toJSArray))
|
||||
val hex = hasher.digest("hex")
|
||||
hex shouldBe sha256TesterHex
|
||||
}
|
||||
|
||||
"work with sha1" in {
|
||||
val str = "sha1Tester"
|
||||
val sha1TesterHex = "879db20eabcecea7d4736a8bae5bc64564b76b2f"
|
||||
|
||||
val hasher = new SHA1()
|
||||
hasher.update(new Uint8Array(str.getBytes().toJSArray))
|
||||
val hex = hasher.digest("hex")
|
||||
hex shouldBe sha1TesterHex
|
||||
}
|
||||
|
||||
"check unsigned array with sha1" in {
|
||||
|
||||
val arr = Array[Byte](3, -9, -31, 48, 10, 125, 51, -39, -20, -125, 123, 61, -36, 49, 76, 90, -16, 54, -61, 62, 50,
|
||||
-116, -37, -88, -125, -32, -105, 120, 118, 13, -37, 33, -36)
|
||||
|
||||
val base64Check = "9keNwsj08vKTlwIpHAEYvsfpdP4="
|
||||
|
||||
val hasher = new SHA1()
|
||||
hasher.update(new Uint8Array(arr.toJSArray))
|
||||
val hex = hasher.digest("hex")
|
||||
|
||||
ByteVector.fromValidHex(hex, Bases.Alphabets.HexLowercase).toBase64 shouldBe base64Check
|
||||
}
|
||||
}
|
||||
}
|
@ -21,8 +21,8 @@ import java.math.BigInteger
|
||||
import java.security._
|
||||
import java.security.interfaces.ECPrivateKey
|
||||
|
||||
import cats.Monad
|
||||
import cats.data.EitherT
|
||||
import cats.instances.either._
|
||||
import cats.syntax.either._
|
||||
import fluence.crypto.KeyPair.Secret
|
||||
import fluence.crypto.{KeyPair, _}
|
||||
import fluence.crypto.hash.JdkCryptoHasher
|
||||
@ -43,49 +43,25 @@ import scala.language.higherKinds
|
||||
class Ecdsa(curveType: String, scheme: String, hasher: Option[Crypto.Hasher[Array[Byte], Array[Byte]]])
|
||||
extends JavaAlgorithm {
|
||||
|
||||
import CryptoError.nonFatalHandling
|
||||
import Ecdsa._
|
||||
|
||||
val HEXradix = 16
|
||||
|
||||
val generateKeyPair: Crypto.KeyPairGenerator =
|
||||
new Crypto.Func[Option[Array[Byte]], KeyPair] {
|
||||
override def apply[F[_]](
|
||||
input: Option[Array[Byte]]
|
||||
)(implicit F: Monad[F]): EitherT[F, CryptoError, fluence.crypto.KeyPair] =
|
||||
for {
|
||||
ecSpec ← EitherT.fromOption(
|
||||
Option(ECNamedCurveTable.getParameterSpec(curveType)),
|
||||
CryptoError("Parameter spec for the curve is not available.")
|
||||
)
|
||||
g ← getKeyPairGenerator
|
||||
_ ← nonFatalHandling {
|
||||
g.initialize(ecSpec, input.map(new SecureRandom(_)).getOrElse(new SecureRandom()))
|
||||
}(s"Could not initialize KeyPairGenerator")
|
||||
p ← EitherT.fromOption(Option(g.generateKeyPair()), CryptoError("Could not generate KeyPair. Unexpected."))
|
||||
keyPair ← nonFatalHandling {
|
||||
//store S number for private key and compressed Q point on curve for public key
|
||||
val pk = ByteVector(p.getPublic.asInstanceOf[ECPublicKey].getQ.getEncoded(true))
|
||||
val bg = p.getPrivate.asInstanceOf[ECPrivateKey].getS
|
||||
val sk = ByteVector.fromValidHex(bg.toString(HEXradix))
|
||||
KeyPair.fromByteVectors(pk, sk)
|
||||
}("Could not generate KeyPair. Unexpected.")
|
||||
} 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
|
||||
* sk secret key
|
||||
* @return key pair
|
||||
*/
|
||||
def restorePairFromSecret[F[_]: Monad](sk: Secret): EitherT[F, CryptoError, KeyPair] =
|
||||
val restorePairFromSecret: Crypto.Func[Secret, KeyPair] =
|
||||
Crypto(
|
||||
sk ⇒
|
||||
for {
|
||||
ecSpec ← EitherT.fromOption(
|
||||
ecSpec ← Either.fromOption(
|
||||
Option(ECNamedCurveTable.getParameterSpec(curveType)),
|
||||
CryptoError("Parameter spec for the curve is not available.")
|
||||
)
|
||||
keyPair ← nonFatalHandling {
|
||||
keyPair ← Crypto.tryUnit {
|
||||
val hex = sk.value.toHex
|
||||
val d = new BigInteger(hex, HEXradix)
|
||||
// to re-create public key from private we need to multiply known from curve point G with D (private key)
|
||||
@ -97,76 +73,128 @@ class Ecdsa(curveType: String, scheme: String, hasher: Option[Crypto.Hasher[Arra
|
||||
KeyPair.fromByteVectors(pk, sk.value)
|
||||
}("Could not generate KeyPair from private key. Unexpected.")
|
||||
} yield keyPair
|
||||
)
|
||||
|
||||
def sign[F[_]: Monad](
|
||||
keyPair: KeyPair,
|
||||
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 curveSpec: Crypto.Result[ECParameterSpec] =
|
||||
Crypto.tryUnit(ECNamedCurveTable.getParameterSpec(curveType).asInstanceOf[ECParameterSpec])(
|
||||
"Cannot get curve parameters"
|
||||
)
|
||||
|
||||
def verify[F[_]: Monad](
|
||||
publicKey: KeyPair.Public,
|
||||
signature: fluence.crypto.signature.Signature,
|
||||
message: ByteVector
|
||||
): EitherT[F, CryptoError, Unit] =
|
||||
verifySign(publicKey.bytes, signature.bytes, message.toArray)
|
||||
private def getKeyPairGenerator =
|
||||
Crypto.tryUnit(KeyPairGenerator.getInstance(ECDSA, BouncyCastleProvider.PROVIDER_NAME))(
|
||||
"Cannot get key pair generator"
|
||||
)
|
||||
|
||||
private def signMessage[F[_]: Monad](
|
||||
privateKey: BigInteger,
|
||||
message: Array[Byte]
|
||||
): EitherT[F, CryptoError, Array[Byte]] =
|
||||
val generateKeyPair: Crypto.KeyPairGenerator =
|
||||
Crypto[Option[Array[Byte]], KeyPair] { input ⇒
|
||||
for {
|
||||
ecSpec ← Either.fromOption(
|
||||
Option(ECNamedCurveTable.getParameterSpec(curveType)),
|
||||
CryptoError("Parameter spec for the curve is not available.")
|
||||
)
|
||||
g ← getKeyPairGenerator
|
||||
_ ← Crypto.tryUnit {
|
||||
g.initialize(ecSpec, input.map(new SecureRandom(_)).getOrElse(new SecureRandom()))
|
||||
}(s"Could not initialize KeyPairGenerator")
|
||||
p ← Either.fromOption(Option(g.generateKeyPair()), CryptoError("Generated key pair is null"))
|
||||
keyPair ← Crypto.tryUnit {
|
||||
val pk = p.getPublic match {
|
||||
case pk: ECPublicKey => ByteVector(p.getPublic.asInstanceOf[ECPublicKey].getQ.getEncoded(true))
|
||||
case p =>
|
||||
throw new ClassCastException(s"Cannot cast public key (${p.getClass}) to Ed25519PublicKeyParameters")
|
||||
}
|
||||
val sk = p.getPrivate match {
|
||||
case sk: ECPrivateKey =>
|
||||
val bg = p.getPrivate.asInstanceOf[ECPrivateKey].getS
|
||||
ByteVector.fromValidHex(bg.toString(HEXradix))
|
||||
case s =>
|
||||
throw new ClassCastException(s"Cannot cast private key (${p.getClass}) to Ed25519PrivateKeyParameters")
|
||||
}
|
||||
KeyPair.fromByteVectors(pk, sk)
|
||||
}("Could not generate KeyPair")
|
||||
} yield keyPair
|
||||
}
|
||||
|
||||
private def getKeyFactory =
|
||||
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 {
|
||||
ec ← curveSpec
|
||||
keySpec ← nonFatalHandling(new ECPrivateKeySpec(privateKey, ec))("Cannot read private key.")
|
||||
keySpec ← Crypto.tryUnit(new ECPrivateKeySpec(privateKey, ec))("Cannot read private key.")
|
||||
keyFactory ← getKeyFactory
|
||||
signProvider ← getSignatureProvider
|
||||
sign ← nonFatalHandling {
|
||||
signProvider.initSign(keyFactory.generatePrivate(keySpec))
|
||||
signProvider.update(hasher.map(_.unsafe(message)).getOrElse(message))
|
||||
_ ← Crypto.tryUnit(signProvider.initSign(keyFactory.generatePrivate(keySpec)))("Cannot initSign")
|
||||
hash ← hasher.fold(
|
||||
message.asRight[CryptoError]
|
||||
)(_.apply(message))
|
||||
|
||||
sign ← Crypto.tryUnit {
|
||||
signProvider.update(hash)
|
||||
signProvider.sign()
|
||||
}("Cannot sign message.")
|
||||
|
||||
} yield sign
|
||||
}
|
||||
|
||||
private def verifySign[F[_]: Monad](
|
||||
publicKey: Array[Byte],
|
||||
signature: Array[Byte],
|
||||
message: Array[Byte],
|
||||
): EitherT[F, CryptoError, Unit] =
|
||||
val sign: Crypto.Func[(KeyPair, ByteVector), signature.Signature] =
|
||||
signMessage
|
||||
.map(bb ⇒ fluence.crypto.signature.Signature(ByteVector(bb)))
|
||||
.local {
|
||||
case (keyPair, message) ⇒ (new BigInteger(keyPair.secretKey.value.toHex, HEXradix), message.toArray)
|
||||
}
|
||||
|
||||
private val verifySign: Crypto.Func[(Array[Byte], Array[Byte], Array[Byte]), Unit] =
|
||||
Crypto {
|
||||
case (
|
||||
publicKey,
|
||||
signature,
|
||||
message
|
||||
) ⇒
|
||||
for {
|
||||
ec ← curveSpec
|
||||
keySpec ← nonFatalHandling(new ECPublicKeySpec(ec.getCurve.decodePoint(publicKey), ec))("Cannot read public key.")
|
||||
keySpec ← Crypto.tryUnit(new ECPublicKeySpec(ec.getCurve.decodePoint(publicKey), ec))(
|
||||
"Cannot read public key"
|
||||
)
|
||||
keyFactory ← getKeyFactory
|
||||
signProvider ← getSignatureProvider
|
||||
verify ← nonFatalHandling {
|
||||
_ ← Crypto.tryUnit(
|
||||
signProvider.initVerify(keyFactory.generatePublic(keySpec))
|
||||
signProvider.update(hasher.map(_.unsafe(message)).getOrElse(message))
|
||||
signProvider.verify(signature)
|
||||
}("Cannot verify message.")
|
||||
)("Cannot initVerify message")
|
||||
|
||||
_ ← EitherT.cond[F](verify, (), CryptoError("Signature is not verified"))
|
||||
hash ← hasher.fold(
|
||||
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 ()
|
||||
}
|
||||
|
||||
private def curveSpec[F[_]: Monad] =
|
||||
nonFatalHandling(ECNamedCurveTable.getParameterSpec(curveType).asInstanceOf[ECParameterSpec])(
|
||||
"Cannot get curve parameters."
|
||||
)
|
||||
|
||||
private def getKeyPairGenerator[F[_]: Monad] =
|
||||
nonFatalHandling(KeyPairGenerator.getInstance(ECDSA, BouncyCastleProvider.PROVIDER_NAME))(
|
||||
"Cannot get key pair generator."
|
||||
)
|
||||
|
||||
private def getKeyFactory[F[_]: Monad] =
|
||||
nonFatalHandling(KeyFactory.getInstance(ECDSA, BouncyCastleProvider.PROVIDER_NAME))(
|
||||
"Cannot get key factory instance."
|
||||
)
|
||||
|
||||
private def getSignatureProvider[F[_]: Monad] =
|
||||
nonFatalHandling(Signature.getInstance(scheme, BouncyCastleProvider.PROVIDER_NAME))(
|
||||
"Cannot get signature instance."
|
||||
)
|
||||
val verify: Crypto.Func[(KeyPair.Public, signature.Signature, ByteVector), Unit] =
|
||||
verifySign.local {
|
||||
case (
|
||||
publicKey,
|
||||
signature,
|
||||
message
|
||||
) ⇒
|
||||
(publicKey.bytes, signature.bytes, message.toArray)
|
||||
}
|
||||
}
|
||||
|
||||
object Ecdsa {
|
||||
@ -187,20 +215,19 @@ object Ecdsa {
|
||||
signer = kp ⇒
|
||||
Signer(
|
||||
kp.publicKey,
|
||||
new Crypto.Func[ByteVector, signature.Signature] {
|
||||
override def apply[F[_]](
|
||||
input: ByteVector
|
||||
)(implicit F: Monad[F]): EitherT[F, CryptoError, signature.Signature] =
|
||||
ecdsa_secp256k1_sha256.sign(kp, input)
|
||||
Crypto { input ⇒
|
||||
ecdsa_secp256k1_sha256.sign(kp -> input)
|
||||
}
|
||||
),
|
||||
checker = pk ⇒
|
||||
new SignatureChecker {
|
||||
override def check[F[_]: Monad](
|
||||
SignatureChecker(
|
||||
Crypto {
|
||||
case (
|
||||
signature: fluence.crypto.signature.Signature,
|
||||
plain: ByteVector
|
||||
): EitherT[F, CryptoError, Unit] =
|
||||
) ⇒
|
||||
ecdsa_secp256k1_sha256.verify(pk, signature, plain)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
170
hashsign/jvm/src/main/scala/fluence/crypto/eddsa/Ed25519.scala
Normal file
170
hashsign/jvm/src/main/scala/fluence/crypto/eddsa/Ed25519.scala
Normal file
@ -0,0 +1,170 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto.eddsa
|
||||
|
||||
import java.security._
|
||||
|
||||
import cats.syntax.either._
|
||||
import cats.instances.either._
|
||||
import cats.syntax.functor._
|
||||
import fluence.crypto.KeyPair.Secret
|
||||
import fluence.crypto.signature.{SignAlgo, SignatureChecker, Signer}
|
||||
import fluence.crypto.{KeyPair, _}
|
||||
import org.bouncycastle.crypto.{AsymmetricCipherKeyPair, KeyGenerationParameters}
|
||||
import org.bouncycastle.crypto.generators.Ed25519KeyPairGenerator
|
||||
import org.bouncycastle.crypto.params.{Ed25519PrivateKeyParameters, Ed25519PublicKeyParameters}
|
||||
import org.bouncycastle.crypto.signers.Ed25519Signer
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
/**
|
||||
* Edwards-curve Digital Signature Algorithm (EdDSA)
|
||||
*/
|
||||
class Ed25519(strength: Int) extends JavaAlgorithm {
|
||||
|
||||
/**
|
||||
* Restores pair of keys from the known secret key.
|
||||
* The public key will be the same each method call with the same secret key.
|
||||
* sk secret key
|
||||
* @return key pair
|
||||
*/
|
||||
val restorePairFromSecret: Crypto.Func[Secret, KeyPair] =
|
||||
Crypto.tryFn[Secret, KeyPair] { sk ⇒
|
||||
val secret = new Ed25519PrivateKeyParameters(sk.bytes, 0)
|
||||
KeyPair.fromBytes(secret.generatePublicKey().getEncoded, sk.bytes)
|
||||
}("Could not generate KeyPair from private key")
|
||||
|
||||
private val signMessage: Crypto.Func[(Array[Byte], Array[Byte]), Array[Byte]] =
|
||||
Crypto.tryFn[(Array[Byte], Array[Byte]), Array[Byte]] {
|
||||
case (
|
||||
privateKey,
|
||||
message
|
||||
) ⇒
|
||||
val privKey = new Ed25519PrivateKeyParameters(privateKey, 0)
|
||||
val signer = new Ed25519Signer
|
||||
signer.init(true, privKey)
|
||||
signer.update(message, 0, message.length)
|
||||
signer.generateSignature()
|
||||
}("Cannot sign message")
|
||||
|
||||
val sign: Crypto.Func[(KeyPair, ByteVector), signature.Signature] =
|
||||
signMessage
|
||||
.map(bb ⇒ fluence.crypto.signature.Signature(ByteVector(bb)))
|
||||
.local {
|
||||
case (keyPair, message) ⇒
|
||||
keyPair.secretKey.bytes -> message.toArray
|
||||
}
|
||||
|
||||
private val verifySign: Crypto.Func[(Array[Byte], Array[Byte], Array[Byte]), Unit] =
|
||||
Crypto.tryFn[(Array[Byte], Array[Byte], Array[Byte]), Boolean] {
|
||||
case (
|
||||
publicKey,
|
||||
signature,
|
||||
message
|
||||
) ⇒
|
||||
val pubKey = new Ed25519PublicKeyParameters(publicKey, 0)
|
||||
val signer = new Ed25519Signer
|
||||
signer.init(false, pubKey)
|
||||
signer.update(message, 0, message.length)
|
||||
signer.verifySignature(signature)
|
||||
}("Cannot verify message") andThen Crypto.cond((), "Signature is not verified")
|
||||
|
||||
val verify: Crypto.Func[(KeyPair.Public, signature.Signature, ByteVector), Unit] =
|
||||
verifySign.local {
|
||||
case (
|
||||
publicKey,
|
||||
signature,
|
||||
message
|
||||
) ⇒
|
||||
(publicKey.bytes, signature.bytes, message.toArray)
|
||||
}
|
||||
|
||||
private def getKeyPairGenerator =
|
||||
Crypto.tryUnit(
|
||||
new Ed25519KeyPairGenerator()
|
||||
)(
|
||||
"Cannot get key pair generator"
|
||||
)
|
||||
|
||||
val generateKeyPair: Crypto.KeyPairGenerator =
|
||||
Crypto[Option[Array[Byte]], KeyPair] { input ⇒
|
||||
getKeyPairGenerator.flatMap { g ⇒
|
||||
val random = input.map(new SecureRandom(_)).getOrElse(new SecureRandom())
|
||||
val keyParameters = new KeyGenerationParameters(random, strength)
|
||||
g.init(keyParameters)
|
||||
Either.fromOption(Option(g.generateKeyPair()), CryptoError("Generated keypair is null"))
|
||||
}.flatMap(
|
||||
(p: AsymmetricCipherKeyPair) ⇒
|
||||
Crypto.tryUnit {
|
||||
val pk = p.getPublic match {
|
||||
case pk: Ed25519PublicKeyParameters => pk.getEncoded
|
||||
case p =>
|
||||
throw new ClassCastException(s"Cannot cast public key (${p.getClass}) to Ed25519PublicKeyParameters")
|
||||
}
|
||||
val sk = p.getPrivate match {
|
||||
case sk: Ed25519PrivateKeyParameters => sk.getEncoded
|
||||
case s =>
|
||||
throw new ClassCastException(s"Cannot cast private key (${p.getClass}) to Ed25519PrivateKeyParameters")
|
||||
}
|
||||
KeyPair.fromBytes(pk, sk)
|
||||
}("Could not generate KeyPair")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
object Ed25519 {
|
||||
|
||||
/**
|
||||
* Keys in tendermint are generating with a random seed of 32 bytes
|
||||
*/
|
||||
val ed25519 = new Ed25519(256)
|
||||
val signAlgo: SignAlgo = signAlgoInit(256)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param strength the size, in bits, of the keys we want to produce
|
||||
*/
|
||||
def ed25519Init(strength: Int) = new Ed25519(strength)
|
||||
|
||||
/**
|
||||
*
|
||||
* @param strength the size, in bits, of the keys we want to produce
|
||||
*/
|
||||
def signAlgoInit(strength: Int): SignAlgo = {
|
||||
val algo = ed25519Init(strength)
|
||||
SignAlgo(
|
||||
name = "ed25519",
|
||||
generateKeyPair = algo.generateKeyPair,
|
||||
signer = kp ⇒
|
||||
Signer(
|
||||
kp.publicKey,
|
||||
Crypto[ByteVector, signature.Signature] { input ⇒
|
||||
algo.sign(kp -> input)
|
||||
}
|
||||
),
|
||||
checker = pk ⇒
|
||||
SignatureChecker(
|
||||
Crypto {
|
||||
case (signature, plain) ⇒
|
||||
algo.verify(pk, signature, plain)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ object JdkCryptoHasher {
|
||||
* [[https://docs.oracle.com/javase/7/docs/technotes/guides/security/StandardNames.html#MessageDigest]]
|
||||
*/
|
||||
def apply(algorithm: String): Crypto.Hasher[Array[Byte], Array[Byte]] =
|
||||
Crypto.liftFuncEither(
|
||||
Crypto(
|
||||
bytes ⇒
|
||||
Try(MessageDigest.getInstance(algorithm).digest(bytes)).toEither.left
|
||||
.map(err ⇒ CryptoError(s"Cannot get $algorithm hash", Some(err)))
|
||||
|
@ -0,0 +1,45 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto
|
||||
|
||||
import fluence.crypto.ecdsa.Ecdsa
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.util.Random
|
||||
|
||||
class JvmEcdsaSpec extends WordSpec with Matchers {
|
||||
|
||||
def rndBytes(size: Int): Array[Byte] = Random.nextString(10).getBytes
|
||||
|
||||
def rndByteVector(size: Int) = ByteVector(rndBytes(size))
|
||||
|
||||
"jvm ecdsa algorithm" should {
|
||||
|
||||
"restore key pair from secret key" in {
|
||||
val algo = Ecdsa.signAlgo
|
||||
val testKeys = algo.generateKeyPair(None).right.get
|
||||
|
||||
val ecdsa = Ecdsa.ecdsa_secp256k1_sha256
|
||||
|
||||
val newKeys = ecdsa.restorePairFromSecret(testKeys.secretKey).right.get
|
||||
|
||||
testKeys shouldBe newKeys
|
||||
}
|
||||
}
|
||||
}
|
@ -1,141 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto
|
||||
|
||||
import java.io.File
|
||||
import java.math.BigInteger
|
||||
|
||||
import cats.data.EitherT
|
||||
import cats.instances.try_._
|
||||
import fluence.crypto.ecdsa.Ecdsa
|
||||
import fluence.crypto.keystore.FileKeyStorage
|
||||
import fluence.crypto.signature.Signature
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.util.{Random, Try}
|
||||
|
||||
class SignatureSpec extends WordSpec with Matchers {
|
||||
|
||||
def rndBytes(size: Int): Array[Byte] = Random.nextString(10).getBytes
|
||||
|
||||
def rndByteVector(size: Int) = ByteVector(rndBytes(size))
|
||||
|
||||
private implicit class TryEitherTExtractor[A <: Throwable, B](et: EitherT[Try, A, B]) {
|
||||
|
||||
def extract: B =
|
||||
et.value.map {
|
||||
case Left(e) ⇒ fail(e) // for making test fail message more describable
|
||||
case Right(v) ⇒ v
|
||||
}.get
|
||||
|
||||
def isOk: Boolean = et.value.fold(_ ⇒ false, _.isRight)
|
||||
}
|
||||
|
||||
"ecdsa algorithm" should {
|
||||
"correct sign and verify data" in {
|
||||
val algorithm = Ecdsa.ecdsa_secp256k1_sha256
|
||||
|
||||
val keys = algorithm.generateKeyPair.unsafe(None)
|
||||
val pubKey = keys.publicKey
|
||||
val data = rndByteVector(10)
|
||||
val sign = algorithm.sign[Try](keys, data).extract
|
||||
|
||||
algorithm.verify[Try](pubKey, sign, data).isOk shouldBe true
|
||||
|
||||
val randomData = rndByteVector(10)
|
||||
val randomSign = algorithm.sign(keys, randomData).extract
|
||||
|
||||
algorithm.verify(pubKey, randomSign, data).isOk shouldBe false
|
||||
|
||||
algorithm.verify(pubKey, sign, randomData).isOk shouldBe false
|
||||
}
|
||||
|
||||
"correctly work with signer and checker" in {
|
||||
val algo = Ecdsa.signAlgo
|
||||
val keys = algo.generateKeyPair.unsafe(None)
|
||||
val signer = algo.signer(keys)
|
||||
val checker = algo.checker(keys.publicKey)
|
||||
|
||||
val data = rndByteVector(10)
|
||||
val sign = signer.sign(data).extract
|
||||
|
||||
checker.check(sign, data).isOk shouldBe true
|
||||
|
||||
val randomSign = signer.sign(rndByteVector(10)).extract
|
||||
checker.check(randomSign, data).isOk shouldBe false
|
||||
}
|
||||
|
||||
"throw an errors on invalid data" in {
|
||||
val algo = Ecdsa.signAlgo
|
||||
val keys = algo.generateKeyPair.unsafe(None)
|
||||
val signer = algo.signer(keys)
|
||||
val checker = algo.checker(keys.publicKey)
|
||||
val data = rndByteVector(10)
|
||||
|
||||
val sign = signer.sign(data).extract
|
||||
|
||||
the[CryptoError] thrownBy {
|
||||
checker.check(Signature(rndByteVector(10)), data).value.flatMap(_.toTry).get
|
||||
}
|
||||
val invalidChecker = algo.checker(KeyPair.fromByteVectors(rndByteVector(10), rndByteVector(10)).publicKey)
|
||||
the[CryptoError] thrownBy {
|
||||
invalidChecker
|
||||
.check(sign, data)
|
||||
.value
|
||||
.flatMap(_.toTry)
|
||||
.get
|
||||
}
|
||||
}
|
||||
|
||||
"store and read key from file" in {
|
||||
val algo = Ecdsa.signAlgo
|
||||
val keys = algo.generateKeyPair.unsafe(None)
|
||||
|
||||
val keyFile = File.createTempFile("test", "")
|
||||
if (keyFile.exists()) keyFile.delete()
|
||||
val storage = new FileKeyStorage(keyFile)
|
||||
|
||||
storage.storeKeyPair(keys).unsafeRunSync()
|
||||
|
||||
val keysReadE = storage.readKeyPair
|
||||
val keysRead = keysReadE.unsafeRunSync()
|
||||
|
||||
val signer = algo.signer(keys)
|
||||
val data = rndByteVector(10)
|
||||
val sign = signer.sign(data).extract
|
||||
|
||||
algo.checker(keys.publicKey).check(sign, data).isOk shouldBe true
|
||||
algo.checker(keysRead.publicKey).check(sign, data).isOk shouldBe true
|
||||
|
||||
//try to store key into previously created file
|
||||
storage.storeKeyPair(keys).attempt.unsafeRunSync().isLeft shouldBe true
|
||||
}
|
||||
|
||||
"restore key pair from secret key" in {
|
||||
val algo = Ecdsa.signAlgo
|
||||
val testKeys = algo.generateKeyPair.unsafe(None)
|
||||
|
||||
val ecdsa = Ecdsa.ecdsa_secp256k1_sha256
|
||||
|
||||
val newKeys = ecdsa.restorePairFromSecret(testKeys.secretKey).extract
|
||||
|
||||
testKeys shouldBe newKeys
|
||||
}
|
||||
}
|
||||
}
|
88
hashsign/src/test/scala/fluence/crypto/EcdsaSpec.scala
Normal file
88
hashsign/src/test/scala/fluence/crypto/EcdsaSpec.scala
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto
|
||||
|
||||
import fluence.crypto.ecdsa.Ecdsa
|
||||
import fluence.crypto.signature.{SignAlgo, Signature}
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.util.{Random, Try}
|
||||
|
||||
class EcdsaSpec extends WordSpec with Matchers {
|
||||
|
||||
def rndBytes(size: Int): Array[Byte] = Random.nextString(10).getBytes
|
||||
|
||||
def rndByteVector(size: Int) = ByteVector(rndBytes(size))
|
||||
|
||||
"ecdsa algorithm" should {
|
||||
"correct sign and verify data" in {
|
||||
val algorithm: SignAlgo = Ecdsa.signAlgo
|
||||
|
||||
val keys = algorithm.generateKeyPair(None).right.get
|
||||
val pubKey = keys.publicKey
|
||||
val data = rndByteVector(10)
|
||||
val sign = algorithm.signer(keys).sign(data).right.get
|
||||
|
||||
algorithm.checker(pubKey).check((sign, data)).isRight shouldBe true
|
||||
|
||||
val randomData = rndByteVector(10)
|
||||
val randomSign = algorithm.signer(keys).sign(randomData).right.get
|
||||
|
||||
algorithm.checker(pubKey).check(randomSign -> data).isRight shouldBe false
|
||||
|
||||
algorithm.checker(pubKey).check(sign -> randomData).isRight shouldBe false
|
||||
}
|
||||
|
||||
"correctly work with signer and checker" in {
|
||||
val algo: SignAlgo = Ecdsa.signAlgo
|
||||
val keys = algo.generateKeyPair(None).right.get
|
||||
val signer = algo.signer(keys)
|
||||
val checker = algo.checker(keys.publicKey)
|
||||
|
||||
val data = rndByteVector(10)
|
||||
val sign = signer.sign(data).right.get
|
||||
|
||||
checker.check(sign -> data).isRight shouldBe true
|
||||
|
||||
val randomSign = signer.sign(rndByteVector(10)).right.get
|
||||
checker.check(randomSign -> data).isRight shouldBe false
|
||||
}
|
||||
|
||||
"throw an errors on invalid data" in {
|
||||
val algo: SignAlgo = Ecdsa.signAlgo
|
||||
val keys = algo.generateKeyPair(None).right.get
|
||||
val signer = algo.signer(keys)
|
||||
val checker = algo.checker(keys.publicKey)
|
||||
val data = rndByteVector(10)
|
||||
|
||||
val sign = signer.sign(data).right.get
|
||||
|
||||
the[CryptoError] thrownBy {
|
||||
checker.check(Signature(rndByteVector(10)) -> data).toTry.get
|
||||
}
|
||||
val invalidChecker = algo.checker(KeyPair.fromByteVectors(rndByteVector(10), rndByteVector(10)).publicKey)
|
||||
the[CryptoError] thrownBy {
|
||||
invalidChecker
|
||||
.check(sign -> data)
|
||||
.toTry
|
||||
.get
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
88
hashsign/src/test/scala/fluence/crypto/Ed25519Spec.scala
Normal file
88
hashsign/src/test/scala/fluence/crypto/Ed25519Spec.scala
Normal file
@ -0,0 +1,88 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto
|
||||
|
||||
import fluence.crypto.eddsa.Ed25519
|
||||
import fluence.crypto.signature.{SignAlgo, Signature}
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.util.{Random, Try}
|
||||
|
||||
class Ed25519Spec extends WordSpec with Matchers {
|
||||
|
||||
def rndBytes(size: Int): Array[Byte] = Random.nextString(10).getBytes
|
||||
|
||||
def rndByteVector(size: Int) = ByteVector(rndBytes(size))
|
||||
|
||||
"ed25519 algorithm" should {
|
||||
"correct sign and verify data" in {
|
||||
val algorithm: SignAlgo = Ed25519.signAlgo
|
||||
|
||||
val keys = algorithm.generateKeyPair(None).right.get
|
||||
val pubKey = keys.publicKey
|
||||
val data = rndByteVector(10)
|
||||
val sign = algorithm.signer(keys).sign(data).right.get
|
||||
|
||||
algorithm.checker(pubKey).check(sign -> data).isRight shouldBe true
|
||||
|
||||
val randomData = rndByteVector(10)
|
||||
val randomSign = algorithm.signer(keys).sign(randomData).right.get
|
||||
|
||||
algorithm.checker(pubKey).check(randomSign -> data).contains(true) shouldBe false
|
||||
|
||||
algorithm.checker(pubKey).check(sign -> randomData).contains(true) shouldBe false
|
||||
}
|
||||
|
||||
"correctly work with signer and checker" in {
|
||||
val algo: SignAlgo = Ed25519.signAlgo
|
||||
val keys = algo.generateKeyPair(None).right.get
|
||||
val signer = algo.signer(keys)
|
||||
val checker = algo.checker(keys.publicKey)
|
||||
|
||||
val data = rndByteVector(10)
|
||||
val sign = signer.sign(data).right.get
|
||||
|
||||
checker.check(sign -> data).isRight shouldBe true
|
||||
|
||||
val randomSign = signer.sign(rndByteVector(10)).right.get
|
||||
checker.check(randomSign, data).contains(true) shouldBe false
|
||||
}
|
||||
|
||||
"throw an errors on invalid data" in {
|
||||
val algo: SignAlgo = Ed25519.signAlgo
|
||||
val keys = algo.generateKeyPair(None).right.get
|
||||
val signer = algo.signer(keys)
|
||||
val checker = algo.checker(keys.publicKey)
|
||||
val data = rndByteVector(10)
|
||||
|
||||
val sign = signer.sign(data).right.get
|
||||
|
||||
the[CryptoError] thrownBy {
|
||||
checker.check(Signature(rndByteVector(10)), data).toTry.get
|
||||
}
|
||||
val invalidChecker = algo.checker(KeyPair.fromByteVectors(rndByteVector(10), rndByteVector(10)).publicKey)
|
||||
the[CryptoError] thrownBy {
|
||||
invalidChecker
|
||||
.check(sign, data)
|
||||
.toTry
|
||||
.get
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -17,27 +17,27 @@
|
||||
|
||||
package fluence.crypto
|
||||
|
||||
import fluence.crypto.hash.JdkCryptoHasher
|
||||
import fluence.crypto.hash.CryptoHashers
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
class JvmHashSpec extends WordSpec with Matchers {
|
||||
class HashSpec extends WordSpec with Matchers {
|
||||
"jvm hasher" should {
|
||||
//test values get from third-party hash services
|
||||
"work with sha256" in {
|
||||
val str = "sha256Tester"
|
||||
val sha256TesterHex = "513c17f8cf6ba96ce412cc2ae82f68821e9a2c6ae7a2fb1f5e46d08c387c8e65"
|
||||
|
||||
val hasher = JdkCryptoHasher.Sha256
|
||||
ByteVector(hasher.unsafe(str.getBytes())).toHex shouldBe sha256TesterHex
|
||||
val hasher = CryptoHashers.Sha256
|
||||
ByteVector(hasher(str.getBytes()).right.get).toHex shouldBe sha256TesterHex
|
||||
}
|
||||
|
||||
"work with sha1" in {
|
||||
val str = "sha1Tester"
|
||||
val sha1TesterHex = "879db20eabcecea7d4736a8bae5bc64564b76b2f"
|
||||
|
||||
val hasher = JdkCryptoHasher.Sha1
|
||||
ByteVector(hasher.unsafe(str.getBytes())).toHex shouldBe sha1TesterHex
|
||||
val hasher = CryptoHashers.Sha1
|
||||
ByteVector(hasher(str.getBytes()).right.get).toHex shouldBe sha1TesterHex
|
||||
}
|
||||
|
||||
"check unsigned array with sha1" in {
|
||||
@ -47,9 +47,9 @@ class JvmHashSpec extends WordSpec with Matchers {
|
||||
|
||||
val base64Check = "9keNwsj08vKTlwIpHAEYvsfpdP4="
|
||||
|
||||
val hasher = JdkCryptoHasher.Sha1
|
||||
val hasher = CryptoHashers.Sha1
|
||||
|
||||
ByteVector(hasher.unsafe(arr)).toBase64 shouldBe base64Check
|
||||
ByteVector(hasher(arr).right.get).toBase64 shouldBe base64Check
|
||||
}
|
||||
}
|
||||
}
|
@ -1,117 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto.jwt
|
||||
|
||||
import cats.syntax.compose._
|
||||
import cats.syntax.flatMap._
|
||||
import cats.syntax.functor._
|
||||
import fluence.codec.bits.BitsCodecs
|
||||
import fluence.codec.bits.BitsCodecs._
|
||||
import fluence.codec.{CodecError, PureCodec}
|
||||
import fluence.codec.circe.CirceCodecs
|
||||
import fluence.crypto.{Crypto, CryptoError, KeyPair}
|
||||
import fluence.crypto.signature.SignAlgo.CheckerFn
|
||||
import fluence.crypto.signature.{PubKeyAndSignature, SignAlgo, Signature, Signer}
|
||||
import io.circe.{Decoder, Encoder}
|
||||
import scodec.bits.{Bases, ByteVector}
|
||||
|
||||
/**
|
||||
* Primitivized version of JWT.
|
||||
*
|
||||
* @param readPubKey Gets public key from decoded Header and Claim
|
||||
* @tparam H Header type
|
||||
* @tparam C Claim type
|
||||
*/
|
||||
class CryptoJwt[H: Encoder: Decoder, C: Encoder: Decoder](
|
||||
readPubKey: PureCodec.Func[(H, C), KeyPair.Public]
|
||||
) {
|
||||
// Public Key reader, with errors lifted to Crypto
|
||||
private val readPK = Crypto.fromOtherFunc(readPubKey)(Crypto.liftCodecErrorToCrypto)
|
||||
|
||||
private val headerAndClaimCodec = Crypto.codec(CryptoJwt.headerClaimCodec[H, C])
|
||||
|
||||
private val signatureCodec = Crypto.codec(CryptoJwt.signatureCodec)
|
||||
|
||||
private val stringTripleCodec = Crypto.codec(CryptoJwt.stringTripleCodec)
|
||||
|
||||
// Take a JWT string, parse and deserialize it, check signature, return Header and Claim on success
|
||||
def reader(checkerFn: CheckerFn): Crypto.Func[String, (H, C)] =
|
||||
Crypto.liftFuncPoint[String, (H, C)](
|
||||
jwtToken ⇒
|
||||
for {
|
||||
triple ← stringTripleCodec.inverse.pointAt(jwtToken)
|
||||
(hc @ (h, c), s) = triple
|
||||
headerAndClaim ← headerAndClaimCodec.inverse.pointAt(hc)
|
||||
pk ← readPK.pointAt(headerAndClaim)
|
||||
signature ← signatureCodec.inverse.pointAt(s)
|
||||
plainData = ByteVector((h + c).getBytes())
|
||||
_ ← SignAlgo.checkerFunc(checkerFn).pointAt(PubKeyAndSignature(pk, signature) → plainData)
|
||||
} yield headerAndClaim
|
||||
)
|
||||
|
||||
// With the given Signer, serialize Header and Claim into JWT string, signing it on the way
|
||||
def writer(signer: Signer): Crypto.Func[(H, C), String] =
|
||||
Crypto.liftFuncPoint[(H, C), String](
|
||||
headerAndClaim ⇒
|
||||
for {
|
||||
pk ← readPK.pointAt(headerAndClaim)
|
||||
_ ← Crypto
|
||||
.liftFuncEither[Boolean, Unit](
|
||||
Either.cond(_, (), CryptoError("JWT encoded PublicKey doesn't match with signer's PublicKey"))
|
||||
)
|
||||
.pointAt(pk == signer.publicKey)
|
||||
hc ← headerAndClaimCodec.direct.pointAt(headerAndClaim)
|
||||
plainData = ByteVector((hc._1 + hc._2).getBytes())
|
||||
signature ← signer.sign.pointAt(plainData)
|
||||
s ← signatureCodec.direct.pointAt(signature)
|
||||
jwtToken ← stringTripleCodec.direct.pointAt((hc, s))
|
||||
} yield jwtToken
|
||||
)
|
||||
}
|
||||
|
||||
object CryptoJwt {
|
||||
|
||||
private val alphabet = Bases.Alphabets.Base64Url
|
||||
|
||||
private val strVec = BitsCodecs.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]
|
||||
}
|
@ -1,82 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto.jwt
|
||||
|
||||
import cats.{Id, Monad}
|
||||
import cats.data.EitherT
|
||||
import fluence.codec.PureCodec
|
||||
import fluence.crypto.signature.{SignAlgo, Signature, SignatureChecker, Signer}
|
||||
import fluence.crypto.{Crypto, CryptoError, KeyPair}
|
||||
import io.circe.{Json, JsonNumber, JsonObject}
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
import scodec.bits.ByteVector
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
class CryptoJwtSpec extends WordSpec with Matchers {
|
||||
"CryptoJwt" should {
|
||||
val keys: Seq[KeyPair] =
|
||||
Stream.from(0).map(ByteVector.fromInt(_)).map(i ⇒ KeyPair.fromByteVectors(i, i))
|
||||
|
||||
val cryptoJwt =
|
||||
new CryptoJwt[JsonNumber, JsonObject](
|
||||
PureCodec.liftFunc(n ⇒ KeyPair.Public(ByteVector.fromInt(n._1.toInt.get)))
|
||||
)
|
||||
|
||||
implicit val checker: SignAlgo.CheckerFn =
|
||||
publicKey ⇒
|
||||
new SignatureChecker {
|
||||
override def check[F[_]: Monad](signature: Signature, plain: ByteVector): EitherT[F, CryptoError, Unit] =
|
||||
EitherT.cond[F](signature.sign == plain.reverse, (), CryptoError("Signatures mismatch"))
|
||||
}
|
||||
|
||||
val signer: SignAlgo.SignerFn =
|
||||
keyPair ⇒ Signer(keyPair.publicKey, Crypto.liftFunc(plain ⇒ Signature(plain.reverse)))
|
||||
|
||||
"be a total bijection for valid JWT" in {
|
||||
val kp = keys.head
|
||||
val str = cryptoJwt
|
||||
.writer(signer(kp))
|
||||
.unsafe((JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" → Json.fromString("value. of test"))))
|
||||
|
||||
str.count(_ == '.') shouldBe 2
|
||||
|
||||
cryptoJwt.reader(checker).unsafe(str)._2.kleisli.run("test").get shouldBe Json.fromString("value. of test")
|
||||
}
|
||||
|
||||
"fail with wrong signer" in {
|
||||
val kp = keys.tail.head
|
||||
val str = cryptoJwt
|
||||
.writer(signer(kp))
|
||||
.runEither[Id](
|
||||
(JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" → Json.fromString("value of test")))
|
||||
)
|
||||
|
||||
str.isLeft shouldBe true
|
||||
}
|
||||
|
||||
"fail with wrong signature" in {
|
||||
val kp = keys.head
|
||||
val str = cryptoJwt
|
||||
.writer(signer(kp))
|
||||
.unsafe((JsonNumber.fromIntegralStringUnsafe("0"), JsonObject("test" → Json.fromString("value of test"))))
|
||||
|
||||
cryptoJwt.reader(checker).runEither[Id](str + "m").isLeft shouldBe true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto.keystore
|
||||
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
import cats.syntax.applicativeError._
|
||||
import cats.effect.IO
|
||||
import fluence.codec.PureCodec
|
||||
import fluence.crypto.{signature, KeyPair}
|
||||
|
||||
import scala.language.higherKinds
|
||||
import scala.util.control.NonFatal
|
||||
|
||||
/**
|
||||
* File based storage for crypto keys.
|
||||
*
|
||||
* @param file Path to keys in file system
|
||||
*/
|
||||
class FileKeyStorage(file: File) extends slogging.LazyLogging {
|
||||
import KeyStore._
|
||||
|
||||
private val codec = PureCodec[KeyPair, String]
|
||||
|
||||
private val readFile: IO[String] =
|
||||
IO(Files.readAllBytes(file.toPath)).map(new String(_))
|
||||
|
||||
val readKeyPair: IO[KeyPair] = readFile.flatMap(codec.inverse.runF[IO])
|
||||
|
||||
private def writeFile(data: String): IO[Unit] = IO {
|
||||
logger.info("Storing secret key to file: " + file)
|
||||
if (!file.getParentFile.exists()) {
|
||||
logger.info(s"Parent directory does not exist: ${file.getParentFile}, trying to create")
|
||||
Files.createDirectories(file.getParentFile.toPath)
|
||||
}
|
||||
if (!file.exists()) file.createNewFile() else throw new RuntimeException(file.getAbsolutePath + " already exists")
|
||||
Files.write(file.toPath, data.getBytes)
|
||||
}
|
||||
|
||||
def storeKeyPair(keyPair: KeyPair): IO[Unit] =
|
||||
codec.direct.runF[IO](keyPair).flatMap(writeFile)
|
||||
|
||||
def readOrCreateKeyPair(createKey: IO[KeyPair]): IO[KeyPair] =
|
||||
readKeyPair.recoverWith {
|
||||
case NonFatal(e) ⇒
|
||||
logger.debug(s"KeyPair can't be loaded from $file, going to generate new keys", e)
|
||||
for {
|
||||
ks ← createKey
|
||||
_ ← storeKeyPair(ks)
|
||||
} yield ks
|
||||
}
|
||||
}
|
||||
|
||||
object FileKeyStorage {
|
||||
|
||||
/**
|
||||
* Generates or loads keypair
|
||||
*
|
||||
* @param keyPath Path to store keys in
|
||||
* @param algo Sign algo
|
||||
* @return Keypair, either loaded or freshly generated
|
||||
*/
|
||||
def getKeyPair(keyPath: String, algo: signature.SignAlgo): IO[KeyPair] =
|
||||
IO(new FileKeyStorage(new File(keyPath)))
|
||||
.flatMap(_.readOrCreateKeyPair(algo.generateKeyPair.runF[IO](None)))
|
||||
|
||||
}
|
@ -1,84 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto.keystore
|
||||
|
||||
import cats.syntax.compose._
|
||||
import fluence.codec.PureCodec
|
||||
import fluence.codec.bits.BitsCodecs
|
||||
import fluence.codec.circe.CirceCodecs
|
||||
import fluence.crypto.KeyPair
|
||||
import io.circe.{HCursor, Json}
|
||||
import scodec.bits.{Bases, ByteVector}
|
||||
|
||||
import scala.language.higherKinds
|
||||
|
||||
/**
|
||||
* Json example:
|
||||
* {
|
||||
* "keystore" : {
|
||||
* "secret" : "SFcDtZClfcxx75w9xJpQgBm09d6h9tVmVUEgHYxlews=",
|
||||
* "public" : "AlTBivFrIYe++9Me4gr4R11BtRzjZ2WXZGDNWD/bEPka"
|
||||
* }
|
||||
* }
|
||||
*/
|
||||
object KeyStore {
|
||||
|
||||
private val alphabet = Bases.Alphabets.Base64Url
|
||||
|
||||
private object Field {
|
||||
val Keystore = "keystore"
|
||||
val Secret = "secret"
|
||||
val Public = "public"
|
||||
}
|
||||
|
||||
// Codec for a tuple of already serialized public and secret keys to json
|
||||
private val pubSecJsonCodec: PureCodec[(String, String), Json] =
|
||||
CirceCodecs.circeJsonCodec(
|
||||
{
|
||||
case (pub, sec) ⇒
|
||||
Json.obj(
|
||||
(
|
||||
Field.Keystore,
|
||||
Json.obj(
|
||||
(Field.Secret, Json.fromString(sec)),
|
||||
(Field.Public, Json.fromString(pub))
|
||||
)
|
||||
)
|
||||
)
|
||||
},
|
||||
(c: HCursor) ⇒
|
||||
for {
|
||||
sec ← c.downField(Field.Keystore).downField(Field.Secret).as[String]
|
||||
pub ← c.downField(Field.Keystore).downField(Field.Public).as[String]
|
||||
} yield (pub, sec)
|
||||
)
|
||||
|
||||
// ByteVector to/from String, with the chosen alphabet
|
||||
private val vecToStr = BitsCodecs.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
|
||||
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2017 Fluence Labs Limited
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package fluence.crypto.keystore
|
||||
|
||||
import fluence.crypto.KeyPair
|
||||
import org.scalatest.{Matchers, WordSpec}
|
||||
|
||||
class KeyStoreSpec extends WordSpec with Matchers {
|
||||
|
||||
private val keyPair = KeyPair.fromBytes("pubKey".getBytes, "secKey".getBytes)
|
||||
private val jsonString = """{"keystore":{"secret":"c2VjS2V5","public":"cHViS2V5"}}"""
|
||||
|
||||
"KeyStore.encodeKeyStorage" should {
|
||||
"transform KeyStore to json" in {
|
||||
val result = KeyStore.keyPairJsonStringCodec.direct.unsafe(keyPair)
|
||||
result shouldBe jsonString
|
||||
}
|
||||
}
|
||||
|
||||
"KeyStore.decodeKeyStorage" should {
|
||||
"transform KeyStore to json" in {
|
||||
val result = KeyStore.keyPairJsonStringCodec.inverse.unsafe(jsonString)
|
||||
result shouldBe keyPair
|
||||
}
|
||||
}
|
||||
|
||||
"KeyStore" should {
|
||||
"transform KeyStore to json and back" in {
|
||||
val result = KeyStore.keyPairJsonCodec.inverse.unsafe(KeyStore.keyPairJsonCodec.direct.unsafe(keyPair))
|
||||
result shouldBe keyPair
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -26,4 +26,3 @@ object FluenceCrossType extends sbtcrossproject.CrossType {
|
||||
override def sharedSrcDir(projectBase: File, conf: String) =
|
||||
Some(shared(projectBase, conf))
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,10 @@
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0")
|
||||
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.1")
|
||||
|
||||
addSbtPlugin("com.typesafe.sbt" % "sbt-native-packager" % "1.3.20")
|
||||
|
||||
addSbtPlugin("de.heikoseeberger" % "sbt-header" % "5.2.0")
|
||||
|
||||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.27")
|
||||
addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.28")
|
||||
addSbtPlugin("org.portable-scala" % "sbt-crossproject" % "0.6.0")
|
||||
addSbtPlugin("org.portable-scala" % "sbt-scalajs-crossproject" % "0.6.0")
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user