From d368ec87bf6b5acac43978bfdd9e42edc1dffa41 Mon Sep 17 00:00:00 2001 From: Dima Date: Mon, 26 Feb 2018 08:47:30 +0300 Subject: [PATCH] Aes encryption with password (#65) * naive implementation of aes encryption with password * refactr AesCrypt to F * made the `codec` cross-platform --- .../fluence/crypto/algorithm/AesConfig.scala | 28 +++ .../fluence/crypto/algorithm/AesCrypt.scala | 202 ++++++++++++++++++ .../test/scala/fluence/crypto/AesSpec.scala | 68 ++++++ 3 files changed, 298 insertions(+) create mode 100644 jvm/src/main/scala/fluence/crypto/algorithm/AesConfig.scala create mode 100644 jvm/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala create mode 100644 jvm/src/test/scala/fluence/crypto/AesSpec.scala diff --git a/jvm/src/main/scala/fluence/crypto/algorithm/AesConfig.scala b/jvm/src/main/scala/fluence/crypto/algorithm/AesConfig.scala new file mode 100644 index 0000000..3ca2307 --- /dev/null +++ b/jvm/src/main/scala/fluence/crypto/algorithm/AesConfig.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.algorithm + +/** + * Config for AES-256 password based encryption + * @param iterationCount The number of iterations of hashing the password + * @param salt Salt, which will be mixed with the password + */ +case class AesConfig( + iterationCount: Int = 50, + salt: String = "fluence" +) diff --git a/jvm/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala b/jvm/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala new file mode 100644 index 0000000..e4a1499 --- /dev/null +++ b/jvm/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala @@ -0,0 +1,202 @@ +/* + * 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.{ Applicative, Monad, MonadError } +import cats.data.EitherT +import cats.syntax.flatMap._ +import cats.syntax.applicative._ +import fluence.codec.Codec +import fluence.crypto.cipher.Crypt +import org.bouncycastle.crypto.CipherParameters +import org.bouncycastle.crypto.engines.AESEngine +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 scodec.bits.ByteVector + +import scala.language.higherKinds +import scala.util.Random + +case class DetachedData(ivData: Array[Byte], encData: Array[Byte]) +case class DataWithParams(data: Array[Byte], params: CipherParameters) + +/** + * PBEWithSHA256And256BitAES-CBC-BC cryptography + * PBE - Password-based encryption + * SHA256 - hash for password + * AES with CBC BC - Advanced Encryption Standard with Cipher Block Chaining + * https://ru.wikipedia.org/wiki/Advanced_Encryption_Standard + * https://en.wikipedia.org/wiki/Block_cipher_mode_of_operation#Cipher_Block_Chaining_(CBC) + * @param password User entered password + * @param withIV Initialization vector to achieve semantic security, a property whereby repeated usage of the scheme + * under the same key does not allow an attacker to infer relationships between segments of the encrypted + * message + */ +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]] with JavaAlgorithm { + import CryptoErr._ + + private val rnd = Random + 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 + private val BITS = 256 + private def generateIV: Array[Byte] = { + val iv = new Array[Byte](IV_SIZE) + rnd.nextBytes(iv) + iv + } + + override def encrypt(plainText: T): F[Array[Byte]] = { + 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) + } yield encData + + e.value.flatMap(ME.fromEither) + } + + override def decrypt(cipherText: Array[Byte]): F[T] = { + val e = for { + dataWithParams ← detachDataAndGetParams(cipherText, password, salt, withIV) + decData ← processData(dataWithParams, None, encrypt = false) + plain ← EitherT.liftF[F, CryptoErr, T](codec.decode(decData)) + } yield plain + + e.value.flatMap(ME.fromEither) + } + + /** + * 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.") + } + + /** + * Setup AES CBC cipher + * @param encrypt True for encryption and false for decryption + * @return cipher + */ + private def setupAesCipher(params: CipherParameters, encrypt: Boolean): EitherT[F, CryptoErr, 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(data: Array[Byte], cipher: PaddedBufferedBlockCipher): EitherT[F, CryptoErr, 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(dataWithParams: DataWithParams, addData: Option[Array[Byte]], encrypt: Boolean): EitherT[F, CryptoErr, 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(data: Array[Byte], ivSize: Int): EitherT[F, CryptoErr, 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 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) + } else { + for { + key ← initSecretKey(password, salt) + // setup cipher parameters with key + params = new KeyParameter(key) + } yield DataWithParams(data, params) + } + } +} + +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/jvm/src/test/scala/fluence/crypto/AesSpec.scala b/jvm/src/test/scala/fluence/crypto/AesSpec.scala new file mode 100644 index 0000000..e824075 --- /dev/null +++ b/jvm/src/test/scala/fluence/crypto/AesSpec.scala @@ -0,0 +1,68 @@ +package fluence.crypto + +import fluence.crypto.algorithm.{ AesConfig, AesCrypt, CryptoErr } +import cats.instances.try_._ +import org.scalatest.{ Matchers, WordSpec } +import scodec.bits.ByteVector + +import scala.util.{ Random, Try } + +class AesSpec extends WordSpec with Matchers { + + 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) + fakeAes.decrypt(crypted).map(_ ⇒ false).recover { + case e: CryptoErr ⇒ true + case _ ⇒ false + }.get shouldBe true + + //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))) + aesWrongSalt.decrypt(crypted).map(_ ⇒ false).recover { + case e: CryptoErr ⇒ true + case _ ⇒ false + }.get shouldBe true + } + + "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) + fakeAes.decrypt(crypted).map(_ ⇒ false).recover { + case e: CryptoErr ⇒ true + case _ ⇒ false + }.get shouldBe true + + //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 = true, config = conf.copy(salt = rndString(10))) + aesWrongSalt.decrypt(crypted).map(_ ⇒ false).recover { + case e: CryptoErr ⇒ true + case _ ⇒ false + }.get shouldBe true + } + } + +}