packages feed

botan-low-0.0.1.0: src/Botan/Low/Cipher.hs

{-|
Module      : Botan.Low.Cipher
Description : Symmetric cipher modes
Copyright   : (c) Leo D, 2023
License     : BSD-3-Clause
Maintainer  : leo@apotheca.io
Stability   : experimental
Portability : POSIX

A block cipher by itself, is only able to securely encrypt a single
data block. To be able to securely encrypt data of arbitrary length,
a mode of operation applies the block cipher’s single block operation
repeatedly to encrypt an entire message.
-}

module Botan.Low.Cipher
(
 
-- * Cipher
-- $introduction

-- * Usage
-- $usage

  Cipher(..)
, CipherName(..)
, CipherKey(..)
, CipherNonce(..)
, CipherInitFlags(..)
, pattern MaskDirection
, pattern Encrypt
, pattern Decrypt
, CipherUpdateFlags(..)
, pattern CipherUpdate
, pattern CipherFinal
, withCipher
, cipherInit
, cipherDestroy
, cipherName
, cipherOutputLength
, cipherValidNonceLength
, cipherGetTagLength
, cipherGetDefaultNonceLength
, cipherGetUpdateGranularity
, cipherGetIdealUpdateGranularity
, cipherQueryKeylen
, cipherGetKeyspec
, cipherSetKey
, cipherReset
, cipherSetAssociatedData
, cipherStart
, cipherUpdate
, cipherEncrypt
, cipherDecrypt
, cipherClear

-- * Cipher modes
, CipherMode(..)
, cbcMode
, cfbMode
, cfbModeWith
, xtsMode

-- ** CBC padding
, CBCPaddingName(..)
, pattern PKCS7
, pattern OneAndZeros
, pattern X9_23
, pattern ESP
, pattern CTS
, pattern NoPadding

-- * AEAD
, AEADName(..)
, pattern ChaCha20Poly1305
, chaCha20Poly1305

-- * AEAD modes
, AEADMode(..)
, gcmMode
, gcmModeWith
, ocbMode
, ocbModeWith
, eaxMode
, eaxModeWith
, sivMode
, ccmMode
, ccmModeWith

-- * Convenience
, cipherEncryptOnline
, cipherDecryptOnline
, cipherModes
, cbcPaddings
, aeads
, allCiphers

) where

import qualified Data.ByteString as ByteString

import Botan.Bindings.Cipher

import Botan.Low.BlockCipher
import Botan.Low.Error
import Botan.Low.Make
import Botan.Low.Prelude
import Botan.Low.Remake

import Botan.Low.RNG

{- $introduction

A `cipher` mode is a cryptographic algorithm suitable for encrypting and
decrypting large quantities of arbitrarily-sized data. An `aead` is a cipher
mode that also used to provide authentication of the ciphertext, potentially
with plaintext `associated data`.

-}

{- $usage

Unless you need a specific `cipher` or `aead`, it is strongly recommended that you use the `cbcMode AES256 PKCS7` and `gcmMode AES256` (or `ChaCha20Poly1305`) algorithms respectively.

> import Botan.Low.Cipher
> encrypter <- cipherInit ChaCha20Poly1305 Encrypt

To use a cipher, we first need to generate (if we haven't already) a secret key.

> import Botan.Low.RNG
> rng <- rngInit "user"
> -- We will use the maximum key size; ChaCha20Poly1305 keys are always 32 bytes
> (_,keySize,_) <- cipherGetKeyspec encrypter
> -- Block cipher keys are randomly generated
> key <- rngGet rng keySize

After the key is generated, we must set it as the cipher key:

> cipherSetKey encrypter key

If the cipher is an `aead`, we may also set the `associated data`:

> cipherSetAssociatedData encrypter "Fee fi fo fum!"

To ensure that the key is not leaked, we should generate a new nonce for every encryption. The range of allowed nonce sizes depends on the specific algorithm.

> import Botan.Low.RNG
> -- The default ChaCha20Poly1305 nonce is always 12 bytes.
> nonceSize <- cipherGetDefaultNonceLength encrypter
> nonce <- rngGet rng nonceSize

To encrypt a message, it must be a multiple of the block size. If the cipher was an aead, the authentication tag will automatically be included in the ciphertext

> -- Rarely, some cipher modes require that the message size be aligned to the block size
> -- Consult algorithm-specific documentation if this occurs. 
> message = "I smell the blood of an Englishman!"
> cipherStart encrypter nonce
> ciphertext <- cipherEncrypt encrypter message

To decrypt a message, we run the same process with a decrypter, using the same `key` and `nonce` to decode the `ciphertext`:

> decrypter <- cipherInit ChaCha20Poly1305 Decrypt
> cipherSetKey decrypter key
> cipherSetAssociatedData decrypter "Fee fi fo fum!"
> cipherStart decrypter nonce
> plaintext <- cipherDecrypt decrypter ciphertext
> message == plaintext -- True

You can completely clear a cipher's state, leaving it ready for reuse:

> cipherClear encrypter
> -- You'll have to set the key, nonce, (and ad, if aead) again.
> cipherSetKey encrypter anotherKey
> cipherStart encrypter anotherNonce
> cipherSetAssociatedData encrypter anotherAD
> -- Process another message
> anotherCiphertext <- cipherEncrypt encrypter anotherMessage

If you are encrypting or decrypting multiple messages with the same key, you can reset the cipher instead of clearing it, leaving the key set:

> cipherClear encrypter
> -- This is equivalent to calling cipherClear followed by cipherSetKey with the original key.
> -- You'll have to set the nonce  (and ad, if aead) again, but not the key.
> cipherStart encrypter anotherNonce
> cipherSetAssociatedData encrypter anotherAD
> -- Process another message with the same key
> anotherCiphertext <- cipherEncrypt encrypter anotherMessage

-}

-- NOTE: This is *symmetric* ciphers  For the 'raw' interface to ECB mode block ciphers, see BlockCipher.hs

newtype Cipher = MkCipher { getCipherForeignPtr :: ForeignPtr BotanCipherStruct }

newCipher      :: BotanCipher -> IO Cipher
withCipher     :: Cipher -> (BotanCipher -> IO a) -> IO a
-- | Destroy the cipher object immediately
cipherDestroy  :: Cipher -> IO ()
createCipher   :: (Ptr BotanCipher -> IO CInt) -> IO Cipher
(newCipher, withCipher, cipherDestroy, createCipher, _)
    = mkBindings
        MkBotanCipher runBotanCipher
        MkCipher getCipherForeignPtr
        botan_cipher_destroy

type CipherInitFlags = Word32
type CipherUpdateFlags = Int
type CipherNonce = ByteString
type CipherKey = ByteString

type CipherName = ByteString

type CipherMode = ByteString

type CBCPaddingName = ByteString

pattern PKCS7
    ,   OneAndZeros
    ,   X9_23
    ,   ESP
    ,   CTS
    ,   NoPadding
    ::  CBCPaddingName

pattern PKCS7       = BOTAN_CBC_PADDING_PKCS7
pattern OneAndZeros = BOTAN_CBC_PADDING_ONE_AND_ZEROS
pattern X9_23       = BOTAN_CBC_PADDING_X9_23
pattern ESP         = BOTAN_CBC_PADDING_ESP
pattern CTS         = BOTAN_CBC_PADDING_CTS
pattern NoPadding   = BOTAN_CBC_PADDING_NO_PADDING

cbcMode :: BlockCipherName -> CBCPaddingName -> CipherName 
cbcMode bc padding = bc // BOTAN_CIPHER_MODE_CBC // padding

cfbMode :: BlockCipherName -> CipherName 
cfbMode bc = bc // BOTAN_CIPHER_MODE_CFB

cfbModeWith :: BlockCipherName -> Int -> CipherName 
cfbModeWith bc feedbackSz = cfbMode bc /$ showBytes feedbackSz

xtsMode :: BlockCipherName -> CipherName
xtsMode bc = bc // BOTAN_CIPHER_MODE_XTS

type AEADName = CipherName

pattern ChaCha20Poly1305 :: CipherName
pattern ChaCha20Poly1305 = BOTAN_AEAD_CHACHA20POLY1305

chaCha20Poly1305 :: AEADName
chaCha20Poly1305 = BOTAN_AEAD_CHACHA20POLY1305

type AEADMode = ByteString

gcmMode :: BlockCipher128Name -> AEADName
gcmMode bc = bc // BOTAN_AEAD_MODE_GCM

gcmModeWith :: BlockCipher128Name -> Int -> AEADName
gcmModeWith bc tagSz = gcmMode bc /$ showBytes tagSz

ocbMode :: BlockCipher128Name -> AEADName
ocbMode bc = bc // BOTAN_AEAD_MODE_OCB

ocbModeWith :: BlockCipher128Name -> Int -> AEADName
ocbModeWith bc tagSz = ocbMode bc /$ showBytes tagSz

eaxMode :: BlockCipherName -> AEADName
eaxMode bc = bc // BOTAN_AEAD_MODE_EAX

eaxModeWith :: BlockCipherName -> Int -> AEADName
eaxModeWith bc tagSz = eaxMode bc /$ showBytes tagSz

sivMode :: BlockCipher128Name -> AEADName
sivMode bc = bc // BOTAN_AEAD_MODE_SIV

ccmMode :: BlockCipher128Name -> AEADName
ccmMode bc = bc // BOTAN_AEAD_MODE_CCM

ccmModeWith :: BlockCipher128Name -> Int -> Int -> AEADName
ccmModeWith bc tagSz l = ccmMode bc /$ showBytes tagSz <> "," <> showBytes l

cbcPaddings =
    [ PKCS7
    , OneAndZeros
    , X9_23
    , ESP
    , CTS
    , NoPadding
    ]

cipherModes = concat
    [ [ cbcMode bc pd | bc <- allBlockCiphers, pd <- cbcPaddings ]
    , [ cfbMode bc    | bc <- allBlockCiphers ]
    , [ xtsMode bc    | bc <- allBlockCiphers ]
    ]

aeads = concat
    [ [ chaCha20Poly1305 ]
    , [ gcmMode bc | bc <- blockCipher128s ]
    , [ ocbMode bc | bc <- blockCipher128s ]
    , [ eaxMode bc | bc <- blockCiphers ] -- WARNING: Why just blockCiphers, why not allBlockCiphers?
    , [ sivMode bc | bc <- blockCipher128s ]
    , [ ccmMode bc | bc <- blockCipher128s ]
    ]

allCiphers = cipherModes ++ aeads

-- TODO: Rename CipherMaskDirection, CipherEncrypt, CipherDecrypt;
--  Leave slim terminology for botan
pattern MaskDirection
    ,   Encrypt         -- ^ May be renamed Encipher to avoid confusion with PKEncrypt
    ,   Decrypt         -- ^ May be renamed Decipher to avoid confusion with PKDecrypt
    ::  CipherInitFlags

pattern MaskDirection = BOTAN_CIPHER_INIT_FLAG_MASK_DIRECTION
pattern Encrypt = BOTAN_CIPHER_INIT_FLAG_ENCRYPT
pattern Decrypt = BOTAN_CIPHER_INIT_FLAG_DECRYPT

pattern CipherUpdate
    ,   CipherFinal
    ::  CipherUpdateFlags

pattern CipherUpdate    = BOTAN_CIPHER_UPDATE_FLAG_NONE
pattern CipherFinal     = BOTAN_CIPHER_UPDATE_FLAG_FINAL

-- |Initialize a cipher object
cipherInit
    :: CipherName       -- ^ __name__
    -> CipherInitFlags  -- ^ __flags__
    -> IO Cipher        -- ^ __cipher__
cipherInit = mkCreateObjectCString1 createCipher botan_cipher_init

-- WARNING: withFooInit-style limited lifetime functions moved to high-level botan
withCipherInit :: CipherName -> CipherInitFlags -> (Cipher -> IO a) -> IO a
withCipherInit = mkWithTemp2 cipherInit cipherDestroy

-- |Return the name of the cipher object
cipherName
    :: Cipher           -- ^ __cipher__
    -> IO CipherName    -- ^ __name__
cipherName = mkGetCString withCipher botan_cipher_name

-- |Return the output length of this cipher, for a particular input length.
--
--  WARNING: This function is of limited use. From the C++ docs:
--   /**
--   * Returns the size of the output if this transform is used to process a
--   * message with input_length bytes. In most cases the answer is precise.
--   * If it is not possible to precise (namely for CBC decryption) instead an
--   * upper bound is returned.
--   */
--  We need to explicitly calculate padding + tag length
cipherOutputLength
    :: Cipher   -- ^ __cipher__
    -> Int      -- ^ __in_len__
    -> IO Int   -- ^ __out_len__
cipherOutputLength = mkGetSize_csize withCipher botan_cipher_output_length

-- NOTE: Unique function form?
-- |Return if the specified nonce length is valid for this cipher
-- NOTE: This just always seems to return 'True', even for -1 and maxBound
cipherValidNonceLength
    :: Cipher   -- ^ __cipher__
    -> Int      -- ^ __nl__
    -> IO Bool
cipherValidNonceLength = mkGetBoolCode_csize withCipher botan_cipher_valid_nonce_length

-- |Get the tag length of the cipher (0 for non-AEAD modes)
cipherGetTagLength
    :: Cipher   -- ^ __cipher__
    -> IO Int   -- ^ __tag_size__
cipherGetTagLength = mkGetSize withCipher botan_cipher_get_tag_length

-- |Get the default nonce length of this cipher
cipherGetDefaultNonceLength
    :: Cipher   -- ^ __cipher__
    -> IO Int   -- ^ __nl__
cipherGetDefaultNonceLength = mkGetSize withCipher botan_cipher_get_default_nonce_length

-- |Return the update granularity of the cipher; botan_cipher_update must be
--  called with blocks of this size, except for the final.
cipherGetUpdateGranularity
    :: Cipher   -- ^ __cipher__
    -> IO Int   -- ^ __ug__
cipherGetUpdateGranularity = mkGetSize withCipher botan_cipher_get_update_granularity

-- |Return the ideal update granularity of the cipher. This is some multiple of the
--  update granularity, reflecting possibilities for optimization.
--
-- Some ciphers (ChaChaPoly, EAX) may consume less input than the reported ideal granularity
cipherGetIdealUpdateGranularity
    :: Cipher   -- ^ __cipher__
    -> IO Int   -- ^ __ug__
cipherGetIdealUpdateGranularity = mkGetSize withCipher botan_cipher_get_ideal_update_granularity

-- |Get information about the key lengths.
cipherQueryKeylen
    :: Cipher       -- ^ __cipher__
    -> IO (Int,Int) -- ^ __(min,max)__
cipherQueryKeylen = mkGetSizes2 withCipher botan_cipher_query_keylen
{-# DEPRECATED cipherQueryKeylen "Prefer cipherGetKeyspec." #-}

-- |Get information about the supported key lengths.
cipherGetKeyspec
    :: Cipher           -- ^ __cipher__
    -> IO (Int,Int,Int) -- ^ __(min,max,mod)__
cipherGetKeyspec = mkGetSizes3 withCipher botan_cipher_get_keyspec

-- |Set the key for this cipher object
cipherSetKey
    :: Cipher       -- ^ __cipher__
    -> ByteString   -- ^ __key__
    -> IO ()
cipherSetKey = mkWithObjectSetterCBytesLen withCipher botan_cipher_set_key

-- |Reset the message specific state for this cipher.
--  Without resetting the keys, this resets the nonce, and any state
--  associated with any message bits that have been processed so far.
--  
--  It is conceptually equivalent to calling botan_cipher_clear followed
--  by botan_cipher_set_key with the original key.
cipherReset
    :: Cipher   -- ^ __cipher__
    -> IO ()
cipherReset = mkAction withCipher botan_cipher_reset

-- |Set the associated data. Will fail if cipher is not an AEAD
cipherSetAssociatedData
    :: Cipher       -- ^ __cipher__
    -> ByteString   -- ^ __ad__
    -> IO ()
cipherSetAssociatedData = mkWithObjectSetterCBytesLen withCipher botan_cipher_set_associated_data

-- |Begin processing a new message using the provided nonce
cipherStart
    :: Cipher       -- ^ __cipher__
    -> ByteString   -- ^ __nonce__
    -> IO ()
cipherStart = mkWithObjectSetterCBytesLen withCipher botan_cipher_start

-- |"Encrypt some data"
--
--  This function is ill-documented.
--
--  See the source for authoritative details:
--  https://github.com/randombit/botan/blob/72dc18bbf598f2c3bef81a4fb2915e9c3c524ac4/src/lib/ffi/ffi_cipher.cpp#L133
--
-- Some ciphers (ChaChaPoly, EAX) may consume less input than the reported ideal granularity
cipherUpdate
    :: Cipher               -- ^ __cipher__
    -> CipherUpdateFlags    -- ^ __flags__
    -> Int                  -- ^ __output_size__
    -> ByteString           -- ^ __input_bytes[]__
    -> IO (Int,ByteString)  -- ^ __(input_consumed,output[])__
cipherUpdate ctx flags outputSz input = withCipher ctx $ \ ctxPtr -> do
    unsafeAsBytesLen input $ \ inputPtr inputSz -> do
        alloca $ \ consumedPtr -> do
            alloca $ \ writtenPtr -> do
                output <- allocBytes outputSz $ \ outputPtr -> do
                    throwBotanIfNegative_ $ botan_cipher_update
                        ctxPtr
                        (fromIntegral flags)
                        outputPtr
                        (fromIntegral outputSz)
                        writtenPtr
                        (ConstPtr inputPtr)
                        inputSz
                        consumedPtr
                consumed <- fromIntegral <$> peek consumedPtr
                written <- fromIntegral <$> peek writtenPtr
                -- NOTE: The safety of this function is suspect - may require deepseq
                let processed = ByteString.take written output
                    in processed `seq` return (consumed,processed)

{- |
Encrypt and finalize a complete piece of data.

This is not a canonical Botan C/C++ function.
-}
cipherEncrypt :: Cipher -> ByteString -> IO ByteString
cipherEncrypt = cipherEncryptOffline

{- |
Encrypt and finalize a complete piece of data.

This is not a canonical Botan C/C++ function.
-}
cipherDecrypt :: Cipher -> ByteString -> IO ByteString
cipherDecrypt = cipherDecryptOffline

-- |Reset the key, nonce, AD and all other state on this cipher object
cipherClear :: Cipher -> IO ()
cipherClear = mkAction withCipher botan_cipher_clear

{-
Non-standard functions
-}

-- NOTE: out + ug + tag is safe overestimate for encryption
-- NOTE: out + ug - tag may not be a safe overestimate for decryption
{-# DEPRECATED cipherEstimateOutputLength "This will be moved from botan-low to botan" #-}
cipherEstimateOutputLength :: Cipher -> CipherInitFlags -> Int -> IO Int
cipherEstimateOutputLength ctx flags input = do
    o <- cipherOutputLength ctx input  -- NOTE: Flawed but usable
    u <- cipherGetUpdateGranularity ctx -- TODO: When u == 1, it should be just input + t, right?
    t <- cipherGetTagLength ctx
    if flags == BOTAN_CIPHER_INIT_FLAG_ENCRYPT
        then return (o + u + t)
        else return (o + u - t) -- TODO: Maybe just 'o'... 

-- NOTE: Offset must be a valid length of the input so far processed
-- NOTE: If (estimated) outputLength input + offset == outputLength (input + offset) then
--  we can just use cipherEstimateOutputLength instead of this
--  However, this may not be completely true due to block alignment requirements
--  For the moment this function exists for clarity.
{-# DEPRECATED cipherEstimateFinalOutputLength "Moving from botan-low to botan" #-}
cipherEstimateFinalOutputLength :: Cipher -> CipherInitFlags -> Int -> Int -> IO Int
cipherEstimateFinalOutputLength ctx flags offset input = do
    len <- cipherEstimateOutputLength ctx flags (offset + input)
    return $ len - offset

-- A better version of cipherUpdate
-- NOTE: It returns (processed,remaining) compared to (consumed,processed)
--  so the processed ciphertext has moved from snd to fst
-- TODO: Use Builder to do this
--  https://hackage.haskell.org/package/bytestring-0.12.0.2/docs/Data-ByteString-Builder.html
-- NOTE: There still is (an efficiency) use for a version that reports only consumed length
--  and defers the computation of the 'remaining' bytestring
{-# DEPRECATED cipherProcess "Moving from botan-low to botan" #-}
cipherProcess :: Cipher -> CipherUpdateFlags -> Int -> ByteString -> IO (ByteString,ByteString)
cipherProcess ctx flags outputSz input = do
    (consumed,processed) <- cipherUpdate ctx flags outputSz input
    -- NOTE: The safety of this function is suspect - may require deepseq
    let remaining = ByteString.drop consumed input
        in processed `seq` remaining `seq` return (processed,remaining)

{-# DEPRECATED cipherProcessOffline "Moving from botan-low to botan" #-}
cipherProcessOffline :: Cipher -> CipherInitFlags -> ByteString -> IO ByteString
cipherProcessOffline ctx flags msg = do
    o <- cipherEstimateOutputLength ctx flags (ByteString.length msg)
    -- snd <$> cipherUpdate ctx BOTAN_CIPHER_UPDATE_FLAG_FINAL o msg
    fst <$> cipherProcess ctx BOTAN_CIPHER_UPDATE_FLAG_FINAL o msg

{-# WARNING cipherEncryptOffline "May be renamed to cipherEncrypt, may be moved to botan" #-}
cipherEncryptOffline :: Cipher -> ByteString -> IO ByteString
cipherEncryptOffline ctx = cipherProcessOffline ctx BOTAN_CIPHER_INIT_FLAG_ENCRYPT

{-# WARNING cipherDecryptOffline "May be renamed to cipherDecrypt, may be moved to botan" #-}
cipherDecryptOffline :: Cipher -> ByteString -> IO ByteString
cipherDecryptOffline ctx = cipherProcessOffline ctx BOTAN_CIPHER_INIT_FLAG_DECRYPT

{-
Experiments with online processing
-}

-- cipherEncryptOnline :: Cipher -> ByteString -> IO ByteString
-- cipherEncryptOnline ctx msg = do
--     u <- cipherGetUpdateGranularity ctx
--     t <- cipherGetTagLength ctx
--     g <- cipherGetIdealUpdateGranularity ctx
--     ByteString.concat <$> go 0 u t g msg
--     where
--         go i u t g bs = case ByteString.splitAt g bs of
--             (block,"")      -> do
--                 o <- cipherOutputLength ctx (i + ByteString.length block)  -- NOTE: Flawed but usable
--                 (_,encblock) <- cipherUpdate ctx BOTAN_CIPHER_UPDATE_FLAG_FINAL (o + u + t - i) block
--                 return [encblock]
--             (block,rest)    -> do
--                 (_,encblock) <- cipherUpdate ctx BOTAN_CIPHER_UPDATE_FLAG_NONE g block
--                 encrest <- go (i + g) u t g rest
--                 return $! encblock : encrest

--  NOTE: Some ciphers (SIV, CCM) are not online-capable algorithms, but Botan does not throw
--  an error even though it should.
{-# DEPRECATED cipherProcessOnline "Moving from botan-low to botan" #-}
cipherProcessOnline :: Cipher -> CipherInitFlags -> ByteString -> IO ByteString
cipherProcessOnline ctx flags = if flags == BOTAN_CIPHER_INIT_FLAG_ENCRYPT
    then cipherEncryptOnline ctx
    else cipherDecryptOnline ctx

-- TODO: Consolidate online encipher / decipher
-- TODO: Use Builder to do this
--  https://hackage.haskell.org/package/bytestring-0.12.0.2/docs/Data-ByteString-Builder.html
{-# DEPRECATED cipherEncryptOnline "Moving from botan-low to botan" #-}
cipherEncryptOnline :: Cipher -> ByteString -> IO ByteString
cipherEncryptOnline ctx msg = do
    g <- cipherGetIdealUpdateGranularity ctx
    ByteString.concat <$> go 0 g msg
    where
        go i g bs = case ByteString.splitAt g bs of
            (block,"")      -> do
                o <- cipherEstimateFinalOutputLength ctx BOTAN_CIPHER_INIT_FLAG_ENCRYPT i (ByteString.length block)
                (processed,_) <- cipherProcess ctx BOTAN_CIPHER_UPDATE_FLAG_FINAL o block
                return [processed]
            (block,rest)    -> do
                (processed,remaining) <- cipherProcess ctx BOTAN_CIPHER_UPDATE_FLAG_NONE g block
                (processed :) <$> go (i + g) g (remaining <> rest)
                -- Though this following version may be more efficient especially with lazy bytestrings
                --  or builder, though note *which* update function it uses - the original
                -- (consumed,processed) <- cipherUpdate ctx BOTAN_CIPHER_UPDATE_FLAG_NONE g block
                -- (processed :) <$> go (i + g) g (ByteString.drop consumed bs)

-- TODO: Consolidate online encipher / decipher
{-# DEPRECATED cipherDecryptOnline "Moving from botan-low to botan" #-}
cipherDecryptOnline :: Cipher -> ByteString -> IO ByteString
cipherDecryptOnline ctx msg = do
    g <- cipherGetIdealUpdateGranularity ctx
    t <- cipherGetTagLength ctx
    ByteString.concat <$> go 0 g t msg
    where
        go i g t bs = case ByteString.splitAt g bs of
            (block,"")      -> do
                o <- cipherEstimateFinalOutputLength ctx BOTAN_CIPHER_INIT_FLAG_DECRYPT i (ByteString.length block)
                (processed,_) <- cipherProcess ctx BOTAN_CIPHER_UPDATE_FLAG_FINAL o block
                return [processed]
            (block,rest)    -> do
                (processed,remaining) <- cipherProcess ctx BOTAN_CIPHER_UPDATE_FLAG_NONE g block
                (processed :) <$> go (i + g) g t (remaining <> rest)