diff --git a/js/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala b/js/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala new file mode 100644 index 0000000..300782d --- /dev/null +++ b/js/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala @@ -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 . + */ + +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) +} diff --git a/js/src/main/scala/fluence/crypto/algorithm/EcdsaJS.scala b/js/src/main/scala/fluence/crypto/algorithm/Ecdsa.scala similarity index 90% rename from js/src/main/scala/fluence/crypto/algorithm/EcdsaJS.scala rename to js/src/main/scala/fluence/crypto/algorithm/Ecdsa.scala index 86ebc39..e6b637c 100644 --- a/js/src/main/scala/fluence/crypto/algorithm/EcdsaJS.scala +++ b/js/src/main/scala/fluence/crypto/algorithm/Ecdsa.scala @@ -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) } diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/AES.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/AES.scala new file mode 100644 index 0000000..2ce8368 --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/AES.scala @@ -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 . + */ + +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 +} diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/Algos.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/Algos.scala new file mode 100644 index 0000000..3bd19ba --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/Algos.scala @@ -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 . + */ + +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 diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/CryptOptions.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/CryptOptions.scala new file mode 100644 index 0000000..6cb9f42 --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/CryptOptions.scala @@ -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 . + */ + +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] + } + + } +} diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/CryptoJS.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/CryptoJS.scala new file mode 100644 index 0000000..7c86134 --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/CryptoJS.scala @@ -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 . + */ + +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 +} diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/Enc.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/Enc.scala new file mode 100644 index 0000000..734f3ef --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/Enc.scala @@ -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 . + */ + +package fluence.crypto.facade.cryptojs + +import scala.scalajs.js + +@js.native +trait Enc extends js.Object { + def Hex: Hex = js.native +} diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/Hex.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/Hex.scala new file mode 100644 index 0000000..a810efc --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/Hex.scala @@ -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 . + */ + +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 +} diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/Key.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/Key.scala new file mode 100644 index 0000000..c51eb90 --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/Key.scala @@ -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 . + */ + +package fluence.crypto.facade.cryptojs + +import scala.scalajs.js + +@js.native +trait Key extends js.Object diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/KeyOptions.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/KeyOptions.scala new file mode 100644 index 0000000..f4281b0 --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/KeyOptions.scala @@ -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 . + */ + +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] + } +} diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/Lib.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/Lib.scala new file mode 100644 index 0000000..b70d3f1 --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/Lib.scala @@ -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 . + */ + +package fluence.crypto.facade.cryptojs + +import scala.scalajs.js + +@js.native +trait Lib extends js.Object { + def WordArray: WordArrayFactory = js.native +} diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/Mode.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/Mode.scala new file mode 100644 index 0000000..0f0bbbc --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/Mode.scala @@ -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 . + */ + +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 diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/Pad.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/Pad.scala new file mode 100644 index 0000000..1c8fef3 --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/Pad.scala @@ -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 . + */ + +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 diff --git a/js/src/main/scala/fluence/crypto/facade/cryptojs/WordArrayFactory.scala b/js/src/main/scala/fluence/crypto/facade/cryptojs/WordArrayFactory.scala new file mode 100644 index 0000000..259f98c --- /dev/null +++ b/js/src/main/scala/fluence/crypto/facade/cryptojs/WordArrayFactory.scala @@ -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 . + */ + +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 diff --git a/js/src/main/scala/fluence/crypto/facade/Elliptic.scala b/js/src/main/scala/fluence/crypto/facade/ecdsa/Elliptic.scala similarity index 98% rename from js/src/main/scala/fluence/crypto/facade/Elliptic.scala rename to js/src/main/scala/fluence/crypto/facade/ecdsa/Elliptic.scala index 93e9557..96bf4b3 100644 --- a/js/src/main/scala/fluence/crypto/facade/Elliptic.scala +++ b/js/src/main/scala/fluence/crypto/facade/ecdsa/Elliptic.scala @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package fluence.crypto.facade +package fluence.crypto.facade.ecdsa import scala.scalajs.js import scala.scalajs.js.annotation._ diff --git a/js/src/main/scala/fluence/crypto/facade/Hash.scala b/js/src/main/scala/fluence/crypto/facade/ecdsa/Hash.scala similarity index 97% rename from js/src/main/scala/fluence/crypto/facade/Hash.scala rename to js/src/main/scala/fluence/crypto/facade/ecdsa/Hash.scala index 6a569a5..8ad9503 100644 --- a/js/src/main/scala/fluence/crypto/facade/Hash.scala +++ b/js/src/main/scala/fluence/crypto/facade/ecdsa/Hash.scala @@ -15,7 +15,7 @@ * along with this program. If not, see . */ -package fluence.crypto.facade +package fluence.crypto.facade.ecdsa import scala.scalajs.js import scala.scalajs.js.annotation.JSImport diff --git a/js/src/main/scala/fluence/crypto/hash/JsCryptoHasher.scala b/js/src/main/scala/fluence/crypto/hash/JsCryptoHasher.scala index 33ea851..a1c9b19 100644 --- a/js/src/main/scala/fluence/crypto/hash/JsCryptoHasher.scala +++ b/js/src/main/scala/fluence/crypto/hash/JsCryptoHasher.scala @@ -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._ diff --git a/js/src/test/scala/fluence/crypto/AesJSSpec.scala b/js/src/test/scala/fluence/crypto/AesJSSpec.scala new file mode 100644 index 0000000..967b97d --- /dev/null +++ b/js/src/test/scala/fluence/crypto/AesJSSpec.scala @@ -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 + } + } +} diff --git a/js/src/test/scala/fluence/crypto/EcdsaJSSpec.scala b/js/src/test/scala/fluence/crypto/EcdsaSpec.scala similarity index 92% rename from js/src/test/scala/fluence/crypto/EcdsaJSSpec.scala rename to js/src/test/scala/fluence/crypto/EcdsaSpec.scala index 70921c4..d0b2fcb 100644 --- a/js/src/test/scala/fluence/crypto/EcdsaJSSpec.scala +++ b/js/src/test/scala/fluence/crypto/EcdsaSpec.scala @@ -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) diff --git a/js/src/test/scala/fluence/crypto/JSHashSpec.scala b/js/src/test/scala/fluence/crypto/JSHashSpec.scala index 6ef1d13..e1de2c0 100644 --- a/js/src/test/scala/fluence/crypto/JSHashSpec.scala +++ b/js/src/test/scala/fluence/crypto/JSHashSpec.scala @@ -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._ diff --git a/jvm/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala b/jvm/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala index ef0d2c9..b274e08 100644 --- a/jvm/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala +++ b/jvm/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala @@ -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) } } diff --git a/jvm/src/test/scala/fluence/crypto/AesSpec.scala b/jvm/src/test/scala/fluence/crypto/AesSpec.scala index 91200e1..36ea20c 100644 --- a/jvm/src/test/scala/fluence/crypto/AesSpec.scala +++ b/jvm/src/test/scala/fluence/crypto/AesSpec.scala @@ -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)) diff --git a/jvm/src/main/scala/fluence/crypto/algorithm/AesConfig.scala b/src/main/scala/fluence/crypto/algorithm/AesConfig.scala similarity index 100% rename from jvm/src/main/scala/fluence/crypto/algorithm/AesConfig.scala rename to src/main/scala/fluence/crypto/algorithm/AesConfig.scala