Symmetric Ciphers
Introduction
For symmetric ciphers, we provide a low level as well as a high-level api construction.
Symmetric ciphers, as an introduction, have the following basic properties, for all non-empty m (as, if a message is empty, you’re not encrypting anything and c = m):
E(K, m) = c
D(K, c) = m
D(K, E(K, m)) = m
Where:
E: Encryption function
D: Decryption function
K: Some symmetric key
m: Some message, or plainText
c: Some ciphertext
However, in practice, block ciphers (and most secure stream ciphers like Salsa20) require a nonce as well
(called an initialization vector for block ciphers). As such, we need to either provide it explicitly, or
implicitly in our cipher construction. TSec provides such an implicit construction using IvStrategy
.
Usage
These are the imports you will need for basic usage:
import tsec.common._
import tsec.cipher.symmetric._
import tsec.cipher.symmetric.jca._
import cats.effect.IO
In tsec, we provide a few default constructions for simple AES encryption:
AES/CTR
and AES/CBC
. For Authenticated encryption,
we provide an AES/GCM
construction.
To be able to generate initialization vectors for a particular cipher, you must either
have an implicit IvStrategy[A]
(where A is the algorithm type, i.e AES128GCM
) in scope, or pass it explicitly,
or use it to generate a nonce of the right size:
scala> val toEncrypt = "hi hello welcome to tsec".utf8Bytes
toEncrypt: Array[Byte] = Array(104, 105, 32, 104, 101, 108, 108, 111, 32, 119, 101, 108, 99, 111, 109, 101, 32, 116, 111, 32, 116, 115, 101, 99)
scala> implicit val ctrStrategy: IvGen[IO, AES128CTR] = AES128CTR.defaultIvStrategy[IO]
ctrStrategy: tsec.cipher.symmetric.IvGen[cats.effect.IO,tsec.cipher.symmetric.jca.AES128CTR] = tsec.cipher.symmetric.jca.JCAIvGen$$anon$1@79273a4f
scala> implicit val cachedInstance = AES128CTR.genEncryptor[IO] //Cache the implicit
cachedInstance: tsec.cipher.symmetric.jca.primitive.JCAPrimitiveCipher[cats.effect.IO,tsec.cipher.symmetric.jca.AES128CTR,tsec.cipher.symmetric.jca.CTR,tsec.cipher.common.padding.NoPadding] = tsec.cipher.symmetric.jca.primitive.JCAPrimitiveCipher$$anon$1@7845ee8a
scala> val onlyEncrypt: IO[String] =
| for {
| key <- AES128CTR.generateKey[IO] //Generate our key
| encrypted <- AES128CTR.encrypt[IO](PlainText(toEncrypt), key) //Encrypt our message
| decrypted <- AES128CTR.decrypt[IO](encrypted, key)
| } yield decrypted.toUtf8String // "hi hello welcome to tsec!"
onlyEncrypt: cats.effect.IO[String] = IO$2035788375
scala> /** You can also turn it into a singular array with the IV concatenated at the end */
| val onlyEncrypt2: IO[String] =
| for {
| key <- AES128CTR.generateKey[IO] //Generate our key
| encrypted <- AES128CTR.encrypt[IO](PlainText(toEncrypt), key) //Encrypt our message
| array = encrypted.toConcatenated
| from <- IO.fromEither(AES128CTR.ciphertextFromConcat(array))
| decrypted <- AES128CTR.decrypt[IO](from, key)
| } yield decrypted.toUtf8String // "hi hello welcome to tsec!"
onlyEncrypt2: cats.effect.IO[String] = IO$1076642142
For authenticated encryption and decryption
scala> implicit val gcmstrategy = AES128GCM.defaultIvStrategy[IO]
gcmstrategy: tsec.cipher.symmetric.IvGen[cats.effect.IO,tsec.cipher.symmetric.jca.AES128GCM] = tsec.cipher.symmetric.jca.package$GCM$$anon$4@2db82155
scala> implicit val cachedAADEncryptor = AES128GCM.genEncryptor[IO]
cachedAADEncryptor: tsec.cipher.symmetric.AADEncryptor[cats.effect.IO,tsec.cipher.symmetric.jca.AES128GCM,tsec.cipher.symmetric.jca.SecretKey] = tsec.cipher.symmetric.jca.primitive.JCAAEADPrimitive$$anon$1@253c7189
scala> val aad = AAD("myAdditionalAuthenticationData".utf8Bytes)
aad: tsec.cipher.symmetric.AAD.Type = Array(109, 121, 65, 100, 100, 105, 116, 105, 111, 110, 97, 108, 65, 117, 116, 104, 101, 110, 116, 105, 99, 97, 116, 105, 111, 110, 68, 97, 116, 97)
scala> val encryptAAD: IO[String] =
| for {
| key <- AES128GCM.generateKey[IO] //Generate our key
| encrypted <- AES128GCM.encryptWithAAD[IO](PlainText(toEncrypt), key, aad) //Encrypt
| decrypted <- AES128GCM.decryptWithAAD[IO](encrypted, key, aad) //Decrypt
| } yield decrypted.toUtf8String // "hi hello welcome to tsec!"
encryptAAD: cats.effect.IO[String] = IO$1012373446
Finally, For more advanced usage, i.e you know which cipher you want specifically, you must import both padding as well as the primitive package.
Note: This is not recommended. Use at your own risk.
scala> /** For more advanced usage, i.e you know which cipher you want specifically, you must import padding
| * as well as the low level package
| *
| * this is not recommended, but useful for.. science!
| *
| */
| import tsec.cipher.common.padding._
import tsec.cipher.common.padding._
scala> import tsec.cipher.symmetric.jca.primitive._
import tsec.cipher.symmetric.jca.primitive._
scala> val desStrategy = JCAIvGen.random[IO, DES]
desStrategy: tsec.cipher.symmetric.IvGen[cats.effect.IO,tsec.cipher.symmetric.jca.DES] = tsec.cipher.symmetric.jca.JCAIvGen$$anon$1@707b0b9b
scala> implicit val instance = JCAPrimitiveCipher.sync[IO, DES, CBC, PKCS7Padding]
instance: tsec.cipher.symmetric.jca.primitive.JCAPrimitiveCipher[cats.effect.IO,tsec.cipher.symmetric.jca.DES,tsec.cipher.symmetric.jca.CBC,tsec.cipher.common.padding.PKCS7Padding] = tsec.cipher.symmetric.jca.primitive.JCAPrimitiveCipher$$anon$1@1780bc15
scala> val advancedUsage: IO[String] = for {
| key <- DES.generateKey[IO]
| iv <- desStrategy.genIv
| encrypted <- instance.encrypt(PlainText(toEncrypt), key, iv) //Encrypt our message, with our auth data
| decrypted <- instance.decrypt(encrypted, key) //Decrypt our message: We need to pass it the same AAD
| } yield decrypted.toUtf8String
advancedUsage: cats.effect.IO[String] = IO$2024314110