All Downloads are FREE. Search and download functionalities are using the official Maven repository.

co.topl.crypto.encryption.cipher.Aes.scala Maven / Gradle / Ivy

The newest version!
package co.topl.crypto.encryption.cipher

import cats.Applicative
import cats.implicits.catsSyntaxApplicativeId
import io.circe.{Decoder, Encoder, HCursor, Json}
import org.bouncycastle.crypto.BufferedBlockCipher
import org.bouncycastle.crypto.engines.AESEngine
import org.bouncycastle.crypto.modes.SICBlockCipher
import org.bouncycastle.crypto.params.{KeyParameter, ParametersWithIV}
import org.bouncycastle.util.Strings

/**
 * AES encryption.
 * Aes is a symmetric block cipher that can encrypt and decrypt data using the same key.
 * @see [[https://en.wikipedia.org/wiki/Advanced_Encryption_Standard]]
 */
object Aes {
  val BlockSize: Int = 16

  /**
   * Generate a random initialization vector.
   *
   * @return a random initialization vector
   */
  def generateIv: Array[Byte] = {
    val iv = new Array[Byte](BlockSize)
    new java.security.SecureRandom().nextBytes(iv)
    iv
  }

  /**
   * AES parameters.
   *
   * @param iv initialization vector
   */
  case class AesParams(iv: Array[Byte]) extends Params {
    override val cipher: String = "aes"

    override def equals(that: Any): Boolean = that match {
      case that: AesParams => java.util.Arrays.equals(iv, that.iv)
      case _               => false
    }

    override def hashCode(): Int = java.util.Arrays.hashCode(iv)
  }

  /**
   * Create an instance of the AES cipher.
   *
   * @param aesParams AES parameters
   * @return an AES cipher
   */
  def make[F[_]: Applicative](aesParams: AesParams): Cipher[F] = new Cipher[F] {
    override val params: AesParams = aesParams

    /**
     * Encrypt data.
     *
     * @note AES block size is a multiple of 16, so the data must have a length multiple of 16.
     *       Simply padding the bytes would make it impossible to determine the initial data bytes upon encryption.
     *       The amount padded to the plaintext is prepended to the plaintext. Since we know the amount padded is
     *       <16, only one byte is needed to store the amount padded.
     * @param plainText data to encrypt
     * @param key       the symmetric key for encryption and decryption
     *                  Must be 128/192/256 bits or 16/24/32 bytes.
     * @return encrypted data
     */
    override def encrypt(plainText: Array[Byte], key: Array[Byte]): F[Array[Byte]] = {
      // + 1 to account for the byte storing the amount padded. This value is guaranteed to be <16
      val amountPadded = (Aes.BlockSize - ((plainText.length + 1) % Aes.BlockSize)) % Aes.BlockSize
      val paddedBytes = amountPadded.toByte +: plainText ++: Array.fill[Byte](amountPadded)(0)
      processAes(paddedBytes, key, params.iv, encrypt = true).pure[F]
    }

    /**
     * Decrypt data.
     *
     * @note The preImage consists of [paddedAmount] ++ [data] ++ [padding]
     * @param cipherText data to decrypt
     * @param key        the symmetric key for encryption and decryption
     *                   Must be 128/192/256 bits or 16/24/32 bytes.
     * @return decrypted data
     */
    override def decrypt(cipherText: Array[Byte], key: Array[Byte]): F[Array[Byte]] = {
      val preImage = processAes(cipherText, key, params.iv, encrypt = false)
      val paddedAmount = preImage.head.toInt
      val paddedBytes = preImage.tail
      paddedBytes.slice(0, paddedBytes.length - paddedAmount).pure[F]
    }

    private def processAes(input: Array[Byte], key: Array[Byte], iv: Array[Byte], encrypt: Boolean): Array[Byte] = {
      val cipherParams = new ParametersWithIV(new KeyParameter(key), iv)
      val aesCtr = new BufferedBlockCipher(new SICBlockCipher(new AESEngine))
      aesCtr.init(encrypt, cipherParams)
      val output = Array.fill[Byte](input.length)(1: Byte)
      aesCtr.processBytes(input, 0, input.length, output, 0)
      aesCtr.doFinal(output, 0)
      output
    }
  }

  /** JSON codecs for AES parameters. */
  object Codecs {

    /** JSON encoder for AES parameters. */
    implicit val aesParamsToJson: Encoder[AesParams] = new Encoder[AesParams] {

      override def apply(a: AesParams): Json =
        Json.obj("iv" -> Json.fromString(Strings.fromByteArray(a.iv)))
    }

    /** JSON decoder for AES parameters. */
    implicit val aesParamsFromJson: Decoder[AesParams] = new Decoder[AesParams] {

      override def apply(c: HCursor): Decoder.Result[AesParams] =
        for {
          iv <- c.downField("iv").as[String]
        } yield AesParams(iv = Strings.toByteArray(iv))
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy