diff --git a/CHANGELOG.md b/CHANGELOG.md
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,3 +6,4 @@
 
 * `sodiumInit`
 * Random bytes generation: `Crypto.Random`
+* Key derivation: `Crypto.Key.derive` and `Crypto.Key.rederive`
diff --git a/crypto-sodium.cabal b/crypto-sodium.cabal
--- a/crypto-sodium.cabal
+++ b/crypto-sodium.cabal
@@ -1,13 +1,13 @@
-cabal-version: 1.22
+cabal-version: 1.18
 
 -- This file has been generated from package.yaml by hpack version 0.33.0.
 --
 -- see: https://github.com/sol/hpack
 --
--- hash: 745d63b84deb20c2dc71acc5006fea4028aa386b595bb6e1d9016deba28aeca5
+-- hash: 2d2774b6f1a9692b774ae9bb3b592030db69473bc4e1ee2aa080184a78ce4a10
 
 name:           crypto-sodium
-version:        0.0.2.0
+version:        0.0.3.0
 synopsis:       Easy-and-safe-to-use high-level cryptography based on Sodium
 description:    This is a collection of high-level cryptographic primitives based on
                 <https://libsodium.io/ Sodium>, spiced up with extra type-safety
@@ -35,30 +35,22 @@
                 .
                 = How
                 .
-                == Secret-key cryptography
-                .
-                * Authenticated encryption: "Crypto.Secretbox"
-                * Encryption: "Crypto.Stream"
-                * Authentication: "Crypto.Auth"
+                == Library initialisation
                 .
+                * "Crypto.Init"
                 .
-                = Thread-safety #threadSafety#
+                == Secret-key cryptography
                 .
-                Some of the Sodium (and NaCl) functions (those that generate random data)
-                are not thread-safe. All these functions are explicitly marked as such
-                in their Haddock documentation.
+                * Authenticated symmetric-key encryption: "Crypto.Encrypt.Secretbox"
                 .
-                Calling 'sodiumInit' before they are used makes them thread-safe, see
-                "Crypto.Init".
+                == Public-key cryptography
                 .
-                = Performance
+                * Authenticated public-key encryption: "Crypto.Encrypt.Box"
                 .
-                Sodium contains multiple implementations of the primitives it provides.
-                There are generic implementations, that are used by default, and
-                multiple alternatives optimised for various platforms.
+                == Additional primitives
                 .
-                'sodiumInit' will quickly benchmark all available implementations and choose
-                the bests ones for each primitive, see "Crypto.Init".
+                * Key derivation and generation: "Crypto.Key"
+                * Cryptographically-secure random: "Crypto.Random"
 category:       Cryptography
 homepage:       https://github.com/serokell/haskell-crypto#readme
 bug-reports:    https://github.com/serokell/haskell-crypto/issues
@@ -78,23 +70,26 @@
 
 library
   exposed-modules:
+      Crypto.Encrypt.Box
+      Crypto.Encrypt.Secretbox
       Crypto.Init
+      Crypto.Key
+      Crypto.Key.Internal
+      Crypto.Pwhash.Internal
       Crypto.Random
   other-modules:
       Paths_crypto_sodium
-  reexported-modules:
-      NaCl:Crypto.Secretbox
   hs-source-dirs:
       lib
-  default-extensions: DataKinds FlexibleContexts FlexibleInstances GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NumericUnderscores OverloadedStrings PolyKinds ScopedTypeVariables
+  default-extensions: DataKinds FlexibleContexts FlexibleInstances GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NamedFieldPuns NumericUnderscores OverloadedStrings PolyKinds ScopedTypeVariables TypeApplications
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
   build-depends:
       NaCl >=0.0.1.0 && <0.1
     , base >=4.10 && <4.15
     , bytestring >=0.9 && <0.11
-    , gdp >=0.0.0.1 && <0.1
+    , cereal >=0.1 && <0.6
     , libsodium >=1.0.11 && <2
-    , memory >=0.1 && <0.16
+    , memory >=0.14.15 && <0.16
     , safe-exceptions >=0.1 && <0.2
   default-language: Haskell2010
 
@@ -102,21 +97,29 @@
   type: exitcode-stdio-1.0
   main-is: Test.hs
   other-modules:
+      Test.Crypto.Gen
+      Test.Crypto.Key.Derivation
+      Test.Crypto.Pwhash
       Test.Crypto.Random
+      Test.Crypto.Secretbox
       Paths_crypto_sodium
   hs-source-dirs:
       test
-  default-extensions: DataKinds FlexibleContexts FlexibleInstances GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NumericUnderscores OverloadedStrings PolyKinds ScopedTypeVariables
+  default-extensions: DataKinds FlexibleContexts FlexibleInstances GeneralizedNewtypeDeriving KindSignatures LambdaCase MultiParamTypeClasses NamedFieldPuns NumericUnderscores OverloadedStrings PolyKinds ScopedTypeVariables TypeApplications
   ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints
   build-tool-depends:
       tasty-discover:tasty-discover
   build-depends:
       HUnit
     , base >=4.10 && <4.15
+    , base16-bytestring
     , bytestring >=0.9 && <0.11
     , crypto-sodium
-    , libsodium
-    , memory
+    , hedgehog
+    , libsodium >=1.0.11 && <2
+    , memory >=0.14.15 && <0.16
+    , safe-exceptions >=0.1 && <0.2
     , tasty
+    , tasty-hedgehog
     , tasty-hunit
   default-language: Haskell2010
diff --git a/lib/Crypto/Encrypt/Box.hs b/lib/Crypto/Encrypt/Box.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Encrypt/Box.hs
@@ -0,0 +1,84 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- ! This module merely re-exports definitions from the corresponding
+-- ! module in NaCl and alters the Haddock to make it more specific
+-- ! to crypto-sodium. So, the docs should be kept more-or-less in sync.
+
+-- | Public-key authenticated encryption.
+--
+-- It is best to import this module qualified:
+--
+-- @
+-- import qualified Crypto.Encrypt.Box as Box
+--
+-- encrypted = Box.'create' pk sk nonce message
+-- decrypted = Box.'open' pk sk nonce encrypted
+-- @
+--
+-- A box is an abstraction from NaCl. One way to think about it
+-- is to imagine that you are putting data into a box protected by
+-- the receiver’s public key and signed by your private key. The
+-- receive will then be able to 'open' it using their private key
+-- and your public key.
+--
+-- Note that this means that you need to exchange your public keys
+-- in advance. It might seem strange at first that the receiver
+-- needs to know your public key too, but this is actually very important
+-- as otherwise the receiver would not be able to have any guarantees
+-- regarding the source or the integrity of the data.
+module Crypto.Encrypt.Box
+  (
+  -- * Keys
+    PublicKey
+  , toPublicKey
+  , SecretKey
+  , toSecretKey
+  , keypair
+
+  -- * Nonce
+  , Nonce
+  , toNonce
+
+  -- * Encryption/decryption
+  , create
+  , open
+  ) where
+
+import Data.ByteArray (ByteArray, ByteArrayAccess)
+import Crypto.Box (Nonce, PublicKey, SecretKey, keypair, open, toNonce, toPublicKey, toSecretKey)
+
+import qualified Crypto.Box as NaCl.Box
+
+
+-- | Encrypt a message.
+--
+-- @
+-- encrypted = Box.create pk sk nonce message
+-- @
+--
+-- *   @pk@ is the receiver’s public key, used for encryption.
+--     @sk@ is the sender’s public key, used for authentication.
+--
+--     These are generated using 'keypair' and are supposed to be exchanged
+--     in advance. Both parties need to know their own secret key and the other’s
+--     public key.
+--
+-- *   @nonce@ is an extra noise that ensures that is required for security.
+--     See "Crypto.Nonce" for how to work with it.
+--
+-- *   @message@ is the data you are encrypting.
+--
+-- This function adds authentication data, so if anyone modifies the cyphertext,
+-- @open@ will refuse to decrypt it.
+create
+  ::  ( ByteArrayAccess nonceBytes
+      , ByteArrayAccess ptBytes, ByteArray ctBytes
+      )
+  => PublicKey  -- ^ Receiver’s public key
+  -> SecretKey  -- ^ Sender’s secret key
+  -> Nonce nonceBytes  -- ^ Nonce
+  -> ptBytes -- ^ Plaintext message
+  -> ctBytes
+create = NaCl.Box.create
diff --git a/lib/Crypto/Encrypt/Secretbox.hs b/lib/Crypto/Encrypt/Secretbox.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Encrypt/Secretbox.hs
@@ -0,0 +1,70 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- ! This module merely re-exports definitions from the corresponding
+-- ! module in NaCl and alters the Haddock to make it more specific
+-- ! to crypto-sodium. So, the docs should be kept more-or-less in sync.
+
+-- | Symmetric authenticated encryption.
+--
+-- It is best to import this module qualified:
+--
+-- @
+-- import qualified Crypto.Encrypt.Secretbox as Secretbox
+--
+-- encrypted = Secretbox.'create' key nonce message
+-- decrypted = Secretbox.'open' key nonce encrypted
+-- @
+--
+-- A secretbox is an abstraction from NaCl. One way to think about it
+-- is to imagine that you are putting data into a box protected by a
+-- secret key. You 'create' such a box first, store it somewhere
+-- (it is just a sequence of bytes), and when you need it in the
+-- future, you 'open' it using the same secret key.
+module Crypto.Encrypt.Secretbox
+  (
+  -- * Keys
+    Key
+  , toKey
+
+  -- * Nonce
+  , Nonce
+  , toNonce
+
+  -- * Encryption/decryption
+  , create
+  , open
+  ) where
+
+import Crypto.Secretbox (Key, Nonce, open, toKey, toNonce)
+import Data.ByteArray (ByteArray, ByteArrayAccess)
+
+import qualified Crypto.Secretbox as NaCl.Secretbox
+
+
+-- | Encrypt a message.
+--
+-- @
+-- encrypted = Secretbox.create key nonce message
+-- @
+--
+-- *   @key@ is the secret key used for encryption. See "Crypto.Key" for how
+--     to get one.
+--
+-- *   @nonce@ is an extra noise that ensures that is required for security.
+--     See "Crypto.Nonce" for how to work with it.
+--
+-- *   @message@ is the data you are encrypting.
+--
+-- This function adds authentication data, so if anyone modifies the cyphertext,
+-- @open@ will refuse to decrypt it.
+create
+  ::  ( ByteArrayAccess keyBytes, ByteArrayAccess nonceBytes
+      , ByteArrayAccess ptBytes, ByteArray ctBytes
+      )
+  => Key keyBytes  -- ^ Secret key
+  -> Nonce nonceBytes  -- ^ Nonce
+  -> ptBytes -- ^ Plaintext message
+  -> ctBytes
+create = NaCl.Secretbox.create
diff --git a/lib/Crypto/Init.hs b/lib/Crypto/Init.hs
--- a/lib/Crypto/Init.hs
+++ b/lib/Crypto/Init.hs
@@ -3,6 +3,23 @@
 -- SPDX-License-Identifier: MPL-2.0
 
 -- | Libsodium initialisation.
+
+-- = Thread-safety #threadSafety#
+--
+-- Some of the Sodium (and NaCl) functions (those that generate random data)
+-- are not thread-safe. All these functions are explicitly marked as such
+-- in their Haddock documentation.
+--
+-- Calling 'sodiumInit' before they are used makes them thread-safe.
+--
+-- = Performance
+--
+-- Sodium contains multiple implementations of the primitives it provides.
+-- There are generic implementations, that are used by default, and
+-- multiple alternatives optimised for various platforms.
+--
+-- 'sodiumInit' will quickly benchmark all available implementations and choose
+-- the best ones for each primitive.
 module Crypto.Init
   ( sodiumInit
   , SodiumInitException (..)
diff --git a/lib/Crypto/Key.hs b/lib/Crypto/Key.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Key.hs
@@ -0,0 +1,198 @@
+{-# OPTIONS_GHC -Wno-redundant-constraints #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | This module gives different ways of obtaining secret keys.
+--
+-- = Key derivation (from a password)
+--
+-- Sometimes, instead of generating fresh random transient encryption keys,
+-- you want an encryption key to be persistent and you don’t want to
+-- store it anywhere – instead you ask the user to provide it.
+-- Such a secret value known and entered by the user is usually called
+-- a “password”.
+--
+-- However, passwords make terrible encryption keys because encryption keys:
+--
+-- 1. often need to have a specific exact length and
+-- 2. need to be hard to guess (or brute-force).
+--
+-- Item 1 above can be easily ticked off by deriving the encryption key from
+-- the password by applying a hash-function to it, however in order to
+-- achieve 2 we need our “hash-function” to:
+--
+-- * be slow to compute (to make brute-forcing less feasible) and
+-- * mix extra “noise” into the derivation process to make it harder
+--   to pre-compute derived values in advance.
+--
+-- A construction that satisfies both requirements is called a
+-- /key derivation function (KDF)/.
+-- This module provides a convenient interface for deriving
+-- secure keys from passwords by the way of one such KDF.
+--
+-- == Use
+--
+-- This module provides two functions: 'derive' and 'rederive'.
+-- You can think of the entire process similar to how you set the password
+-- on your account at some website once, and then use this password to log in.
+--
+-- When you derive a key for the first time (e.g. you ask the user to enter their
+-- password twice, and then encrypt something), you use the 'derive' function,
+-- which gives you the derived key and a /derivation slip/. The slip is not
+-- secret, you can store it in plain text and, in fact, you /have to/ store it
+-- in plaintext somewhere next to the encrypted data.
+--
+-- When you need to derive the key in the future (e.g. to decrypt some previously
+-- encrypted data), you will need the user’s password (ask them) /and/ you
+-- will need the original derivation slip, which you should have stored.
+-- You pass these to 'rederive' and it will give you the same key.
+--
+-- @
+-- import qualified Crypto.Key as Key
+--
+-- encrypt = do
+--   password <- {- ask the user to enter their password -}
+--   password2 <- {- ask the user to confirm their password -}
+--   when (password /= password2 then) $ throwIO {- passwords do not match -}
+--
+--   let params = {- choose key derivation parameters -}
+--   (key, slip) <- Key.derive params password
+--
+--   {- store slip (it is not secret) -}
+--   {- encrypt data with key -}
+--
+-- decrypt = do
+--   password <- {- ask the user to enter their password -}
+--   slip <- {- get the stored slip -}
+--
+--   key <- Key.rederive slip password
+--
+--   {- decrypt data with key -}
+-- @
+--
+-- = Random key generation
+--
+-- The 'random' function is great at generating new secure secret keys.
+module Crypto.Key
+  ( type (!>=!)
+
+  -- * Key derivation
+  , Params (..)
+  , DerivationSlip
+  , derive
+  , rederive
+
+  -- * Random key generation
+  , generate
+  ) where
+
+import Data.ByteArray (ByteArrayAccess, ScrubbedBytes)
+import Data.ByteArray.Sized (ByteArrayN, SizedByteArray)
+import Data.Kind (Constraint)
+import GHC.TypeLits (type (<=), KnownNat)
+import System.IO.Unsafe (unsafeDupablePerformIO)
+
+import qualified Libsodium as Na
+
+import Crypto.Key.Internal (DerivationSlip, Params (..))
+
+import qualified Crypto.Key.Internal as I
+import qualified Crypto.Random
+
+
+-- | “At least as secure as”.
+--
+-- @a !>=! b@ means that the storage behind a is not less secure than b.
+-- This is a little bit of an ad-hoc safety hack, which ensures that if
+-- @b@ is stored in a securely allocated memory, then @a@ is stored in
+-- memory allocated as securely, or more securely.
+--
+-- Here are our very ad-hoc rules:
+--
+-- * This relation is reflexive (@a@ is as secure as @a@ for any @a@).
+-- * 'ScrubbedBytes' is more secure than anything.
+-- * Everything else is equally (in)secure.
+--
+-- So, for example, if the original password is stored in @ScrubbedBytes@,
+-- you will not be able to put the derived from it key into a @ByteString@,
+-- because that would be less secure.
+type family a !>=! b :: Constraint where
+  a !>=! a = ()  -- reflexivity
+  a !>=! ScrubbedBytes = LessSecureStorage a ScrubbedBytes
+  a !>=! b = ()
+class LessSecureStorage a b
+
+
+-- | Derive a key from a password using a secure KDF for the first time.
+--
+-- This function takes two arguments:
+--
+-- 1. key derivation parameters, which specify how slow the derivation process
+-- will be (the slower you can afford the better for security),
+--
+-- 2. the user’s password to derive the key from.
+--
+-- See @libsodium@ documentation for how to determine 'Params'.
+--
+-- It returns the derived key and a /slip/ that you need to save in order to be
+-- able to derive the same key from the same password in the future. The slip
+-- is not secret, so you can store it in plaintext; just make sure you can
+-- access it in the future, as you will need to provide it to 'rederive'.
+--
+-- It can derive a key of almost any length and the output length is encoded
+-- in the type. There is an additional type-level restriction which forces
+-- you to store the derived key in memory at least as securely as you
+-- stored the password.
+--
+-- Note: This function is not thread-safe until Sodium is initialised.
+-- See "Crypto.Init" for details.
+derive
+  ::  forall key n passwd.
+      ( ByteArrayAccess passwd
+      , ByteArrayN n key, key !>=! passwd
+      , Na.CRYPTO_PWHASH_BYTES_MIN <= n, n <= Na.CRYPTO_PWHASH_BYTES_MAX
+      )
+  => I.Params -- ^ Derivation parameters.
+  -> passwd  -- ^ Password to derive from.
+  -> IO (Maybe (key, I.DerivationSlip))
+derive = I.derive
+
+-- | Reerive a key from a password using a secure KDF.
+--
+-- This function takes two arguments:
+--
+-- 1. A derivation slip previously returned by 'derive'.
+--
+-- 2. The user’s password.
+--
+-- This function is guaranteed to derive the same key from the same password
+-- as long as the same derivation slip was provided.
+--
+-- See 'derive' for additional details.
+rederive
+  ::  forall key n passwd.
+      ( ByteArrayAccess passwd
+      , ByteArrayN n key, key !>=! passwd
+      , Na.CRYPTO_PWHASH_BYTES_MIN <= n, n <= Na.CRYPTO_PWHASH_BYTES_MAX
+      )
+  => I.DerivationSlip -- ^ Original derivation slip.
+  -> passwd  -- ^ Password to rederive from.
+  -> Maybe key
+rederive slip passwd =
+  unsafeDupablePerformIO $ I.rederive slip passwd
+  -- This IO is safe, because it is pure.
+
+
+-- | Generate a new secret key using a cryptographically-secure generator.
+--
+-- This is just a specialisation of @Crypto.Random.'generate'@ that stores
+-- it in a secure memory location.
+--
+-- Note: This function is not thread-safe until Sodium is initialised.
+-- See "Crypto.Init" for details.
+generate :: KnownNat n => IO (SizedByteArray n ScrubbedBytes)
+generate = Crypto.Random.generate
diff --git a/lib/Crypto/Key/Internal.hs b/lib/Crypto/Key/Internal.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Key/Internal.hs
@@ -0,0 +1,106 @@
+{-# LANGUAGE TupleSections #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | Key derivation/generation internals.
+module Crypto.Key.Internal
+  ( Params (..)
+  , DerivationSlip
+  , derive
+  , rederive
+
+  , DerivationSlipData (..)
+  , derivationSlipEncode
+  , derivationSlipDecode
+  ) where
+
+import Control.Monad (when)
+import Data.ByteArray (ByteArrayAccess)
+import Data.ByteArray.Sized (ByteArrayN, sizedByteArray, unSizedByteArray)
+import Data.ByteString (ByteString)
+import Data.Serialize (Serialize (put, get), decode, encode)
+import Data.Word (Word8)
+import GHC.TypeLits (type (<=))
+
+import qualified Libsodium as Na
+
+import Crypto.Pwhash.Internal (Algorithm (Argon2id_1_3), Params (..), Salt, pwhash)
+import Crypto.Random (generate)
+
+
+-- | Opaque bytes that contain the nonce and pwhash params.
+type DerivationSlip = ByteString
+
+-- | Data contained in a derivation slip.
+--
+-- This data type is used only internally within this module for
+-- convenience. It is exported only for testing purposes.
+--
+-- Currently only one KDF is supported, so it is assumed implicitly,
+-- however the actual binary encoding contains an identifier of the KDF
+-- used (for forward-compatibility).
+data DerivationSlipData = DerivationSlipData
+  { params :: !Params
+  , nonce :: !(Salt ByteString)
+  }
+  deriving (Eq, Show)
+
+instance Serialize DerivationSlipData where
+  put (DerivationSlipData Params{opsLimit, memLimit} nonce) = do
+    put (1 :: Word8)  -- algorithm marker for forward-compatibility
+    put opsLimit >> put memLimit
+    put (unSizedByteArray nonce)
+  get = do
+    tag <- get @Word8
+    when (tag /= 1) $ fail "Wrong algorithm parameters encoding tag"
+    params <- Params <$> get <*> get
+    mnonce <- sizedByteArray <$> get @ByteString
+    case mnonce of
+      Nothing -> fail "Unexpected nonce size"
+      Just nonce -> pure $ DerivationSlipData params nonce
+
+
+-- | Encode derivation slip data into bytes.
+derivationSlipEncode :: DerivationSlipData -> DerivationSlip
+derivationSlipEncode = encode
+
+-- | Decode derivation slip data from bytes.
+derivationSlipDecode :: DerivationSlip -> Maybe DerivationSlipData
+derivationSlipDecode bytes = case decode bytes of
+  Right slip -> Just slip
+  Left _ -> Nothing
+
+
+-- | Derive a key for the first time.
+derive
+  ::  ( ByteArrayAccess passwd
+      , ByteArrayN n key
+      , Na.CRYPTO_PWHASH_BYTES_MIN <= n, n <= Na.CRYPTO_PWHASH_BYTES_MAX
+      )
+  => Params
+  -> passwd
+  -> IO (Maybe (key, DerivationSlip))
+derive params passwd = do
+  nonce <- generate
+  mkey <- pwhash Argon2id_1_3 params passwd nonce
+  let slip = DerivationSlipData params nonce
+  pure $ fmap (, derivationSlipEncode slip) mkey
+
+-- | Derive the same key form the same password again.
+rederive
+  ::  ( ByteArrayAccess passwd
+      , ByteArrayN n key
+      , Na.CRYPTO_PWHASH_BYTES_MIN <= n, n <= Na.CRYPTO_PWHASH_BYTES_MAX
+      )
+  => DerivationSlip
+  -> passwd
+  -> IO (Maybe key)
+rederive slip passwd =
+  case derivationSlipDecode slip of
+    Nothing -> pure Nothing
+    Just (DerivationSlipData{params, nonce}) ->
+      pwhash Argon2id_1_3 params passwd nonce
diff --git a/lib/Crypto/Pwhash/Internal.hs b/lib/Crypto/Pwhash/Internal.hs
new file mode 100644
--- /dev/null
+++ b/lib/Crypto/Pwhash/Internal.hs
@@ -0,0 +1,82 @@
+{-# OPTIONS_GHC -Wno-redundant-constraints #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | Tools for hashing passwords.
+module Crypto.Pwhash.Internal
+  ( Algorithm (..)
+  , Params (..)
+  , Salt
+
+  , pwhash
+  ) where
+
+import Prelude hiding (length)
+
+import Data.ByteArray (ByteArrayAccess, length, withByteArray)
+import Data.ByteArray.Sized (ByteArrayN, SizedByteArray, allocRet)
+import Data.Proxy (Proxy (Proxy))
+import Data.Word (Word64)
+import GHC.TypeLits (type (<=), natVal)
+import Foreign.C.Types (CInt, CSize (CSize), CULLong (CULLong))
+
+import qualified Libsodium as Na
+
+
+-- | Secure hashing algorithm.
+data Algorithm
+  = Argon2i_1_3 -- ^ Argon2i version 1.3
+  | Argon2id_1_3 -- ^ Argon2id version 1.3
+  deriving (Eq, Ord, Show)
+
+algorithmToInt :: Algorithm -> CInt
+algorithmToInt Argon2i_1_3 = Na.crypto_pwhash_alg_argon2i13
+algorithmToInt Argon2id_1_3 = Na.crypto_pwhash_alg_argon2id13
+
+
+-- | Secure-hashing parameters.
+data Params = Params
+  { opsLimit :: !Word64  -- ^ Maximum amount of computation to perform.
+  , memLimit :: !Word64  -- ^ Maximum amount of RAM (bytes) to use.
+  }
+  deriving (Eq, Ord, Show)
+
+
+-- | Salt used for password hashing.
+--
+-- This type is parametrised by the actual data type that contains
+-- bytes. This can be, for example, a @ByteString@.
+type Salt a = SizedByteArray Na.CRYPTO_PWHASH_SALTBYTES a
+
+
+-- | Securely hash a password.
+--
+-- This is @crypto_pwhash@, it can be used for key derivation.
+pwhash
+  ::  forall passwd salt n hash.
+      ( ByteArrayAccess passwd, ByteArrayAccess salt
+      , ByteArrayN n hash
+      , Na.CRYPTO_PWHASH_BYTES_MIN <= n, n <= Na.CRYPTO_PWHASH_BYTES_MAX
+      )
+  => Algorithm  -- ^ Hashing algorithm.
+  -> Params  -- ^ Hashing parameters.
+  -> passwd  -- ^ Password to hash.
+  -> Salt salt  -- ^ Hashing salt.
+  -> IO (Maybe hash)
+pwhash alg Params{opsLimit, memLimit} passwd salt = do
+  (ret, hash) <-
+    allocRet (Proxy :: Proxy n) $ \hashPtr ->
+    withByteArray passwd $ \passwdPtr ->
+    withByteArray salt $ \saltPtr -> do
+      Na.crypto_pwhash hashPtr (fromIntegral $ natVal (Proxy :: Proxy n))
+        passwdPtr (fromIntegral $ length passwd)
+        saltPtr
+        (CULLong opsLimit) (CSize memLimit) (algorithmToInt alg)
+  if ret == 0 then
+    pure $ Just hash
+  else
+    pure $ Nothing
diff --git a/lib/Crypto/Random.hs b/lib/Crypto/Random.hs
--- a/lib/Crypto/Random.hs
+++ b/lib/Crypto/Random.hs
@@ -18,6 +18,9 @@
 -- | Generate a sequence of cryptographically-secure renadom bytes.
 --
 -- The output of this function is suitable to generate secret keys.
+--
+-- Note: This function is not thread-safe until Sodium is initialised.
+-- See "Crypto.Init" for details.
 generate
   :: forall ba n. (ByteArray ba, KnownNat n)
   => IO (SizedByteArray n ba)
diff --git a/test/Test/Crypto/Gen.hs b/test/Test/Crypto/Gen.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Crypto/Gen.hs
@@ -0,0 +1,36 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | Hedgehog generators of test data
+module Test.Crypto.Gen where
+
+import Hedgehog (Gen)
+
+import Data.ByteString (ByteString)
+import Data.ByteArray (ByteArray, ScrubbedBytes, convert)
+import Data.ByteArray.Sized (SizedByteArray, sizedByteArray)
+import Data.Maybe (fromJust)
+import Data.Proxy (Proxy (Proxy))
+import GHC.TypeLits (KnownNat, natVal)
+
+import qualified Hedgehog.Gen as G
+import qualified Hedgehog.Range as R
+
+
+-- | Generate a random sized byte array
+sizedBytes
+  :: forall n ba. (ByteArray ba, KnownNat n)
+  => Gen (SizedByteArray n ba)
+sizedBytes = fromJust . sizedByteArray . convert <$>
+  G.bytes (R.singleton $ fromIntegral $ natVal (Proxy @n))
+
+-- | Generate a random nonce of the right size.
+nonce :: forall n. KnownNat n => Gen (SizedByteArray n ByteString)
+nonce = sizedBytes
+
+-- | Generate a random key of the right size.
+--
+-- THIS FUNCTION IS NOT SECURE AND IS ONLY SUITABLE FOR TESTS.
+key :: forall n. KnownNat n => Gen (SizedByteArray n ScrubbedBytes)
+key = sizedBytes
diff --git a/test/Test/Crypto/Key/Derivation.hs b/test/Test/Crypto/Key/Derivation.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Crypto/Key/Derivation.hs
@@ -0,0 +1,49 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | Tests for our cool slip-based KDF
+module Test.Crypto.Key.Derivation where
+
+import Hedgehog (Gen, Property, (===), evalIO, forAll, property, tripping)
+
+import qualified Hedgehog.Gen as G
+import qualified Hedgehog.Range as R
+
+import Data.ByteString (ByteString)
+import Data.ByteArray.Sized (SizedByteArray)
+
+import qualified Libsodium as Na
+
+import qualified Test.Crypto.Gen as G
+
+import Crypto.Key (Params (Params), derive, rederive)
+
+import qualified Crypto.Key.Internal as KI
+
+
+genParams :: Gen Params
+genParams = Params
+  <$> G.integral
+    (R.linear (fromIntegral Na.crypto_pwhash_opslimit_min) 10)
+  <*> G.integral
+    (R.linear (fromIntegral Na.crypto_pwhash_memlimit_min) (2 * 1024 * 1024))
+
+genSlipData :: Gen KI.DerivationSlipData
+genSlipData = KI.DerivationSlipData
+  <$> genParams
+  <*> G.nonce
+
+
+hprop_slip_encode_decode :: Property
+hprop_slip_encode_decode = property $ do
+    slipData <- forAll $ genSlipData
+    tripping slipData KI.derivationSlipEncode KI.derivationSlipDecode
+
+hprop_derive_rederive :: Property
+hprop_derive_rederive = property $ do
+    params <- forAll $ genParams
+    passwd <- forAll $ G.bytes (R.linear 0 100)
+    Just (key, slip) <- evalIO $
+      derive @(SizedByteArray 64 ByteString) params passwd
+    rederive slip passwd === Just key
diff --git a/test/Test/Crypto/Pwhash.hs b/test/Test/Crypto/Pwhash.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Crypto/Pwhash.hs
@@ -0,0 +1,277 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
+{-# LANGUAGE AllowAmbiguousTypes #-}
+{-# LANGUAGE TypeFamilies #-}
+{-# LANGUAGE TypeOperators #-}
+
+module Test.Crypto.Pwhash where
+
+import Test.HUnit ((@?=), Assertion)
+
+import Data.ByteArray.Sized (sizedByteArray)
+import Data.ByteString (ByteString)
+import GHC.TypeLits (type (<=), KnownNat)
+
+import qualified Data.ByteString as BS
+import qualified Data.ByteString.Base16 as B16
+import qualified Libsodium as Na
+
+import Crypto.Pwhash.Internal (Algorithm (..), Params (Params), pwhash)
+
+
+pwhash_test_vector
+  ::  forall n. -- ^ Output length.
+      ( KnownNat n
+      , Na.CRYPTO_PWHASH_BYTES_MIN <= n, n <= Na.CRYPTO_PWHASH_BYTES_MAX
+      )
+  => ByteString  -- ^ Expected hash.
+  -> ByteString  -- ^ Password.
+  -> ByteString  -- ^ Salt.
+  -> Algorithm  -- ^ Hashing algorithm.
+  -> Params  -- ^ Hashing params.
+  -> Assertion
+pwhash_test_vector hash passwd salt alg params = do
+  let (hash', "") = B16.decode hash
+  let (passwd', "") = B16.decode passwd
+  let (salt', "") = B16.decode salt
+  let Just salt'N = sizedByteArray (BS.take 16 salt')  -- Note:
+    -- for some reason, the test vectors in the file are 32 bytes long,
+    -- while the pwhash function needs a 16-byte salt :/
+  let Just hash'N = sizedByteArray @n hash'
+  result <- pwhash alg params passwd' salt'N
+  result @?= Just hash'N
+
+
+-- Test vectors from
+-- https://github.com/jedisct1/libsodium/blob/f911b56650b680ecfc5d32b11b090849fc2b5f92/test/default/pwhash_argon2id.c
+
+unit_pwhash_argon2id_1 :: Assertion
+unit_pwhash_argon2id_1 =
+  pwhash_test_vector
+    @155
+    "18acec5d6507739f203d1f5d9f1d862f7c2cdac4f19d2bdff64487e60d969e3ced615337b9eec6ac4461c6ca07f0939741e57c24d0005c7ea171a0ee1e7348249d135b38f222e4dad7b9a033ed83f5ca27277393e316582033c74affe2566a2bea47f91f0fd9fe49ece7e1f79f3ad6e9b23e0277c8ecc4b313225748dd2a80f5679534a0700e246a79a49b3f74eb89ec6205fe1eeb941c73b1fcf1"
+    (mconcat $
+      [ "a347ae92bce9f80f6f595a4480fc9c2fe7e7d7148d371e9487d75f5c23008ffae0"
+      , "65577a928febd9b1973a5a95073acdbeb6a030cfc0d79caa2dc5cd011cef02c08d"
+      , "a232d76d52dfbca38ca8dcbd665b17d1665f7cf5fe59772ec909733b24de97d6f5"
+      , "8d220b20c60d7c07ec1fd93c52c31020300c6c1facd77937a597c7a6"
+      ]
+    )
+    "5541fbc995d5c197ba290346d2c559dedf405cf97e5f95482143202f9e74f5c2"
+    Argon2id_1_3
+    (Params 5 7256678)
+
+unit_pwhash_argon2id_2 :: Assertion
+unit_pwhash_argon2id_2 =
+  pwhash_test_vector
+    @250
+    "26bab5f101560e48c711da4f05e81f5a3802b7a93d5155b9cab153069cc42b8e9f910bfead747652a0708d70e4de0bada37218bd203a1201c36b42f9a269b675b1f30cfc36f35a3030e9c7f57dfba0d341a974c1886f708c3e8297efbfe411bb9d51375264bd7c70d57a8a56fc9de2c1c97c08776803ec2cd0140bba8e61dc0f4ad3d3d1a89b4b710af81bfe35a0eea193e18a6da0f5ec05542c9eefc4584458e1da715611ba09617384748bd43b9bf1f3a6df4ecd091d0875e08d6e2fd8a5c7ce08904b5160cd38167b76ec76ef2d310049055a564da23d4ebd2b87e421cc33c401e12d5cd8d936c9baf75ebdfb557d342d2858fc781da31860"
+    (mconcat $
+      [ "e125cee61c8cb7778d9e5ad0a6f5d978ce9f84de213a8556d9ffe202020ab4a6ed"
+      , "9074a4eb3416f9b168f137510f3a30b70b96cbfa219ff99f6c6eaffb15c06b60e0"
+      , "0cc2890277f0fd3c622115772f7048adaebed86e"
+      ]
+    )
+    "f1192dd5dc2368b9cd421338b22433455ee0a3699f9379a08b9650ea2c126f0d"
+    Argon2id_1_3
+    (Params 4 7849083)
+
+unit_pwhash_argon2id_3 :: Assertion
+unit_pwhash_argon2id_3 =
+  pwhash_test_vector
+    @249
+    "6eb45e668582d63788ca8f6e930ca60b045a795fca987344f9a7a135aa3b5132b50a34a3864c26581f1f56dd0bcbfafbfa92cd9bff6b24a734cfe88f854aef4bda0a7983120f44936e8ff31d29728ac08ccce6f3f916b3c63962755c23a1fa9bb4e8823fc867bfd18f28980d94bc5874423ab7f96cc0ab78d8fa21fbd00cd3a1d96a73fa439ccc3fc4eab1590677b06cc78b0f674dfb680f23022fb902022dd8620803229c6ddf79a8156ccfce48bbd76c05ab670634f206e5b2e896230baa74a856964dbd8511acb71d75a1506766a125d8ce037f1db72086ebc3bccaefbd8cd9380167c2530386544ebfbeadbe237784d102bb92a10fd242"
+    (mconcat $
+      [ "92263cbf6ac376499f68a4289d3bb59e5a22335eba63a32e6410249155b956b6a3"
+      , "b48d4a44906b18b897127300b375b8f834f1ceffc70880a885f47c33876717e392"
+      , "be57f7da3ae58da4fd1f43daa7e44bb82d3717af4319349c24cd31e46d295856b0"
+      , "441b6b289992a11ced1cc3bf3011604590244a3eb737ff221129215e4e4347f491"
+      , "5d41292b5173d196eb9add693be5319fdadc242906178bb6c0286c9b6ca6012746"
+      , "711f58c8c392016b2fdfc09c64f0f6b6ab7b"
+      ]
+    )
+    "3b840e20e9555e9fb031c4ba1f1747ce25cc1d0ff664be676b9b4a90641ff194"
+    Argon2id_1_3
+    (Params 3 7994791)
+
+-- unit_pwhash_argon2id_4 is skipped because it is a test for an incorrect output
+-- size, which is impossible due to stronger types in this library
+
+unit_pwhash_argon2id_5 :: Assertion
+unit_pwhash_argon2id_5 =
+  pwhash_test_vector
+    @190
+    "08d8cd330c57e1b4643241d05bb468ba4ee4e932cd0858816be9ef15360b27bbd06a87130ee92222be267a29b81f5ae8fe8613324cfc4832dc49387fd0602f1c57b4d0f3855db94fb7e12eb05f9a484aed4a4307abf586cd3d55c809bc081541e00b682772fb2066504ff935b8ebc551a2083882f874bc0fae68e56848ae34c91097c3bf0cca8e75c0797eef3efde3f75e005815018db3cf7c109a812264c4de69dcb22322dbbcfa447f5b00ecd1b04a7be1569c8e556adb7bba48adf81d"
+    (mconcat $
+      [ "4a857e2ee8aa9b6056f2424e84d24a72473378906ee04a46cb05311502d5250b82"
+      , "ad86b83c8f20a23dbb74f6da60b0b6ecffd67134d45946ac8ebfb3064294bc097d"
+      , "43ced68642bfb8bbbdd0f50b30118f5e"
+      ]
+    )
+    "39d82eef32010b8b79cc5ba88ed539fbaba741100f2edbeca7cc171ffeabf258"
+    Argon2id_1_3
+    (Params 3 1432947)
+
+unit_pwhash_argon2id_6 :: Assertion
+unit_pwhash_argon2id_6 =
+  pwhash_test_vector
+    @178
+    "d6e9d6cabd42fb9ba7162fe9b8e41d59d3c7034756cb460c9affe393308bd0225ce0371f2e6c3ca32aca2002bf2d3909c6b6e7dfc4a00e850ff4f570f8f749d4bb6f0091e554be67a9095ae1eefaa1a933316cbec3c2fd4a14a5b6941bda9b7eabd821d79abde2475a53af1a8571c7ee46460be415882e0b393f48c12f740a6a72cba9773000602e13b40d3dfa6ac1d4ec43a838b7e3e165fecad4b2498389e60a3ff9f0f8f4b9fca1126e64f49501e38690"
+    (mconcat $
+      [ "c7b09aec680e7b42fedd7fc792e78b2f6c1bea8f4a884320b648f81e8cf515e8ba"
+      , "9dcfb11d43c4aae114c1734aa69ca82d44998365db9c93744fa28b63fd16000e82"
+      , "61cbbe083e7e2da1e5f696bde0834fe53146d7e0e35e7de9920d041f5a5621aabe"
+      , "02da3e2b09b405b77937efef3197bd5772e41fdb73fb5294478e45208063b5f58e"
+      , "089dbeb6d6342a909c1307b3fff5fe2cf4da56bdae50848f"
+      ]
+    )
+    "039c056d933b475032777edbaffac50f143f64c123329ed9cf59e3b65d3f43b6"
+    Argon2id_1_3
+    (Params 3 4886999)
+
+unit_pwhash_argon2id_7 :: Assertion
+unit_pwhash_argon2id_7 =
+  pwhash_test_vector
+    @231
+    "7fb72409b0987f8190c3729710e98c3f80c5a8727d425fdcde7f3644d467fe973f5b5fee683bd3fce812cb9ae5e9921a2d06c2f1905e4e839692f2b934b682f11a2fe2b90482ea5dd234863516dba6f52dc0702d324ec77d860c2e181f84472bd7104fedce071ffa93c5309494ad51623d214447a7b2b1462dc7d5d55a1f6fd5b54ce024118d86f0c6489d16545aaa87b6689dad9f2fb47fda9894f8e12b87d978b483ccd4cc5fd9595cdc7a818452f915ce2f7df95ec12b1c72e3788d473441d884f9748eb14703c21b45d82fd667b85f5b2d98c13303b3fe76285531a826b6fc0fe8e3dddecf"
+    (mconcat $
+      [ "b540beb016a5366524d4605156493f9874514a5aa58818cd0c6dfffaa9e90205f1"
+      , "7b"
+      ]
+    )
+    "44071f6d181561670bda728d43fb79b443bb805afdebaf98622b5165e01b15fb"
+    Argon2id_1_3
+    (Params 1 1631659)
+
+unit_pwhash_argon2id_8 :: Assertion
+unit_pwhash_argon2id_8 =
+  pwhash_test_vector
+    @167
+    "4e702bc5f891df884c6ddaa243aa846ce3c087fe930fef0f36b3c2be34164ccc295db509254743f18f947159c813bcd5dd8d94a3aec93bbe57605d1fad1aef1112687c3d4ef1cb329d21f1632f626818d766915d886e8d819e4b0b9c9307f4b6afc081e13b0cf31db382ff1bf05a16aac7af696336d75e99f82163e0f371e1d25c4add808e215697ad3f779a51a462f8bf52610af21fc69dba6b072606f2dabca7d4ae1d91d919"
+    (mconcat $
+      [ "a14975c26c088755a8b715ff2528d647cd343987fcf4aa25e7194a8417fb2b4b3f"
+      , "7268da9f3182b4cfb22d138b2749d673a47ecc7525dd15a0a3c66046971784bb63"
+      , "d7eae24cc84f2631712075a10e10a96b0e0ee67c43e01c423cb9c44e5371017e9c"
+      , "496956b632158da3fe12addecb88912e6759bc37f9af2f45af72c5cae3b179ffb6"
+      , "76a697de6ebe45cd4c16d4a9d642d29ddc0186a0a48cb6cd62bfc3dd229d313b30"
+      , "1560971e740e2cf1f99a9a090a5b283f35475057e96d7064e2e0fc81984591068d"
+      , "55a3b4169f22cccb0745a2689407ea1901a0a766eb99"
+      ]
+    )
+    "3d968b2752b8838431165059319f3ff8910b7b8ecb54ea01d3f54769e9d98daf"
+    Argon2id_1_3
+    (Params 3 1784128)
+
+
+-- Test vectors from
+-- https://github.com/jedisct1/libsodium/blob/f911b56650b680ecfc5d32b11b090849fc2b5f92/test/default/pwhash_argon2i.c
+
+unit_pwhash_argon2i_1 :: Assertion
+unit_pwhash_argon2i_1 =
+  pwhash_test_vector
+    @155
+    "23b803c84eaa25f4b44634cc1e5e37792c53fcd9b1eb20f865329c68e09cbfa9f1968757901b383fce221afe27713f97914a041395bbe1fb70e079e5bed2c7145b1f6154046f5958e9b1b29055454e264d1f2231c316f26be2e3738e83a80315e9a0951ce4b137b52e7d5ee7b37f7d936dcee51362bcf792595e3c896ad5042734fc90c92cae572ce63ff659a2f7974a3bd730d04d525d253ccc38"
+    (mconcat $
+      [ "a347ae92bce9f80f6f595a4480fc9c2fe7e7d7148d371e9487d75f5c23008ffae0"
+      , "65577a928febd9b1973a5a95073acdbeb6a030cfc0d79caa2dc5cd011cef02c08d"
+      , "a232d76d52dfbca38ca8dcbd665b17d1665f7cf5fe59772ec909733b24de97d6f5"
+      , "8d220b20c60d7c07ec1fd93c52c31020300c6c1facd77937a597c7a6"
+      ]
+    )
+    "5541fbc995d5c197ba290346d2c559dedf405cf97e5f95482143202f9e74f5c2"
+    Argon2i_1_3
+    (Params 5 7256678)
+
+unit_pwhash_argon2i_2 :: Assertion
+unit_pwhash_argon2i_2 =
+  pwhash_test_vector
+    @250
+    "0bb3769b064b9c43a9460476ab38c4a9a2470d55d4c992c6e723af895e4c07c09af41f22f90eab583a0c362d177f4677f212482fd145bfb9ac6211635e48461122bb49097b5fb0739d2cd22a39bf03d268e7495d4fd8d710aa156202f0a06e932ff513e6e7c76a4e98b6df5cf922f124791b1076ad904e6897271f5d7d24c5929e2a3b836d0f2f2697c2d758ee79bf1264f3fae65f3744e0f6d7d07ef6e8b35b70c0f88e9036325bfb24ac7f550351486da87aef10d6b0cb77d1cf6e31cf98399c6f241c605c6530dffb4764784f6c0b0bf601d4e4431e8b18dabdc3079c6e264302ade79f61cbd5497c95486340bb891a737223100be0429650"
+    (mconcat $
+      [ "e125cee61c8cb7778d9e5ad0a6f5d978ce9f84de213a8556d9ffe202020ab4a6ed"
+      , "9074a4eb3416f9b168f137510f3a30b70b96cbfa219ff99f6c6eaffb15c06b60e0"
+      , "0cc2890277f0fd3c622115772f7048adaebed86e"
+      ]
+    )
+    "f1192dd5dc2368b9cd421338b22433455ee0a3699f9379a08b9650ea2c126f0d"
+    Argon2i_1_3
+    (Params 4 7849083)
+
+unit_pwhash_argon2i_3 :: Assertion
+unit_pwhash_argon2i_3 =
+  pwhash_test_vector
+    @249
+    "e9aa073b0b872f15c083d1d7ce52c09f493b827ca78f13a06c1721b45b1e17b24c04e19fe869333135360197a7eb55994fee3e8d9680aedfdf7674f3ad7b84d59d7eab03579ffc10c7093093bc48ec84252aa1b30f40f5e838f1443e15e2772a39f4e774eb052097e8881e94f15457b779fa2af2bbc9a993687657c7704ac8a37c25c1df4289eb4c70da45f2fd46bc0f78259767d3dd478a7c369cf866758bc36d9bd8e2e3c9fb0cf7fd6073ebf630c1f67fa7d303c07da40b36749d157ea37965fef810f2ea05ae6fc7d96a8f3470d73e15b22b42e8d6986dbfe5303256b2b3560372c4452ffb2a04fb7c6691489f70cb46831be0679117f7"
+    (mconcat $
+      [ "92263cbf6ac376499f68a4289d3bb59e5a22335eba63a32e6410249155b956b6a3"
+      , "b48d4a44906b18b897127300b375b8f834f1ceffc70880a885f47c33876717e392"
+      , "be57f7da3ae58da4fd1f43daa7e44bb82d3717af4319349c24cd31e46d295856b0"
+      , "441b6b289992a11ced1cc3bf3011604590244a3eb737ff221129215e4e4347f491"
+      , "5d41292b5173d196eb9add693be5319fdadc242906178bb6c0286c9b6ca6012746"
+      , "711f58c8c392016b2fdfc09c64f0f6b6ab7b"
+      ]
+    )
+    "3b840e20e9555e9fb031c4ba1f1747ce25cc1d0ff664be676b9b4a90641ff194"
+    Argon2i_1_3
+    (Params 3 7994791)
+
+-- unit_pwhash_argon2i_4 is skipped because it is a test for an incorrect output
+-- size, which is impossible due to stronger types in this library
+
+unit_pwhash_argon2i_5 :: Assertion
+unit_pwhash_argon2i_5 =
+  pwhash_test_vector
+    @190
+    "c121209f0ba70aed93d49200e5dc82cce013cef25ea31e160bf8db3cf448a59d1a56f6c19259e18ea020553cb75781761d112b2d949a297584c65e60df95ad89c4109825a3171dc6f20b1fd6b0cdfd194861bc2b414295bee5c6c52619e544abce7d520659c3d51de2c60e89948d830695ab38dcb75dd7ab06a4770dd4bc7c8f335519e04b038416b1a7dbd25c026786a8105c5ffe7a0931364f0376ae5772be39b51d91d3281464e0f3a128e7155a68e87cf79626ffca0b2a3022fc8420"
+    (mconcat $
+      [ "4a857e2ee8aa9b6056f2424e84d24a72473378906ee04a46cb05311502d5250b82"
+      , "ad86b83c8f20a23dbb74f6da60b0b6ecffd67134d45946ac8ebfb3064294bc097d"
+      , "43ced68642bfb8bbbdd0f50b30118f5e"
+      ]
+    )
+    "39d82eef32010b8b79cc5ba88ed539fbaba741100f2edbeca7cc171ffeabf258"
+    Argon2i_1_3
+    (Params 3 1432947)
+
+unit_pwhash_argon2i_6 :: Assertion
+unit_pwhash_argon2i_6 =
+  pwhash_test_vector
+    @178
+    "91c337ce8918a5805a59b00bd1819d3eb4356807cbd2a80b271c4b482dce03f5b02ae4eb831ff668cbb327b93c300b41da4852e5547bea8342d518dd9311aaeb5f90eccf66d548f9275631f0b1fd4b299cec5d2e86a59e55dc7b3afab6204447b21d1ef1da824abaf31a25a0d6135c4fe81d34a06816c8a6eab19141f5687108500f3719a862af8c5fee36e130c69921e11ce83dfc72c5ec3b862c1bccc5fd63ad57f432fbcca6f9e18d5a59015950cdf053"
+    (mconcat $
+      [ "c7b09aec680e7b42fedd7fc792e78b2f6c1bea8f4a884320b648f81e8cf515e8ba"
+      , "9dcfb11d43c4aae114c1734aa69ca82d44998365db9c93744fa28b63fd16000e82"
+      , "61cbbe083e7e2da1e5f696bde0834fe53146d7e0e35e7de9920d041f5a5621aabe"
+      , "02da3e2b09b405b77937efef3197bd5772e41fdb73fb5294478e45208063b5f58e"
+      , "089dbeb6d6342a909c1307b3fff5fe2cf4da56bdae50848f"
+      ]
+    )
+    "039c056d933b475032777edbaffac50f143f64c123329ed9cf59e3b65d3f43b6"
+    Argon2i_1_3
+    (Params 3 4886999)
+
+-- unit_pwhash_argon2i_7 is skipped because it is a test for an incorrect
+-- opslimit.
+-- XXX: Maybe we could encode this restriction in types?
+
+unit_pwhash_argon2i_8 :: Assertion
+unit_pwhash_argon2i_8 =
+  pwhash_test_vector
+    @167
+    "e942951dfbc2d508294b10f9e97b47d0cd04e668a043cb95679cc1139df7c27cd54367688725be9d069f5704c12223e7e4ca181fbd0bed18bb4634795e545a6c04a7306933a41a794baedbb628d41bc285e0b9084055ae136f6b63624c874f5a1e1d8be7b0b7227a171d2d7ed578d88bfdcf18323198962d0dcad4126fd3f21adeb1e11d66252ea0c58c91696e91031bfdcc2a9dc0e028d17b9705ba2d7bcdcd1e3ba75b4b1fea"
+    (mconcat $
+      [ "a14975c26c088755a8b715ff2528d647cd343987fcf4aa25e7194a8417fb2b4b3f"
+      , "7268da9f3182b4cfb22d138b2749d673a47ecc7525dd15a0a3c66046971784bb63"
+      , "d7eae24cc84f2631712075a10e10a96b0e0ee67c43e01c423cb9c44e5371017e9c"
+      , "496956b632158da3fe12addecb88912e6759bc37f9af2f45af72c5cae3b179ffb6"
+      , "76a697de6ebe45cd4c16d4a9d642d29ddc0186a0a48cb6cd62bfc3dd229d313b30"
+      , "1560971e740e2cf1f99a9a090a5b283f35475057e96d7064e2e0fc81984591068d"
+      , "55a3b4169f22cccb0745a2689407ea1901a0a766eb99"
+      ]
+    )
+    "3d968b2752b8838431165059319f3ff8910b7b8ecb54ea01d3f54769e9d98daf"
+    Argon2i_1_3
+    (Params 3 1784128)
diff --git a/test/Test/Crypto/Random.hs b/test/Test/Crypto/Random.hs
--- a/test/Test/Crypto/Random.hs
+++ b/test/Test/Crypto/Random.hs
@@ -2,8 +2,6 @@
 --
 -- SPDX-License-Identifier: MPL-2.0
 
-{-# OPTIONS_GHC -Wno-incomplete-uni-patterns #-}
-
 module Test.Crypto.Random where
 
 import Test.HUnit ((@?=), Assertion)
@@ -16,7 +14,7 @@
 
 import Crypto.Random (generate)
 
-import qualified Crypto.Secretbox as Secretbox
+import qualified Crypto.Encrypt.Secretbox as Secretbox
 
 
 -- Well, this is kinda stupid, because we merely generate one random sequence,
diff --git a/test/Test/Crypto/Secretbox.hs b/test/Test/Crypto/Secretbox.hs
new file mode 100644
--- /dev/null
+++ b/test/Test/Crypto/Secretbox.hs
@@ -0,0 +1,32 @@
+-- SPDX-FileCopyrightText: 2020 Serokell
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | “Integration” tests: using Secretbox with our helpers.
+module Test.Crypto.Secretbox where
+
+import Hedgehog (Property, forAll, property, tripping)
+import Hedgehog.Internal.Property (forAllT)
+
+import Control.Monad.IO.Class (liftIO)
+import Data.ByteString (ByteString)
+
+import qualified Hedgehog.Gen as G
+import qualified Hedgehog.Range as R
+
+import qualified Crypto.Key as Key (generate)
+import qualified Crypto.Random (generate)
+
+import qualified Crypto.Encrypt.Secretbox as Secretbox
+
+
+hprop_encode_decode :: Property
+hprop_encode_decode = property $ do
+    key <- forAllT $ liftIO Key.generate
+    nonce <- forAllT $ liftIO $ Crypto.Random.generate @ByteString
+    msg <- forAll $ G.bytes (R.linear 0 1_000)
+    tripping msg (encodeBs key nonce) (decodeBs key nonce)
+  where
+    -- We need to specify the type of the cyphertext as it is polymorphic
+    encodeBs key nonce msg = Secretbox.create key nonce msg :: ByteString
+    decodeBs key nonce ct = Secretbox.open key nonce ct :: Maybe ByteString
