mirror of
https://github.com/fluencelabs/crypto
synced 2025-04-25 06:42:19 +00:00
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:
parent
554e1db8b1
commit
d368ec87bf
28
jvm/src/main/scala/fluence/crypto/algorithm/AesConfig.scala
Normal file
28
jvm/src/main/scala/fluence/crypto/algorithm/AesConfig.scala
Normal 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"
|
||||||
|
)
|
202
jvm/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala
Normal file
202
jvm/src/main/scala/fluence/crypto/algorithm/AesCrypt.scala
Normal 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)
|
||||||
|
|
||||||
|
}
|
68
jvm/src/test/scala/fluence/crypto/AesSpec.scala
Normal file
68
jvm/src/test/scala/fluence/crypto/AesSpec.scala
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user