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

tsec.cipher.symmetric.bouncy.ChaCha20Cipher.scala Maven / Gradle / Ivy

The newest version!
package tsec.cipher.symmetric.bouncy

import java.security.MessageDigest
import java.util

import cats.effect.Sync
import org.bouncycastle.crypto.StreamCipher
import org.bouncycastle.crypto.macs.Poly1305
import org.bouncycastle.crypto.params.{KeyParameter, ParametersWithIV}
import org.bouncycastle.util.Pack
import tsec.cipher._
import tsec.cipher.symmetric._
import tsec.common.ManagedRandom
import tsec.keygen.symmetric.SymmetricKeyGen

/** A trait to help factor out the ChaCha20 construction
  * common code.
  *
  * Unfortunately, the covariant bound on C is necessary
  * to define code in terms of `init` and
  * `processBytes`, despite nothing else being used.
  *
  * All ChaCha cipher protocols same the same key size, as well as
  * processing block size, as well as the same authentication
  * tag length since it's dependent on poly.
  *
  */
private[tsec] trait ChaCha20Cipher[A, C <: StreamCipher] {

  /** Note: ChaCha and salsa are stream ciphers but they operate
    * on fixed length blocks of 64 bytes.
    *
    * See:
    * https://github.com/jedisct1/libsodium/blob/master/src/libsodium/crypto_stream/chacha20/ref/chacha20_ref.c
    *
    */
  private val BlockSize = 64
  val KeySize: Int      = 32
  val TagSize: Int      = 16
  def nonceSize: Int

  implicit def defaultKeyGen[F[_]](implicit F: Sync[F]): SymmetricKeyGen[F, A, BouncySecretKey] =
    new SymmetricKeyGen[F, A, BouncySecretKey] with ManagedRandom {
      def generateKey: F[BouncySecretKey[A]] = F.delay {
        val kBytes = new Array[Byte](KeySize)
        nextBytes(kBytes)
        BouncySecretKey(kBytes)
      }

      def build(rawKey: Array[Byte]): F[BouncySecretKey[A]] =
        if (rawKey.length != KeySize)
          F.raiseError(CipherKeyBuildError("Invalid key length"))
        else
          F.pure(BouncySecretKey(rawKey))
    }

  protected def getCipherImpl: C

  /** Mutates the internal
    *
    * @param key
    * @param aad
    * @param in
    */
  protected def poly1305Auth(
      key: KeyParameter,
      aad: AAD,
      in: Array[Byte],
      inSize: Int,
      tagOut: Array[Byte],
      tOutOffset: Int
  ): Unit

  /** Encrypt the plaintext using the chacha function.
    *
    * Run an empty block of 64 bytes through the cipher to
    * generate the Poly1305 key. Encrypt the plaintext,
    * then return the block and the tag concatenated like:
    *
    * cipherText || block
    */
  def unsafeEncrypt(
      plainText: PlainText,
      k: BouncySecretKey[A],
      iv: Iv[A]
  ): CipherText[A] =
    unsafeEncryptAAD(plainText, k, iv, AAD(Array.empty[Byte]))

  /** Encrypt the plaintext using the chacha function.
    *
    * Run an empty block of 64 bytes through the cipher to
    * generate the Poly1305 key. Encrypt the plaintext,
    * then return the block and the tag concatenated like:
    *
    * cipherText || block
    */
  def unsafeEncryptAAD(
      plainText: PlainText,
      k: BouncySecretKey[A],
      iv: Iv[A],
      aad: AAD
  ): CipherText[A] = {
    if (iv.length != nonceSize)
      throw IvError("Invalid Nonce Size")

    val chacha20   = getCipherImpl
    val firstBlock = new Array[Byte](BlockSize)
    val ctOut      = RawCipherText[A](new Array[Byte](plainText.length + TagSize))

    chacha20.init(true, new ParametersWithIV(new KeyParameter(k), iv))
    chacha20.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0)
    val macKey = new KeyParameter(firstBlock, 0, KeySize)
    util.Arrays.fill(firstBlock, 0.toByte)

    chacha20.processBytes(plainText, 0, plainText.length, ctOut, 0)
    poly1305Auth(macKey, aad, ctOut, plainText.length, ctOut, plainText.length)
    CipherText(ctOut, iv)
  }

  /** Decrypt the plaintext using the chacha function.
    *
    * Using the DJB ciphers, encryption and decryption
    * are the same operation applied. simply in reverse,
    * thus, the `init` parameter in the
    * `StreamCipher` is irrelevant.
    *
    * We assume the ciphertext is of the form
    * cipherText || block
    * Thus, it must have a minimum size of at least one.
    */
  def unsafeDecrypt(
      ct: CipherText[A],
      k: BouncySecretKey[A]
  ): PlainText = unsafeDecryptAAD(ct, k, AAD(Array.empty))

  /** Decrypt the plaintext using the chacha function.
    *
    * Using the DJB ciphers, encryption and decryption
    * are the same operation applied. simply in reverse,
    * thus, the `init` parameter in the
    * `StreamCipher` is irrelevant.
    *
    * We assume the ciphertext is of the form
    * cipherText || block
    * Thus, it must have a minimum size of at least one.
    *
    * Run the empty block on the cipher, run the encryption
    * algorithm (which is essentially decryption) and
    * compare the tag computed from the original ciphertext.
    *
    */
  def unsafeDecryptAAD(
      ct: CipherText[A],
      k: BouncySecretKey[A],
      aad: AAD
  ): PlainText = {
    val ctLen = ct.content.length - TagSize
    if (ctLen < 1)
      throw CipherTextError("Ciphertext is 0 or less bytes")
    if (ct.nonce.length != nonceSize)
      throw IvError("Invalid nonce Size")

    val chacha20    = getCipherImpl
    val firstBlock  = new Array[Byte](BlockSize)
    val out         = PlainText(new Array[Byte](ctLen))
    val computedTag = new Array[Byte](TagSize)
    val oldTag      = new Array[Byte](TagSize)
    System.arraycopy(ct.content, ctLen, oldTag, 0, TagSize)

    chacha20.init(false, new ParametersWithIV(new KeyParameter(k), ct.nonce))
    chacha20.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0)
    val macKey = new KeyParameter(firstBlock, 0, KeySize)
    util.Arrays.fill(firstBlock, 0.toByte)
    chacha20.processBytes(ct.content, 0, ctLen, out, 0)
    poly1305Auth(macKey, aad, ct.content, ctLen, computedTag, 0)

    if (!MessageDigest.isEqual(computedTag, oldTag))
      throw AuthTagError("Tags do not match")

    PlainText(out)
  }

  /** Encrypt the plaintext using the chacha function.
    *
    * Run an empty block of 64 bytes through the cipher to
    * generate the Poly1305 key. Encrypt the plaintext,
    * then return the block and the tag in a separate fashion.
    */
  def unsafeEncryptDetached(
      plainText: PlainText,
      k: BouncySecretKey[A],
      iv: Iv[A]
  ): (CipherText[A], AuthTag[A]) =
    unsafeEncryptDetachedAAD(plainText, k, iv, AAD(Array.empty[Byte]))

  /** Encrypt the plaintext using the chacha function.
    *
    * Run an empty block of 64 bytes through the cipher to
    * generate the Poly1305 key. Encrypt the plaintext,
    * then return the block and the tag in a separate fashion.
    */
  def unsafeEncryptDetachedAAD(
      plainText: PlainText,
      k: BouncySecretKey[A],
      iv: Iv[A],
      aad: AAD
  ): (CipherText[A], AuthTag[A]) = {
    if (iv.length != nonceSize)
      throw IvError("Invalid nonce size")

    val chacha20   = getCipherImpl
    val ctOut      = RawCipherText[A](new Array[Byte](plainText.length))
    val tagOut     = AuthTag[A](new Array[Byte](TagSize))
    val firstBlock = new Array[Byte](BlockSize)

    chacha20.init(true, new ParametersWithIV(new KeyParameter(k), iv))
    chacha20.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0)
    val macKey = new KeyParameter(firstBlock, 0, KeySize)
    util.Arrays.fill(firstBlock, 0.toByte)
    chacha20.processBytes(plainText, 0, plainText.length, ctOut, 0)
    poly1305Auth(macKey, aad, ctOut, plainText.length, tagOut, 0)
    (CipherText(ctOut, iv), tagOut)
  }

  /** Decrypt the plaintext using the chacha function.
    *
    * Run an empty block of 64 bytes through the cipher to
    * generate the Poly1305 key. Decrypt the plaintext,
    * generate the authentication tag and compare it to the
    * supplied tag.
    *
    */
  def unsafeDecryptDetached(
      ct: CipherText[A],
      authTag: AuthTag[A],
      k: BouncySecretKey[A]
  ): PlainText = unsafeDecryptDetachedAAD(ct, authTag, k, AAD(Array.empty[Byte]))

  def unsafeDecryptDetachedAAD(
      ct: CipherText[A],
      authTag: AuthTag[A],
      k: BouncySecretKey[A],
      aad: AAD
  ): PlainText = {
    if (ct.content.length < 1)
      throw CipherTextError("Ciphertext is 0 or less bytes")
    if (ct.nonce.length != nonceSize)
      throw IvError("Invalid nonce Size")

    val cipher      = getCipherImpl
    val firstBlock  = new Array[Byte](BlockSize)
    val out         = PlainText(new Array[Byte](ct.content.length))
    val computedTag = new Array[Byte](TagSize)

    cipher.init(false, new ParametersWithIV(new KeyParameter(k), ct.nonce))
    cipher.processBytes(firstBlock, 0, firstBlock.length, firstBlock, 0)
    val macKey = new KeyParameter(firstBlock, 0, KeySize)
    util.Arrays.fill(firstBlock, 0.toByte)

    cipher.processBytes(ct.content, 0, ct.content.length, out, 0)
    poly1305Auth(macKey, aad, ct.content, ct.content.length, computedTag, 0)

    if (!MessageDigest.isEqual(computedTag, authTag))
      throw AuthTagError("Tags do not match")

    PlainText(out)
  }

  implicit def authEncryptor[F[_]](implicit F: Sync[F]): AADEncryptor[F, A, BouncySecretKey] =
    new AADEncryptor[F, A, BouncySecretKey] {
      def encryptWithAAD(
          plainText: PlainText,
          key: BouncySecretKey[A],
          iv: Iv[A],
          aad: AAD
      ): F[CipherText[A]] =
        F.delay(unsafeEncryptAAD(plainText, key, iv, aad))

      def encryptWithAADDetached(
          plainText: PlainText,
          key: BouncySecretKey[A],
          iv: Iv[A],
          aad: AAD
      ): F[(CipherText[A], AuthTag[A])] =
        F.delay(unsafeEncryptDetachedAAD(plainText, key, iv, aad))

      def decryptWithAAD(
          cipherText: CipherText[A],
          key: BouncySecretKey[A],
          aad: AAD
      ): F[PlainText] =
        F.delay(unsafeDecryptAAD(cipherText, key, aad))

      def decryptWithAADDetached(
          cipherText: CipherText[A],
          key: BouncySecretKey[A],
          aad: AAD,
          authTag: AuthTag[A]
      ): F[PlainText] =
        F.delay(unsafeDecryptDetachedAAD(cipherText, authTag, key, aad))

      def encryptDetached(
          plainText: PlainText,
          key: BouncySecretKey[A],
          iv: Iv[A]
      ): F[(CipherText[A], AuthTag[A])] =
        F.delay(unsafeEncryptDetached(plainText, key, iv))

      def decryptDetached(
          cipherText: CipherText[A],
          key: BouncySecretKey[A],
          authTag: AuthTag[A]
      ): F[PlainText] =
        F.delay(unsafeDecryptDetached(cipherText, authTag, key))

      def encrypt(
          plainText: PlainText,
          key: BouncySecretKey[A],
          iv: Iv[A]
      ): F[CipherText[A]] =
        F.delay(unsafeEncrypt(plainText, key, iv))

      def decrypt(cipherText: CipherText[A], key: BouncySecretKey[A]): F[PlainText] =
        F.delay(unsafeDecrypt(cipherText, key))
    }

  def defaultIvGen[F[_]](implicit F: Sync[F]): IvGen[F, A] =
    new IvGen[F, A] with ManagedRandom {

      def genIv: F[Iv[A]] =
        F.delay(genIvUnsafe)

      def genIvUnsafe: Iv[A] = {
        val nonce = new Array[Byte](nonceSize)
        nextBytes(nonce)
        Iv[A](nonce)
      }
    }
}

private[tsec] trait IETFChaCha20Cipher[A, C <: StreamCipher] extends ChaCha20Cipher[A, C] {
  private val BytePadding = new Array[Byte](16)

  protected def poly1305Auth(
      key: KeyParameter,
      aad: AAD,
      in: Array[Byte],
      inSize: Int,
      tagOut: Array[Byte],
      tOutOffset: Int
  ): Unit = {
    val poly1305 = new Poly1305()
    val ctLen    = Pack.longToLittleEndian(inSize & 0xFFFFFFFFL)
    val aadLen   = Pack.longToLittleEndian(aad.length & 0xFFFFFFFFL)

    poly1305.init(key)
    poly1305.update(aad, 0, aad.length)
    poly1305.update(BytePadding, 0, (0x10 - aad.length) & 0xF)
    poly1305.update(in, 0, inSize)
    poly1305.update(BytePadding, 0, (0x10 - inSize) & 0xF)
    poly1305.update(aadLen, 0, ctLen.length)
    poly1305.update(ctLen, 0, ctLen.length)
    poly1305.doFinal(tagOut, tOutOffset)
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy