Aes encryption with password (#65)

* naive implementation of aes encryption with password

* refactr AesCrypt to F

* made the `codec` cross-platform
This commit is contained in:
Dima 2018-02-26 08:47:30 +03:00 committed by GitHub
parent 554e1db8b1
commit d368ec87bf
3 changed files with 298 additions and 0 deletions

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.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"
)

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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)
}

View File

@ -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
}
}
}