I aim to create a few primitives for creating AES256-then-HMACSHA256 secured session state through cookies.

What I've considered:

  • HMAC instead of plain sha to avoid hash extension attacks
  • input validation from the programmer
  • AES in CBC mode, because we're supporting more than 128 bits of data
  • PKCS7 mode for padding
  • Compression for saving space in the cookies (max 4 KiB data)
  • Typed errors so they can easily be interpreted
  • Avoiding something similar to the ASP.Net oracle attack, because HMAC check is done before decryption so there's no risk of the padding being malleable
  • HMACSHA256 instead of SHA1, due to the deprecation that google has done to SHA1
  • HMAC after encryption, to avoid some tampering attacks and the padding problems of TLS
  • .Net plain/mono doesn't seem to have a AES-GCM mode implementation, wanted to use what's available, even if it costs a few more CPU cycles

Full code is here. I'm not looking for comments like; 'snake case is odd'. ;)

module Crypto =
  open System
  open System.IO
  open System.IO.Compression
  open System.Text
  open System.Security.Cryptography

  /// The default hmac algorithm
  let HMACAlgorithm = "HMACSHA256"

  /// The length of the HMAC value in number of bytes
  let HMACLength = 32 // = 256 / 8

  /// Calculate the HMAC of the passed data given a private key
  let hmac (key : byte []) offset count (data : byte[]) =
    use hmac = HMAC.Create(HMACAlgorithm)
    hmac.Key <- key
    hmac.ComputeHash (data, offset, count)

  let hmac' key (data : byte []) =
    hmac key 0 (data.Length) data

  /// Calculate the HMAC value given the key
  /// and a seq of string-data which will be concatenated in its order and hmac-ed.
  let hmac'' (key : byte []) (data : string seq) =
    hmac' key (String.Concat data |> UTF8.bytes)

  /// # bits in key
  let KeySize   = 256

  /// # bytes in key
  let KeyLength = KeySize / 8

  /// # bits in block
  let BlockSize = 128

  /// # bytes in IV
  /// 16 bytes for 128 bit blocks
  let IVLength = BlockSize / 8

  /// the global crypto-random pool for uniform and therefore cryptographically
  /// secure random values
  let crypt_random = RandomNumberGenerator.Create()

  /// Fills the passed array with random bytes
  let randomize (bytes : byte []) =
    crypt_random.GetBytes bytes

  /// Generates a string key from the available characters with the given key size
  /// in characters. Note that this key is not cryptographically as random as a pure
  /// random number generator would produce as we only use a small subset alphabet.
  let generate_key key_length =
    Array.zeroCreate<byte> key_length |> randomize

  let generate_key' () =
    generate_key KeyLength

  let generate_iv iv_length =
    Array.zeroCreate<byte> iv_length |> randomize

  let generate_iv' () =
    generate_iv IVLength

  /// key: 32 bytes for 256 bit key
  /// Returns a new key and a new iv as two byte arrays as a tuple.
  let generate_keys () =
    generate_key' (), generate_iv' ()

  type SecretboxEncryptionError =
    | InvalidKeyLength of string
    | EmptyMessageGiven

  type SecretboxDecryptionError =
    | TruncatedMessage of string
    | AlteredOrCorruptMessage of string

  let private secretbox_init key iv =
    let aes = new AesManaged()
    aes.KeySize   <- KeySize
    aes.BlockSize <- BlockSize
    aes.Mode      <- CipherMode.CBC
    aes.Padding   <- PaddingMode.PKCS7
    aes.IV        <- iv
    aes.Key       <- key

  let secretbox (key : byte []) (msg : string) =
    if key.Length <> KeyLength then
      Choice2Of2 (InvalidKeyLength (sprintf "key should be %d bytes but was %d bytes" KeyLength (key.Length)))
    elif String.IsNullOrWhiteSpace msg then
      Choice2Of2 EmptyMessageGiven
      let iv  = generate_iv' ()
      use aes = secretbox_init key iv

      let mk_cipher_text (msg : string) key iv =
        use enc      = aes.CreateEncryptor(key, iv)
        use cipher   = new MemoryStream()
        use crypto   = new CryptoStream(cipher, enc, CryptoStreamMode.Write)
        let bytes = msg |> UTF8.bytes |> Compression.gzip_encode
        crypto.Write (bytes, 0, bytes.Length)

      use cipher_text = new MemoryStream()

      let bw  = new BinaryWriter(cipher_text)
      bw.Write iv
      bw.Write (mk_cipher_text msg key iv)
      bw.Flush ()

      let hmac = hmac' key (cipher_text.ToArray())
      bw.Write hmac

      Choice1Of2 (cipher_text.ToArray())

  let secretbox_open (key : byte []) (cipher_text : byte []) =
    let hmac_calc = hmac key 0 (cipher_text.Length - HMACLength) cipher_text
    let hmac_given = Array.zeroCreate<byte> HMACLength
    Array.blit cipher_text (cipher_text.Length - HMACLength) // from
               hmac_given  0                                 // to
               HMACLength                                    // # bytes for hmac

    if cipher_text.Length < HMACLength + IVLength then
      Choice2Of2 (
        TruncatedMessage (
          sprintf "cipher text length was %d but expected >= %d"
                  cipher_text.Length (HMACLength + IVLength)))
    elif not (Bytes.cnst_time_cmp hmac_calc hmac_given) then
      Choice2Of2 (AlteredOrCorruptMessage "calculated HMAC does not match expected/given")
      let iv = Array.zeroCreate<byte> IVLength
      Array.blit cipher_text 0
                 iv 0
      use aes     = secretbox_init key iv
      use denc    = aes.CreateDecryptor(key, iv)
      use plain   = new MemoryStream()
      use crypto  = new CryptoStream(plain, denc, CryptoStreamMode.Write)
      crypto.Write(cipher_text, IVLength, cipher_text.Length - IVLength - HMACLength)
      Choice1Of2 (plain.ToArray() |> Compression.gzip_decode |> UTF8.to_string')


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.