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