Skip to content

haskell-cryptography/botan

Repository files navigation

Welcome to botan

CI

botan is a set of Haskell bindings for the Botan cryptography library.

Botan's goal is to be the best option for cryptography in C++ by offering the tools necessary to implement a range of practical systems, such as TLS protocol, X.509 certificates, modern AEAD ciphers, PKCS#11 and TPM hardware support, password hashing, and post quantum crypto schemes.

Acknowledgements

This project has received support from the Haskell Foundation, and was made possible through funding provided by Mercury.

Navigation

Expand / Collapse Navigation

Introduction

This project has the goal of providing a set of safe and performant bindings to the Botan C++ cryptography library via its C FFI (Foreign Function Interface). It does this by providing 3 libraries at varying levels of complexity and abstraction:

  • botan-bindings contains raw bindings with buffers and pointers, and is otherwise an almost a 1:1 translation of the C FFI into Haskell
  • botan-low contains low-level bindings with imperative IO and exceptions, and safely wraps buffers and pointers into bytestrings and autoreleased objects
  • botan contains high-level bindings with strong types and idiomatic Haskell, and provides algebraic data types and convenience functions

We suggest using the highest-level library possible, unless you wish to build your own abstraction over the Botan C++ library. The highest-level stable library is currently: botan-low

Features

This library provides its bindings through the Botan C FFI, which does not currently cover the entire range of features available in C++. As such, it provides a sizeable yet limited subset of those features.

These bindings provide the following features:

  • Random number generators
  • Hashing and non-cryptographic checksums
  • Message authentication codes
  • Block, mode, and AEAD ciphers
  • Password hashing and key derivation functions
  • Hash- and time-based one-time passwords
  • Post-quantum crypto schemes
  • SRP6 password authenticated key exchange
  • X.509 certificate systems (experimental)
  • ZFEC forward error correction

NOTE: This project includes efforts to expand this subset of features through a fork of the Botan C++ library - see Enabling experimental support for more details. This fork will be contributed back to the original library when it becomes sufficiently stable.

Installation

This library requires Botan 3 to be installed in order to work. See Building the Library in the handbook for more details.

Unix package

Botan is available already in nearly all packaging systems so you can probably install it through your distribution / system package manager.

At current, Botan 2 is readily available, but Botan 3 is not yet available as a prebuilt linux package. Please see 'Building from source'.

Arch

Untested.

pacman -S botan
# or maybe
pacman -S botan3
Debian

Untested.

apt-get update
apt-get install botan
Red Hat

Untested.

yum update
yum install botan
Ubuntu

Untested.

sudo apt update
sudo apt install botan

MacOS package

Botan is available through the Homebrew package manager:

Homebrew
brew install botan

From source

Botan can be built from source, for additional configuration options and customization.

Build from source

First, clone the Botan C++ repository:

git clone https://github.com/randombit/botan

Botan’s build configuration is controlled by configure.py, and requires Python 3.x or later.

This works for most systems:

./configure.py [--prefix=/some/directory]
make
make install

If you wish to run unit tests before installation, run make check before make install.

On platforms that do not understand the #! convention for beginning script files, or that have Python installed in an unusual spot, you might need to prefix the configure.py command with python3 or /path/to/python:

python3 ./configure.py [arguments]

The make install target has a default directory in which it will install Botan (typically /usr/local). You can override this by using the --prefix argument to configure.py, like so:

./configure.py --prefix=/botan [arguments]

Some features rely on third party libraries which your system might not have or that you might not want the resulting binary to depend on. For instance to enable sqlite3 support, add --with-sqlite3 to your invocation of configure.py.

./configure.py --with-sqlite3 [arguments]

Windows from source

Prebuilt botan is not available for windows, and it must be built from source. The process is similar to building from source in Unix or MacOS.

Windows from source

Untested.

You need to have a copy of Python installed, and have both Python and your chosen compiler in your path. Open a command shell (or the SDK shell), and run:

python3 configure.py --cc=msvc --os=windows
nmake
nmake check
nmake install

Alternately, starting in Botan 3.2, there is additionally support for using the ninja build tool as an alternative to nmake:

python3 configure.py --cc=msvc --os=windows --build-tool=ninja
ninja
ninja check
ninja install

For MinGW, use:

python3 configure.py --cc=gcc --os=mingw
make

By default the install target will be C:\botan; you can modify this with the --prefix option.

When building your applications, all you have to do is tell the compiler to look for both include files and library files in C:\botan, and it will find both. Or you can move them to a place where they will be in the default compiler search paths (consult your documentation and/or local expert for details).

Usage

You will need to add botan as a package dependency in order to use it.

Cabal

This package is now available on hackage. Simply add it to your [project].cabal under the build-depends stanza:

build-depends:
    botan-low

If you installed botan in a non-standard location, you may need to specify where using --extra-include-dirs and --extra-lib-dirs flags:

cabal repl TARGET --extra-include-dirs /botan/include --extra-lib-dirs /botan/lib

You can add these flags permanently to your cabal.project or cabal.project.local file:

extra-include-dirs:
- /botan/include
extra-lib-dirs:
- /botan/lib
Stack

Untested.

This package is not yet available on stackage, so you'll have to add the repo to your stack.yaml file under the extra-deps stanza:

extra-deps:
- github: haskellfoundation/botan
  commit: [commit-hash]

Then, add it to your package.yaml file under the dependencies stanza:

dependencies:
- botan-low

If you installed botan in a non-standard location, you may need to specify where using --extra-include-dirs and --extra-lib-dirs flags:

stack repl TARGET --extra-include-dirs /botan/include --extra-lib-dirs /botan/lib

You can add these flags permanently to your stack.yaml or global config.yaml file:

extra-include-dirs:
- /botan/include
extra-lib-dirs:
- /botan/lib

After you have added botan as a dependency, you can begin importing modules and using them in your code.

import Botan.Low.Hash

main = do
    hash <- hashInit "SHA-256"
    digest <- hashUpdateFinalize hash "Fee fi fo fum!"
    print digest

Tutorials

Botan.Low.Bcrypt

bcrypt is a password-hashing algorithm designed to protect your passwords against hackers using an expensive key setup phase. Instead of storing a user's password in plaintext in the database, the server may instead generate a salted bcrypt digest upon signup, and verify it upon login.

The bcrypt implementation provided by botan generates a random salt for you automatically. A work factor of 12 or greater is recommended.

To generate a bcrypt digest:

import Botan.Low.RNG
import Botan.Low.Bcrypt

-- The user has sent us a username and password in order to sign up 
onUserSignup :: ByteString -> ByteString -> IO ()
onUserSignup username password = do
    rng <- rngInit "user"
    digest <- bcryptGenerate password rng 12
    createAndStoreNewUser username digest

To validate a bcrypt digest:

import Botan.Low.RNG
import Botan.Low.Bcrypt

-- The user has sent us a username and password in order to log in
onUserLogin :: ByteString -> ByteString -> IO Bool
onUserLogin username password = do
    rng <- rngInit "user"
    digestMaybe <- getStoredUserDigest username
    case digestMaybe of
        Nothing     -> return False
        Just digest -> bcryptIsValid password digest
Botan.Low.BlockCipher

A block cipher is a deterministic, cryptographic primitive suitable for encrypting or decrypting a single, fixed-size block of data at a time. Block ciphers are used as building blocks for more complex cryptographic operations. If you are looking to encrypt user data, you are probably looking for Botan.Low.Cipher instead.

Unless you need a specific block cipher, it is strongly recommended that you use the AES256 algorithm.

import Botan.Low.BlockCipher
blockCipher <- blockCipherInit AES256

To use a block 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; AES256 keys are always 16 bytes
(_,keySize,_) <- blockCipherGetKeyspec blockCipher
-- Block cipher keys are randomly generated
key <- rngGet rng keySize

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

blockCipherSetKey blockCipher key

To encrypt a message, it must be a multiple of the block size.

blockSize <- blockCipherBlockSize blockCipher
-- AES256 block size is always 16 bytes
message = "0000DEADBEEF0000" :: ByteString
ciphertext <- blockCipherEncryptBlocks blockCipher message

To decrypt a message, simply reverse the process:

plaintext <- blockCipherDecryptBlocks blockCipher ciphertext
message == plaintext -- True
Botan.Low.Cipher

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.

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
Botan.Low.Hash

A hash is deterministic, one-way function suitable for producing a deterministic, fixed-size digest from an arbitrarily-sized message, which is used to verify the integrity of the data.

Unless you need a specific hash, it is strongly recommended that you use the SHA3 algorithm.

import Botan.Low.Hash
hash <- hashInit SHA3
message = "Fee fi fo fum!"
hashUpdate hash message
digest <- hashFinal hash

You can verify a digest by hashing the message a second time, and comparing the two:

rehash <- hashInit SHA3
hashUpdate rehash message
redigest <- hashFinal rehash
digest == redigest -- True

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

hashClear hash
-- Process another message
hashUpdate hash anotherMessage
anotherDigest <- hashFinal hash
Botan.Low.HOTP

To use HOTP for MFA / 2FA, the client authenticator must generate a client-specific shared secret and counter value, and securely communicate them to the server authenticator.

The secret key may be any bytestring value with more than 160 bits, such as a Bcrypt digest or SRP6 shared key. The counter value is not required to be a secret; it may start at 0 for simplicity, or it may start at a value that was selected at random.

import Botan.Low.HOTP
import Botan.Low.RNG
import Botan.Low.MPI
import Data.Bits
sharedSecret <- systemRNGGet 16
-- TODO: Use random:System.Random.Stateful.Uniform instead of MPI in `botan`
(hi :: Word32) <- mpInit >>= \ w -> mpRandBits w rng 32 >> mpToWord32 w
(lo :: Word32) <- mpInit >>= \ w -> mpRandBits w rng 32 >> mpToWord32 w
(counter :: Word64) = shiftL (fromIntegral hi) 32 `xor` fromIntegral lo 

The client and server authenticators are now in a shared state, and any login attempts from a new device may be authenticated using HOTP as MFA.

A client has requested a new connection, and HOTP is being used as MFA/2FA to authenticate their request. The server authenticator receives the client connection request and initializes a HOTP session using the stored client-specific shared secret, and then sends an authentication request to the client authenticator:

-- (serverSharedSecret, serverCounter) <- lookupServerSharedSecretAndCounter
serverSession <- hotpInit serverSharedSecret HOTP_SHA512 8
-- sendMFAAuthenticationRequest

The client authenticator receives the authentication request, generates a client-side code, increments their counter, and displays the HOTP code to the user:

-- (clientSharedSecret, clientCounter) <- lookupClientSharedSecretAndCounter
clientSession <- hotpInit clientSharedSecret HOTP_SHA512 8
clientCode <- hotpGenerate clientSession clientCounter
-- incrementAndPersistClientCounter
-- displayClientCode clientCode

NOTE: The client authenticator is responsible for incrementing and persisting their own counter manually.

The client then sends the client code to the server authenticator using the unauthenticated / requested connection:

-- clientCode <- readClientCode
-- sendMFAAuthenticationResponse clientCode

The server authenticator receives the authentication response, and performs a check of the key, with a resync range of R in case the client has generated a few codes without logging in successfully:

-- serverClientCode <- didreceiveMFAAuthenticationResponse
(isValid,nextCounter) <- hotpCheck serverSession serverClientCode serverCounter 10
persistClientCounter nextCounter

NOTE: The server authentication check returns an incremented and resynced counter which must be persisted manually. If the authentication check fails, the counter value is return unchanged.

If the code is valid, then the signin may be completed on the new connection as normal.

The server should discontinue the session and refuse any new connections to the account after T unsuccessful authentication attempts, where T < R. The user should then be notified.

Botan.Low.MAC

A mac (or message authentication code) is a cryptographic algorithm that uses a secret key to produce a fixed-size digest from an arbitrarily-sized message, which is then used to verify the integrity and authenticity of the data.

Unless you need a specific mac, it is strongly recommended that you use the hmac SHA3 algorithm.

import Botan.Low.MAC
import Botan.Low.Hash
mac <- macInit (hmac SHA3)

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

import Botan.Low.RNG
rng <- rngInit "user"
-- HMAC allows for an arbitrary key size, but we can check the key spec if necessary
(keyMin,keyMax,keyMod) <- macGetKeyspec mac
-- MAC are randomly generated; 32 bytes is acceptable
key <- rngGet rng 32

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

macSetKey mac key

Then, we may produce an authentication code from a message using the secret key:

macUpdate mac "Fee fi fo fum!"
auth <- macFinal mac

To verify an message authentication code, we can reproduce it using the secret key and message, and then check for equality:

verify <- macInit (hmac SHA3)
macSetKey verify key
macUpdate verify "Fee fi fo fum!"
verifyAuth <- macFinal verify
auth == verifyAuth -- True

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

macClear mac
-- You'll have to set the key again
macSetKey mac anotherKey
-- Process another message
macUpdate mac anotherMessage
anotherAuth <- macFinal mac

Some algorithms (GMAC, Poly1305) have additional requirements for use. Avoid if possible, and consult algorithm-specific documentation for GMAC and Poly1305. If you must use GMAC, a nonce needs to be set:

mac <- macInit (gmac AES256)
k <- systemRNGGet 32
n <- systemRNGGet 32    -- Here
macSetKey mac k
macSetNonce mac n       -- Here
macUpdate mac "Fee fi fo fum!"
auth <- macFinal mac
Botan.Low.PubKey

Unless you need a specific public key cryptosystem, it is strongly recommended that you use the RSA, Ed25519, or Curve25519 algorithms, depending on your desired operation.

Create an RSA keypair:

import Botan.Low.RNG
import Botan.Low.PubKey
rng <- rngInit "user"
-- Alice generates her keys
alicePrivKey <- privKeyCreate RSA "4096" rng
alicePubKey <- privKeyExportPubKey alicePrivKey

NOTE: For algorithm-specific parameters, consult the Botan documentation and source

Botan.Low.PubKey.Encrypt

Encrypt a message:

import Botan.Low.PubKey.Encrypt
message = "Fee fi fo fum!"
-- Bob encrypts a message for Alice using her public key
-- Unlike `Crypto.Saltine.Core.Box`, the message is only encrypted, not signed.
encrypter <- encryptCreate alicePubKey EME_PKCS1_v1_5
ciphertext <- encrypt encrypter rng message

NOTE: For algorithm-specific padding parameters, consult the Botan documentation and source

Decrypt a message:

import Botan.Low.PubKey.Decrypt
-- Alice decrypts the message from Bob using her private key
decrypter <- decryptCreate alicePrivKey EME_PKCS1_v1_5
plaintext <- decrypt decrypter ciphertext
message == plaintext -- True

NOTE: The same padding must be used for both encryption and decryption

Botan.Low.PubKey.Sign

Sign a message:

import Botan.Low.PubKey.Sign
import Botan.Low.Hash
message = "Fee fi fo fum!"
-- Alice signs the message using her private key
signer <- signCreate alicePrivKey (emsa_emsa4 SHA3) StandardFormatSignature
signUpdate signer message
sig <- signFinish signer rng

NOTE: Signing uses a different set of padding algorithms EMSA from encryption EME, and different signing / encryption algorithms support different specific padding algorithms

NOTE: Signing does not yet have proper constants for selecting a padding mechanism. For more information, refer to the Botan.PubKey, Botan.PubKey.Sign, or the Botan C++ documentation. This area will be improved in the near future.

Verify a message:

import Botan.Low.PubKey.Verify
-- Bob verifies the message using Alice's public key
verifier <- verifyCreate alicePubKey (emsa_emsa4 SHA3) StandardFormatSignature
verifyUpdate verifier message
verified <- verifyFinish verifier sig
verified -- True

NOTE: The same padding must be used for both encryption and decryption

Botan.Low.PubKey.KeyAgreement

First, Alice and Bob generate their private keys:

import Botan.Low.PubKey
import Botan.Low.PubKey.KeyAgreement
import Botan.Low.RNG
import Botan.Low.Hash
import Botan.Low.KDF
rng <- rngInit "system"
-- Alice creates her private key
alicePrivKey <- privKeyCreate ECDH Secp521r1 rng 
-- Bob creates his private key
bobPrivKey <-  privKeyCreate ECDH Secp521r1 rng 

Then, they exchange their public keys using any channel, private or public:

-- Alice and Bob exchange public keys
alicePubKey <- keyAgreementExportPublic alicePrivKey
bobPubKey <- keyAgreementExportPublic bobPrivKey
-- ...

Then, they may separately generate the same agreed-upon key and a randomized, agreed-upon salt:

salt <- rngGet rng 4
-- Alice generates her shared key:
aliceKeyAgreement <- keyAgreementCreate alicePrivKey (kdf2 SHA256)
aliceSharedKey    <- keyAgreement aliceKeyAgreement bobPubKey salt
-- Bob generates his shared key:
bobKeyAgreement   <- keyAgreementCreate bobPrivKey (kdf2 SHA256)
bobSharedKey      <- keyAgreement bobKeyAgreement alicePubKey salt
-- They are the same
aliceSharedKey == bobSharedKey
-- True
Botan.Low.PubKey.KeyEncapsulation

First, Alice generates her private and public key pair:

import Botan.Low.PubKey
import Botan.Low.PubKey.KeyEncapsulation
import Botan.Low.Hash
import Botan.Low.KDF
import Botan.Low.RNG
rng <- rngInit UserRNG
-- Alice generates her private and public keys
alicePrivKey <- privKeyCreate RSA "2048" rng
alicePubKey <- privKeyExportPubKey alicePrivKey

Then, Alice shares her public key somewhere where others can see. When Bob wants to create a shared key with Alice, they choose a KDF algorithm, generate a salt, and choose a shared key length.

kdfAlg = hkdf SHA256
salt <- rngGet rng 4
sharedKeyLength = 256

Then, Bob generates the shared + encapsulated key, and sends the encapsulated key to Alice:

encryptCtx <- kemEncryptCreate alicePubKey kdfAlg
(bobSharedKey, encapsulatedKey) <- kemEncryptCreateSharedKey encryptCtx rng salt sharedKeyLength
-- sendToAlice encapsulatedKey

Upon receiving the encapsulated key, Alice can decrypt and extract the shared key using her private key:

decryptCtx <- kemDecryptCreate alicePrivKey kdfAlg
aliceSharedKey <- kemDecryptSharedKey decryptCtx salt encapsulatedKey sharedKeyLength
bobSharedKey == aliceSharedKey
-- True

Then, this shared key may be used for any suitable purpose.

Botan.Low.RNG

A random number generator is used to generate uniform samples of pseudorandom bytes.

You can always use the system RNG:

import Botan.Low.RNG
randomBytes <- systemRNGGet 16

Unless you need a specific RNG, it is strongly recommended that you use the autoseeded user RNG.

import Botan.Low.RNG
rng <- rngInit "user"
randomBytes <- rngGet rng 16

You can reseed a generator using the system generator:

rngReseed rng 64

You can also reseed a generator using a specific generator:

systemRNG <- rngInit "system"
rngReseedFromRNG rng systemRNG 64

You can also seed it with your own entropy; this is safe and can never decrease the amount of entropy in the generator.

rngAddEntropy rng "Fee fi fo fum!"
Botan.Low.SRP6

On signup, the client generates a salt and verifier, and securely sends them to a server:

import Botan.Low.SRP6
import Botan.Low.Hash
import Botan.Low.RNG
import Botan.Low.MAC
rng <- rngInit UserRNG
group = MODP_SRP_4096
hash = SHA512
identifier = "alice"
password = "Fee fi fo fum!"
salt <- rngGet rng 12
verifier <- srp6GenerateVerifier identifier password salt group hash
-- signUpUserWithServer identifier verifier salt group hash

Later, on the server when the client request authentication, the server looks up the verfier, generates a server key (a SRP6 'B' value), and sends it back to the client:

-- rng <- rngInit UserRNG
session <- srp6ServerSessionInit 
-- (verifier, salt, group, hash) <- lookupUser identifier
serverKey <- srp6ServerSessionStep1 session verifier group hash rng

Once the client receives the server key, it generates a client key (SRP6 'A' value) and the session key, and sends the client key to the server:

-- serverKey <- didReceiveServerKey
(clientKey, clientSessionKey) <- srp6ClientAgree identifier password group hash salt serverKey rng
-- sendClientKey clientKey

The server then receives client key, and generates a matching session key:

-- clientKey <- didReceiveClientKey
serverSessionKey <- srp6ServerSessionStep2 session clientKey

At this point, clientSessionKey and serverSessionKey should be equal, but this should be confirmed by exchanging a hash digest to check for integrity, using the exchange's session key, identifier, salt, and client and server keys.

There are many ways to do this, but preferrably, an (h)mac digest should be used to also include authentication and avoid impersonation.

NOTE: Both sides could calculate 'identifier <> salt <> serverKey <> clientKey' individually but then we need to prove that each side has calculated it without relying on the copy received for validation, so we do this song and dance:

The client should first calculate and send the HMAC auth, using identifier + salt + clientKey:

mac <- macInit (hmac SHA3)
macSetKey mac clientSessionKey
macUpdate mac $ identifier <> salt <> clientKey
clientAuth <- macFinal mac
-- sendClientAuth clientAuth

The server should then verify the client auth, and send its own HMAC auth back to the client using serverKey + clientAuth:

-- clientAuth <- didReceiveClientAuth
mac <- macInit (hmac SHA3)
macSetKey mac serverSessionKey
macUpdate mac $ identifier <> salt <> clientKey
verifiedClientAuth <- macFinal mac
-- clientAuth == verifiedClientAuth
macClear mac
macSetKey mac serverSessionKey
macUpdate mac $ serverKey <> clientAuth
serverAuth <- macFinal mac
-- sendServerAuth serverAuth

The client then receives the server HMAC auth, and validates it

-- serverAuth <- didReceiveServerAuth
macClear mac
macSetKey mac clientSessionKey
macUpdate mac $ serverKey <> clientAuth
verifiedServerAuth <- macFinal mac
-- serverAuth == verifiedServerAuth

After this, the shared session key may be safely used.

Botan.Low.TOTP

To use TOTP for MFA / 2FA, the client authenticator must generate a client-specific shared secret, and securely communicate it to the server authenticator.

The secret key may be any bytestring value with more than 160 bits, such as a Bcrypt digest or SRP6 shared key.

import Botan.Low.TOTP
import Botan.Low.RNG
import Data.Time.Clock.POSIX
timestep = 30
drift = 3
sharedSecret <- systemRNGGet 16

The client and server authenticators are now in a shared state, and any login attempts from a new device may be authenticated using TOTP as MFA.

A client has requested a new connection, and TOTP is being used as MFA/2FA to authenticate their request. The server authenticator receives the client connection request and initializes a TOTP session using the stored client-specific shared secret, and then sends an authentication request to the client authenticator:

-- serverSharedSecret <- lookupServerSharedSecret
serverSession <- totpInit serverSharedSecret TOTP_SHA512 8 timestep
-- sendMFAAuthenticationRequest

NOTE: We are using a timestep value of 30 seconds, which means that the code will refresh every 30 seconds

The client authenticator receives the authentication request, generates a client-side code using their timestamp, and displays the TOTP code to the user:

-- clientSharedSecret <- lookupClientSharedSecret
clientSession <- totpInit clientSharedSecret TOTP_SHA512 8 timestep
(clientTimestamp :: TOTPTimestamp) <- round <$> getPOSIXTime
clientCode <- totpGenerate clientSession clientTimestamp
-- displayClientCode clientCode

The client then sends the client code to the server authenticator using the unauthenticated / requested connection:

-- clientCode <- readClientCode
-- sendMFAAuthenticationResponse clientCode

The server authenticator receives the authentication response, and performs a check of the key, with an acceptable clock drift in steps, in case the client and server are slightly desynchronized.

-- serverClientCode <- didreceiveMFAAuthenticationResponse
(serverTimestamp :: TOTPTimestamp) <- round <$> getPOSIXTime
isValid <- totpCheck serverSession serverClientCode serverTimestamp drift

NOTE: We are using a acceptable clock drift value of 3, which means that the codes for the previous 3 time steps are still valid.

If the code is valid, then the signin may be completed on the new connection as normal.

The server should discontinue the session and refuse any new connections to the account after multiple unsuccessful authentication attempts. The user should then be notified.

Botan.Low.ZFEC

Forward error correction takes an input and creates multiple “shares”, such that any K of N shares is sufficient to recover the entire original input.

First, we choose a K value appropriate to our message - the higher K is, the smaller (but more numerous) the resulting shares will be:

k = 7
message = "The length of this message must be divisible by K"

NOTE: ZFEC requires that the input length be exactly divisible by K; if needed define a padding scheme to pad your input to the necessary size.

We can calculate N = K + R, where R is the number of redundant shares, meaning we can tolerate the loss of up to R shares and still recover the original message.

We want 2 additional shares of redundancy, so we set R and N appropriately:

r = 2
n = k + r -- 7 + 2 = 9

Then, we encode the message into N shares:

shares <- zfecEncode k n message
length shares
-- 9

Then, we can recover the message from any K of N shares:

someShares <- take k <$> shuffle shares
recoveredMessage <- zfecDecode k n someShares
message == recoveredMessage
-- True

Enabling experimental support

Some features rely on an experimental fork of the Botan C++ library, which will be contributed back upstream to Botan C++ when it is stable.

Build with experimental features
  1. Clone the experimental fork
git clone https://github.com/apotheca/botan-upstream $BOTAN_CPP
  1. Build and install the experimental fork as from source. You may wish to install to a non-standard location using --prefix during configuration, to avoid overwriting any pre-existing install.
cd $BOTAN_CPP
./configure.py --prefix=$BOTAN_OUT
make
make install
  1. Use the XFFI flag to enable the experimental FFI modules. If you installed the experimental fork to a non-standard location, you may also need to specify where using --extra-include-dirs and --extra-lib-dirs flags.
cd $BOTAN_HASKELL
cabal build TARGET -fXFFI --extra-include-dirs $BOTAN_OUT/include --extra-lib-dirs $BOTAN_OUT/lib
# or
stack build TARGET --flag XFFI --extra-include-dirs $BOTAN_OUT/include --extra-lib-dirs $BOTAN_OUT/lib

To check that you've done everything correctly, you can run the following:

import Botan.Bindings.Version 
import Foreign.C.String
import Prelude
botan_version_string >>= peekCString

The version will say unreleased if it is properly pointing to our built Botan.

Resources

There are several resources for this project that might be helpful:

  • Devlog for project status and updates.
  • GitHub for Haskell source code, issues, and pull requests.
  • Upstream C++ Github experimental fork of Botan C++
  • Proposal Haskell Foundation funding proposal submission thread.

As well, there are resources for the original Botan C++ library:

  • Botan Crypto and TLS for Modern C++
  • Botan C++ Github for original Botan C++ source code, issues, and pull requests.
  • Botan Handbook for documentation on the original library
  • Botan FFI for documentation on the Botan C FFI
  • Issue FFI APIs for X.509 are insufficient

License

This project is licensed under the BSD 3-Clause License and is free, open-source software.

Contributing

There are several ways to contribute to the development of this project, and we are happy to receive any bug reports, fixes, documentation, and any other improvements to this project.

Reporting Bugs

See a bug?

  1. Describe the issue

  2. Write down the steps required to reproduce the issue

  3. Report the issue by opening a ticket!

Assist in Development

Want to help?

  1. Fork or clone the repo, and create a new branch:
git checkout https://github.com/haskellfoundation/botan -b some_new_branch
  1. Make your changes, and test them

  2. Submit a pull request with a comprehensive description of the changes

Donations

This is free, open-source software. If you'd like to support the continued development of this project and future projects like this, or just to say thanks, you can donate directly using the following link(s):

ko-fi