Aes crypto js (#72)

* WIP

* facade for encrypt-decrypt and TestApp for testing

* test

* understanding of return types in crypto-js

* refactor jvm aes, keyJVM === keyJS, encryptedJVM === encryptedJS

* great refactoring of cryptography

* small improvements

* refactoring and documentation

* delete main classes

* rename AesCryptJS to AesCrypt

* fix tests

* License headers
This commit is contained in:
Dima 2018-03-05 17:53:14 +03:00 committed by Dmitry Kurinskiy
parent 26b41e7b4d
commit d091146dd2
23 changed files with 638 additions and 51 deletions

View File

@ -0,0 +1,142 @@
/*
* 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.algorithm
import cats.data.EitherT
import cats.{ Applicative, Monad, MonadError }
import cats.syntax.applicative._
import cats.syntax.flatMap._
import fluence.codec.Codec
import fluence.crypto.algorithm.CryptoErr.nonFatalHandling
import fluence.crypto.cipher.Crypt
import fluence.crypto.facade.cryptojs.{ CryptOptions, CryptoJS, Key, KeyOptions }
import scodec.bits.ByteVector
import scalajs.js.JSConverters._
import scala.language.higherKinds
import scala.scalajs.js.typedarray.Int8Array
class AesCrypt[F[_] : Monad, T](password: Array[Char], withIV: Boolean, config: AesConfig)(implicit ME: MonadError[F, Throwable], codec: Codec[F, T, Array[Byte]]) extends Crypt[F, T, Array[Byte]] {
private val salt = config.salt
private val rndStr = CryptoJS.lib.WordArray
//number of password hashing iterations
private val iterationCount = config.iterationCount
//initialisation vector must be the same length as block size
private val IV_SIZE = 16
private val BITS = 256
//generate IV in hex
private def generateIV = rndStr.random(IV_SIZE)
private val pad = CryptoJS.pad.Pkcs7
private val mode = CryptoJS.mode.CBC
private val aes = CryptoJS.AES
override def encrypt(plainText: T): F[Array[Byte]] = {
val e = for {
data EitherT.liftF(codec.encode(plainText))
key initSecretKey()
encrypted encryptData(data, key)
} yield encrypted
e.value.flatMap(ME.fromEither)
}
override def decrypt(cipherText: Array[Byte]): F[T] = {
val e = for {
detachedData detachData(cipherText)
(iv, base64) = detachedData
key initSecretKey()
decData decryptData(key, base64, iv)
_ EitherT.cond(decData.nonEmpty, decData, CryptoErr("Cannot decrypt message with this password."))
plain EitherT.liftF[F, CryptoErr, T](codec.decode(decData.toArray))
} yield plain
e.value.flatMap(ME.fromEither)
}
/**
* Encrypt data.
* @param data Data to encrypt
* @param key Salted and hashed password
* @return Encrypted data with IV
*/
private def encryptData(data: Array[Byte], key: Key): EitherT[F, CryptoErr, Array[Byte]] = {
nonFatalHandling {
//transform data to JS type
val wordArray = CryptoJS.lib.WordArray.create(new Int8Array(data.toJSArray))
val iv = if (withIV) Some(generateIV) else None
val cryptOptions = CryptOptions(iv = iv, padding = pad, mode = mode)
//encryption return base64 string, transform it to byte array
val crypted = ByteVector.fromValidBase64(aes.encrypt(wordArray, key, cryptOptions).toString)
//IV also needs to be transformed in byte array
val byteIv = iv.map(i ByteVector.fromValidHex(i.toString))
byteIv.map(_.toArray ++ crypted.toArray).getOrElse(crypted.toArray)
}("Cannot encrypt data.")
}
private def decryptData(key: Key, base64Data: String, iv: Option[String]) = {
nonFatalHandling {
//parse IV to WordArray JS format
val cryptOptions = CryptOptions(iv = iv.map(i CryptoJS.enc.Hex.parse(i)), padding = pad, mode = mode)
val dec = aes.decrypt(base64Data, key, cryptOptions)
ByteVector.fromValidHex(dec.toString)
}("Cannot decrypt data.")
}
/**
* @param cipherText Encrypted data with IV
* @return IV in hex and data in base64
*/
private def detachData(cipherText: Array[Byte]): EitherT[F, CryptoErr, (Option[String], String)] = {
nonFatalHandling {
val dataWithParams = if (withIV) {
val ivDec = ByteVector(cipherText.slice(0, IV_SIZE)).toHex
val encMessage = cipherText.slice(IV_SIZE, cipherText.length)
(Some(ivDec), encMessage)
} else (None, cipherText)
val (ivOp, data) = dataWithParams
val base64 = ByteVector(data).toBase64
(ivOp, base64)
}("Cannot detach data and IV.")
}
/**
* Hash password with salt `iterationCount` times
*/
private def initSecretKey(): EitherT[F, CryptoErr, Key] = {
nonFatalHandling {
// 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.")
}
}
object AesCrypt extends slogging.LazyLogging {
def forString[F[_] : Applicative](password: ByteVector, withIV: Boolean, config: AesConfig)(implicit ME: MonadError[F, Throwable]): AesCrypt[F, String] = {
implicit val codec: Codec[F, String, Array[Byte]] = Codec[F, String, Array[Byte]](_.getBytes.pure[F], bytes new String(bytes).pure[F])
apply[F, String](password, withIV, config)
}
def apply[F[_] : Applicative, T](password: ByteVector, withIV: Boolean, config: AesConfig)(implicit ME: MonadError[F, Throwable], codec: Codec[F, T, Array[Byte]]): AesCrypt[F, T] =
new AesCrypt(password.toHex.toCharArray, withIV, config)
}

View File

@ -18,9 +18,9 @@
package fluence.crypto.algorithm
import cats.data.EitherT
import cats.{ Monad, MonadError }
import cats.Monad
import fluence.crypto.SignAlgo
import fluence.crypto.facade.EC
import fluence.crypto.facade.ecdsa.EC
import fluence.crypto.hash.{ CryptoHasher, JsCryptoHasher }
import fluence.crypto.keypair.KeyPair
import fluence.crypto.signature.Signature
@ -34,7 +34,7 @@ import scala.scalajs.js.JSConverters._
* Return in all js methods hex, because in the other case we will receive javascript objects
* @param ec implementation of ecdsa logic for different curves
*/
class EcdsaJS(ec: EC, hasher: Option[CryptoHasher[Array[Byte], Array[Byte]]]) extends Algorithm with SignatureFunctions with KeyGenerator {
class Ecdsa(ec: EC, hasher: Option[CryptoHasher[Array[Byte], Array[Byte]]]) extends Algorithm with SignatureFunctions with KeyGenerator {
import CryptoErr._
override def generateKeyPair[F[_] : Monad](seed: Option[Array[Byte]] = None): EitherT[F, CryptoErr, KeyPair] = {
@ -82,8 +82,8 @@ class EcdsaJS(ec: EC, hasher: Option[CryptoHasher[Array[Byte], Array[Byte]]]) ex
}
object EcdsaJS {
val ecdsa_secp256k1_sha256 = new EcdsaJS(new EC("secp256k1"), Some(JsCryptoHasher.Sha256))
object Ecdsa {
val ecdsa_secp256k1_sha256 = new Ecdsa(new EC("secp256k1"), Some(JsCryptoHasher.Sha256))
val signAlgo = new SignAlgo("ecdsa/secp256k1/sha256/js", ecdsa_secp256k1_sha256)
}

View File

@ -0,0 +1,36 @@
/*
* 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.cryptojs
import scala.scalajs.js
import scala.scalajs.js.annotation.JSGlobal
@js.native
@JSGlobal
class AES extends js.Object {
/**
* @param msg Message to encrypt in JS WordArray.
* Could be created with CryptoJS.lib.WordArray.create(new Int8Array(arrayByte.toJSArray))
* @param options { iv: iv, padding: CryptoJS.pad.Pkcs7, mode: CryptoJS.mode.CBC }
* @return Encrypted message
*/
def encrypt(msg: WordArray, key: Key, options: CryptOptions): js.Any = js.native
def decrypt(encrypted: String, key: Key, options: CryptOptions): js.Any = js.native
}

View File

@ -0,0 +1,28 @@
/*
* 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.cryptojs
import scala.scalajs.js
@js.native
trait Algos extends js.Object {
def SHA256: Algo = js.native
}
@js.native
trait Algo extends js.Object

View File

@ -0,0 +1,40 @@
/*
* 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.cryptojs
import scala.scalajs.js
@js.native
trait CryptOptions extends js.Object {
val iv: Option[js.Any]
val padding: Pad
val mode: Mode
}
object CryptOptions {
def apply(iv: Option[WordArray], padding: Pad, mode: Mode): CryptOptions = {
iv match {
case Some(i)
js.Dynamic.literal(iv = i, padding = padding, mode = mode).asInstanceOf[CryptOptions]
case None
//if IV is empty, there will be an error in JS lib
js.Dynamic.literal(iv = CryptoJS.lib.WordArray.random(0), padding = padding, mode = mode).asInstanceOf[CryptOptions]
}
}
}

View File

@ -0,0 +1,42 @@
/*
* 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.cryptojs
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
@js.native
@JSImport("crypto-js", JSImport.Namespace)
object CryptoJS extends js.Object {
def pad: Paddings = js.native
def mode: Modes = js.native
def AES: AES = js.native
/**
* https://en.wikipedia.org/wiki/PBKDF2
* @return Salted and hashed key
*/
def PBKDF2(pass: String, salt: String, options: KeyOptions): Key = js.native
def lib: Lib = js.native
def enc: Enc = js.native
def algo: Algos = js.native
}

View File

@ -0,0 +1,25 @@
/*
* 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.cryptojs
import scala.scalajs.js
@js.native
trait Enc extends js.Object {
def Hex: Hex = js.native
}

View File

@ -0,0 +1,29 @@
/*
* Copyright (C) 2017 Fluence Labs Limited
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.facade.cryptojs
import scala.scalajs.js
@js.native
trait Hex extends js.Object {
/**
* Parse from HEX to JS byte representation
* @param str Hex
*/
def parse(str: String): WordArray = js.native
}

View File

@ -0,0 +1,23 @@
/*
* 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.cryptojs
import scala.scalajs.js
@js.native
trait Key extends js.Object

View File

@ -0,0 +1,33 @@
/*
* 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.cryptojs
import scala.scalajs.js
@js.native
trait KeyOptions extends js.Object {
val keySize: Int
val iterations: Int
val hasher: Algo
}
object KeyOptions {
def apply(keySizeBits: Int, iterations: Int, hasher: Algo): KeyOptions = {
js.Dynamic.literal(keySize = keySizeBits / 32, iterations = iterations, hasher = hasher).asInstanceOf[KeyOptions]
}
}

View File

@ -0,0 +1,25 @@
/*
* 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.cryptojs
import scala.scalajs.js
@js.native
trait Lib extends js.Object {
def WordArray: WordArrayFactory = js.native
}

View File

@ -0,0 +1,28 @@
/*
* 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.cryptojs
import scala.scalajs.js
@js.native
trait Modes extends js.Object {
val CBC: Mode = js.native
}
@js.native
trait Mode extends js.Object

View File

@ -0,0 +1,32 @@
/*
* 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.cryptojs
import scala.scalajs.js
import scala.scalajs.js.annotation.JSGlobal
@js.native
@JSGlobal
class Paddings extends js.Object {
val Pkcs7: Pad = js.native
}
@js.native
@JSGlobal
class Pad extends js.Object

View File

@ -0,0 +1,30 @@
/*
* 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.cryptojs
import scala.scalajs.js
@js.native
trait WordArrayFactory extends js.Object {
def random(size: Int): WordArray = js.native
def create(array: js.Any): WordArray = js.native
}
@js.native
trait WordArray extends js.Object

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.facade
package fluence.crypto.facade.ecdsa
import scala.scalajs.js
import scala.scalajs.js.annotation._

View File

@ -15,7 +15,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package fluence.crypto.facade
package fluence.crypto.facade.ecdsa
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport

View File

@ -17,7 +17,7 @@
package fluence.crypto.hash
import fluence.crypto.facade.{ SHA1, SHA256 }
import fluence.crypto.facade.ecdsa.{ SHA1, SHA256 }
import scodec.bits.ByteVector
import scala.scalajs.js.JSConverters._

View File

@ -0,0 +1,65 @@
package fluence.crypto
import cats.instances.try_._
import fluence.crypto.algorithm.{ AesConfig, AesCrypt, CryptoErr }
import org.scalactic.source.Position
import org.scalatest.{ Assertion, Matchers, WordSpec }
import scodec.bits.ByteVector
import scala.util.{ Random, Try }
class AesJSSpec extends WordSpec with Matchers with slogging.LazyLogging {
def rndString(size: Int): String = Random.nextString(10)
val conf = AesConfig()
"aes crypto" should {
"work with IV" in {
val pass = ByteVector("pass".getBytes())
val crypt = AesCrypt.forString[Try](pass, withIV = true, config = conf)
val str = rndString(200)
val crypted = crypt.encrypt(str).get
crypt.decrypt(crypted).get shouldBe str
val fakeAes = AesCrypt.forString[Try](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[Try](pass, withIV = false, config = conf)
aesWithoutIV.decrypt(crypted).get shouldNot be (str)
val aesWrongSalt = AesCrypt.forString[Try](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[Try](pass, withIV = false, config = conf)
val str = rndString(200)
val crypted = crypt.encrypt(str).get
crypt.decrypt(crypted).get shouldBe str
val fakeAes = AesCrypt.forString[Try](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[Try](pass, withIV = true, config = conf)
aesWithIV.decrypt(crypted).get shouldNot be (str)
val aesWrongSalt = AesCrypt.forString[Try](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 = {
tr.map{ r r != msg }.recover {
case e: CryptoErr true
case e
logger.error("Unexpected error", e)
false
}.get shouldBe true
}
}
}

View File

@ -19,13 +19,13 @@ package fluence.crypto
import cats.data.EitherT
import cats.instances.try_._
import fluence.crypto.algorithm.{ CryptoErr, EcdsaJS }
import fluence.crypto.algorithm.{ CryptoErr, Ecdsa }
import org.scalatest.{ Matchers, WordSpec }
import scodec.bits.ByteVector
import scala.util.{ Random, Try }
class EcdsaJSSpec extends WordSpec with Matchers {
class EcdsaSpec extends WordSpec with Matchers {
def rndBytes(size: Int) = Random.nextString(10).getBytes
@ -42,7 +42,7 @@ class EcdsaJSSpec extends WordSpec with Matchers {
"ecdsa algorithm" should {
"correct sign and verify data" in {
val algorithm = EcdsaJS.ecdsa_secp256k1_sha256
val algorithm = Ecdsa.ecdsa_secp256k1_sha256
val keys = algorithm.generateKeyPair[Try]().extract
val data = rndByteVector(10)
@ -59,7 +59,7 @@ class EcdsaJSSpec extends WordSpec with Matchers {
}
"correctly work with signer and checker" in {
val algo = EcdsaJS.signAlgo
val algo = Ecdsa.signAlgo
val keys = algo.generateKeyPair().extract
val signer = algo.signer(keys)
@ -73,7 +73,7 @@ class EcdsaJSSpec extends WordSpec with Matchers {
}
"throw an errors on invalid data" in {
val algo = EcdsaJS.signAlgo
val algo = Ecdsa.signAlgo
val keys = algo.generateKeyPair().extract
val signer = algo.signer(keys)
val data = rndByteVector(10)

View File

@ -1,6 +1,6 @@
package fluence.crypto
import fluence.crypto.facade.{ SHA1, SHA256 }
import fluence.crypto.facade.ecdsa.{ SHA1, SHA256 }
import org.scalatest.{ Matchers, WordSpec }
import scala.scalajs.js.JSConverters._

View File

@ -17,22 +17,19 @@
package fluence.crypto.algorithm
import cats.{ Applicative, Monad, MonadError }
import cats.data.EitherT
import cats.syntax.flatMap._
import cats.syntax.applicative._
import cats.syntax.flatMap._
import cats.{ Applicative, Monad, MonadError }
import fluence.codec.Codec
import fluence.crypto.cipher.Crypt
import org.bouncycastle.crypto.CipherParameters
import org.bouncycastle.crypto.{ CipherParameters, PBEParametersGenerator }
import org.bouncycastle.crypto.digests.SHA256Digest
import org.bouncycastle.crypto.engines.AESEngine
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator
import org.bouncycastle.crypto.modes.CBCBlockCipher
import org.bouncycastle.crypto.paddings.PKCS7Padding
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher
import org.bouncycastle.crypto.params.KeyParameter
import org.bouncycastle.crypto.params.ParametersWithIV
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
import org.bouncycastle.crypto.paddings.{ PKCS7Padding, PaddedBufferedBlockCipher }
import org.bouncycastle.crypto.params.{ KeyParameter, ParametersWithIV }
import scodec.bits.ByteVector
import scala.language.higherKinds
@ -61,7 +58,6 @@ class AesCrypt[F[_] : Monad, T](password: Array[Char], withIV: Boolean, config:
private val salt = config.salt.getBytes()
//number of password hashing iterations
//todo should be configurable
private val iterationCount = config.iterationCount
//initialisation vector must be the same length as block size
private val IV_SIZE = 16
@ -76,18 +72,8 @@ class AesCrypt[F[_] : Monad, T](password: Array[Char], withIV: Boolean, config:
val e = for {
data EitherT.liftF(codec.encode(plainText))
key initSecretKey(password, salt)
(extData, params) = {
if (withIV) {
val ivData = generateIV
// setup cipher parameters with key and IV
val keyParam = new KeyParameter(key)
(Some(ivData), new ParametersWithIV(keyParam, ivData))
} else {
(None, new KeyParameter(key))
}
}
encData processData(DataWithParams(data, params), extData, encrypt = true)
extDataWithParams extDataWithParams(key)
encData processData(DataWithParams(data, extDataWithParams._2), extDataWithParams._1, encrypt = true)
} yield encData
e.value.flatMap(ME.fromEither)
@ -103,18 +89,28 @@ class AesCrypt[F[_] : Monad, T](password: Array[Char], withIV: Boolean, config:
e.value.flatMap(ME.fromEither)
}
/**
* Generate key parameters with IV if it is necessary
* @param key Password
* @return Optional IV and cipher parameters
*/
def extDataWithParams(key: Array[Byte]): EitherT[F, CryptoErr, (Option[Array[Byte]], CipherParameters)] = {
if (withIV) {
val ivData = generateIV
// setup cipher parameters with key and IV
paramsWithIV(key, ivData).map(k (Some(ivData), k))
} else {
params(key).map(k (None, k))
}
}
/**
* Key spec initialization
*/
private def initSecretKey(password: Array[Char], salt: Array[Byte]): EitherT[F, CryptoErr, Array[Byte]] = {
nonFatalHandling {
// get raw key from password and salt
val pbeKeySpec = new PBEKeySpec(password, salt, iterationCount, BITS)
val keyFactory: SecretKeyFactory = SecretKeyFactory.getInstance("PBEWithSHA256And256BitAES-CBC-BC")
val secretKey = new SecretKeySpec(keyFactory.generateSecret(pbeKeySpec).getEncoded, "AES")
secretKey.getEncoded
}("Cannot init secret key.")
}
private def initSecretKey(password: Array[Char], salt: Array[Byte]): EitherT[F, CryptoErr, Array[Byte]] = nonFatalHandling {
PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(password)
}("Cannot init secret key.")
/**
* Setup AES CBC cipher
@ -170,20 +166,34 @@ class AesCrypt[F[_] : Monad, T](password: Array[Char], withIV: Boolean, config:
}("Cannot detach data and IV.")
}
private def paramsWithIV(key: Array[Byte], iv: Array[Byte]): EitherT[F, CryptoErr, ParametersWithIV] = {
params(key).flatMap { keyParam
nonFatalHandling(new ParametersWithIV(keyParam, iv))("Cannot generate key parameters with IV")
}
}
private def params(key: Array[Byte]): EitherT[F, CryptoErr, 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(data: Array[Byte], password: Array[Char], salt: Array[Byte], withIV: Boolean): EitherT[F, CryptoErr, DataWithParams] = {
if (withIV) {
for {
ivDataWithEncData detachIV(data, IV_SIZE)
key initSecretKey(password, salt)
// setup cipher parameters with key and IV
keyParam = new KeyParameter(key)
params = new ParametersWithIV(keyParam, ivDataWithEncData.ivData)
} yield DataWithParams(ivDataWithEncData.encData, params)
paramsWithIV paramsWithIV(key, ivDataWithEncData.ivData)
} yield DataWithParams(ivDataWithEncData.encData, paramsWithIV)
} else {
for {
key initSecretKey(password, salt)
// setup cipher parameters with key
params = new KeyParameter(key)
params params(key)
} yield DataWithParams(data, params)
}
}

View File

@ -22,7 +22,6 @@ class AesSpec extends WordSpec with Matchers with slogging.LazyLogging {
val str = rndString(200)
val crypted = crypt.encrypt(str).get
crypt.decrypt(crypted).get shouldBe str
val fakeAes = AesCrypt.forString[Try](ByteVector("wrong".getBytes()), withIV = true, config = conf)
checkCryptoError(fakeAes.decrypt(crypted))