mirror of
https://github.com/fluencelabs/crypto
synced 2025-04-24 14:22:18 +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