diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,10 @@
+# Changelog
+
+## Unreleased
+
+### Added
+
+* The `SensitiveBytes` data type.
+  * `withSecureMemory`
+  * `withSensitiveBytes`
+* IO: `withUserPassword`
diff --git a/LICENSES/MPL-2.0.txt b/LICENSES/MPL-2.0.txt
new file mode 100644
--- /dev/null
+++ b/LICENSES/MPL-2.0.txt
@@ -0,0 +1,312 @@
+Mozilla Public License Version 2.0
+
+   1. Definitions
+
+1.1. "Contributor" means each individual or legal entity that creates, contributes
+to the creation of, or owns Covered Software.
+
+1.2. "Contributor Version" means the combination of the Contributions of others
+(if any) used by a Contributor and that particular Contributor's Contribution.
+
+      1.3. "Contribution" means Covered Software of a particular Contributor.
+
+1.4. "Covered Software" means Source Code Form to which the initial Contributor
+has attached the notice in Exhibit A, the Executable Form of such Source Code
+Form, and Modifications of such Source Code Form, in each case including portions
+thereof.
+
+      1.5. "Incompatible With Secondary Licenses" means
+
+(a) that the initial Contributor has attached the notice described in Exhibit
+B to the Covered Software; or
+
+(b) that the Covered Software was made available under the terms of version
+1.1 or earlier of the License, but not also under the terms of a Secondary
+License.
+
+1.6. "Executable Form" means any form of the work other than Source Code Form.
+
+1.7. "Larger Work" means a work that combines Covered Software with other
+material, in a separate file or files, that is not Covered Software.
+
+      1.8. "License" means this document.
+
+1.9. "Licensable" means having the right to grant, to the maximum extent possible,
+whether at the time of the initial grant or subsequently, any and all of the
+rights conveyed by this License.
+
+      1.10. "Modifications" means any of the following:
+
+(a) any file in Source Code Form that results from an addition to, deletion
+from, or modification of the contents of Covered Software; or
+
+(b) any new file in Source Code Form that contains any Covered Software.
+
+1.11. "Patent Claims" of a Contributor means any patent claim(s), including
+without limitation, method, process, and apparatus claims, in any patent Licensable
+by such Contributor that would be infringed, but for the grant of the License,
+by the making, using, selling, offering for sale, having made, import, or
+transfer of either its Contributions or its Contributor Version.
+
+1.12. "Secondary License" means either the GNU General Public License, Version
+2.0, the GNU Lesser General Public License, Version 2.1, the GNU Affero General
+Public License, Version 3.0, or any later versions of those licenses.
+
+1.13. "Source Code Form" means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your") means an individual or a legal entity exercising rights
+under this License. For legal entities, "You" includes any entity that controls,
+is controlled by, or is under common control with You. For purposes of this
+definition, "control" means (a) the power, direct or indirect, to cause the
+direction or management of such entity, whether by contract or otherwise,
+or (b) ownership of more than fifty percent (50%) of the outstanding shares
+or beneficial ownership of such entity.
+
+   2. License Grants and Conditions
+
+      2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free, non-exclusive
+license:
+
+(a) under intellectual property rights (other than patent or trademark) Licensable
+by such Contributor to use, reproduce, make available, modify, display, perform,
+distribute, and otherwise exploit its Contributions, either on an unmodified
+basis, with Modifications, or as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer for
+sale, have made, import, and otherwise transfer either its Contributions or
+its Contributor Version.
+
+      2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution become
+effective for each Contribution on the date the Contributor first distributes
+such Contribution.
+
+      2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under this
+License. No additional rights or licenses will be implied from the distribution
+or licensing of Covered Software under this License. Notwithstanding Section
+2.1(b) above, no patent license is granted by a Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software; or
+
+(b) for infringements caused by: (i) Your and any other third party's modifications
+of Covered Software, or (ii) the combination of its Contributions with other
+software (except as part of its Contributor Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of its
+Contributions.
+
+This License does not grant any rights in the trademarks, service marks, or
+logos of any Contributor (except as may be necessary to comply with the notice
+requirements in Section 3.4).
+
+      2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to distribute
+the Covered Software under a subsequent version of this License (see Section
+10.2) or under the terms of a Secondary License (if permitted under the terms
+of Section 3.3).
+
+      2.5. Representation
+
+Each Contributor represents that the Contributor believes its Contributions
+are its original creation(s) or it has sufficient rights to grant the rights
+to its Contributions conveyed by this License.
+
+      2.6. Fair Use
+
+This License is not intended to limit any rights You have under applicable
+copyright doctrines of fair use, fair dealing, or other equivalents.
+
+      2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted in
+Section 2.1.
+
+   3. Responsibilities
+
+      3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any Modifications
+that You create or to which You contribute, must be under the terms of this
+License. You must inform recipients that the Source Code Form of the Covered
+Software is governed by the terms of this License, and how they can obtain
+a copy of this License. You may not attempt to alter or restrict the recipients'
+rights in the Source Code Form.
+
+      3.2. Distribution of Executable Form
+
+      If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code Form,
+as described in Section 3.1, and You must inform recipients of the Executable
+Form how they can obtain a copy of such Source Code Form by reasonable means
+in a timely manner, at a charge no more than the cost of distribution to the
+recipient; and
+
+(b) You may distribute such Executable Form under the terms of this License,
+or sublicense it under different terms, provided that the license for the
+Executable Form does not attempt to limit or alter the recipients' rights
+in the Source Code Form under this License.
+
+      3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice, provided
+that You also comply with the requirements of this License for the Covered
+Software. If the Larger Work is a combination of Covered Software with a work
+governed by one or more Secondary Licenses, and the Covered Software is not
+Incompatible With Secondary Licenses, this License permits You to additionally
+distribute such Covered Software under the terms of such Secondary License(s),
+so that the recipient of the Larger Work may, at their option, further distribute
+the Covered Software under the terms of either this License or such Secondary
+License(s).
+
+      3.4. Notices
+
+You may not remove or alter the substance of any license notices (including
+copyright notices, patent notices, disclaimers of warranty, or limitations
+of liability) contained within the Source Code Form of the Covered Software,
+except that You may alter any license notices to the extent required to remedy
+known factual inaccuracies.
+
+      3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support, indemnity
+or liability obligations to one or more recipients of Covered Software. However,
+You may do so only on Your own behalf, and not on behalf of any Contributor.
+You must make it absolutely clear that any such warranty, support, indemnity,
+or liability obligation is offered by You alone, and You hereby agree to indemnify
+every Contributor for any liability incurred by such Contributor as a result
+of warranty, support, indemnity or liability terms You offer. You may include
+additional disclaimers of warranty and limitations of liability specific to
+any jurisdiction.
+
+   4. Inability to Comply Due to Statute or Regulation
+
+If it is impossible for You to comply with any of the terms of this License
+with respect to some or all of the Covered Software due to statute, judicial
+order, or regulation then You must: (a) comply with the terms of this License
+to the maximum extent possible; and (b) describe the limitations and the code
+they affect. Such description must be placed in a text file included with
+all distributions of the Covered Software under this License. Except to the
+extent prohibited by statute or regulation, such description must be sufficiently
+detailed for a recipient of ordinary skill to be able to understand it.
+
+   5. Termination
+
+5.1. The rights granted under this License will terminate automatically if
+You fail to comply with any of its terms. However, if You become compliant,
+then the rights granted under this License from a particular Contributor are
+reinstated (a) provisionally, unless and until such Contributor explicitly
+and finally terminates Your grants, and (b) on an ongoing basis, if such Contributor
+fails to notify You of the non-compliance by some reasonable means prior to
+60 days after You have come back into compliance. Moreover, Your grants from
+a particular Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the first
+time You have received notice of non-compliance with this License from such
+Contributor, and You become compliant prior to 30 days after Your receipt
+of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent infringement
+claim (excluding declaratory judgment actions, counter-claims, and cross-claims)
+alleging that a Contributor Version directly or indirectly infringes any patent,
+then the rights granted to You by any and all Contributors for the Covered
+Software under Section 2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all end
+user license agreements (excluding distributors and resellers) which have
+been validly granted by You or Your distributors under this License prior
+to termination shall survive termination.
+
+   6. Disclaimer of Warranty
+
+Covered Software is provided under this License on an "as is" basis, without
+warranty of any kind, either expressed, implied, or statutory, including,
+without limitation, warranties that the Covered Software is free of defects,
+merchantable, fit for a particular purpose or non-infringing. The entire risk
+as to the quality and performance of the Covered Software is with You. Should
+any Covered Software prove defective in any respect, You (not any Contributor)
+assume the cost of any necessary servicing, repair, or correction. This disclaimer
+of warranty constitutes an essential part of this License. No use of any Covered
+Software is authorized under this License except under this disclaimer.
+
+   7. Limitation of Liability
+
+Under no circumstances and under no legal theory, whether tort (including
+negligence), contract, or otherwise, shall any Contributor, or anyone who
+distributes Covered Software as permitted above, be liable to You for any
+direct, indirect, special, incidental, or consequential damages of any character
+including, without limitation, damages for lost profits, loss of goodwill,
+work stoppage, computer failure or malfunction, or any and all other commercial
+damages or losses, even if such party shall have been informed of the possibility
+of such damages. This limitation of liability shall not apply to liability
+for death or personal injury resulting from such party's negligence to the
+extent applicable law prohibits such limitation. Some jurisdictions do not
+allow the exclusion or limitation of incidental or consequential damages,
+so this exclusion and limitation may not apply to You.
+
+   8. Litigation
+
+Any litigation relating to this License may be brought only in the courts
+of a jurisdiction where the defendant maintains its principal place of business
+and such litigation shall be governed by laws of that jurisdiction, without
+reference to its conflict-of-law provisions. Nothing in this Section shall
+prevent a party's ability to bring cross-claims or counter-claims.
+
+   9. Miscellaneous
+
+This License represents the complete agreement concerning the subject matter
+hereof. If any provision of this License is held to be unenforceable, such
+provision shall be reformed only to the extent necessary to make it enforceable.
+Any law or regulation which provides that the language of a contract shall
+be construed against the drafter shall not be used to construe this License
+against a Contributor.
+
+   10. Versions of the License
+
+      10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section 10.3,
+no one other than the license steward has the right to modify or publish new
+versions of this License. Each version will be given a distinguishing version
+number.
+
+      10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version of
+the License under which You originally received the Covered Software, or under
+the terms of any subsequent version published by the license steward.
+
+      10.3. Modified Versions
+
+If you create software not governed by this License, and you want to create
+a new license for such software, you may create and use a modified version
+of this License if you rename the license and remove any references to the
+name of the license steward (except to note that such modified license differs
+from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With Secondary
+Licenses under the terms of this version of the License, the notice described
+in Exhibit B of this License must be attached. Exhibit A - Source Code Form
+License Notice
+
+This Source Code Form is subject to the terms of the Mozilla Public License,
+v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain
+one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular file,
+then You may include the notice in a location (such as a LICENSE file in a
+relevant directory) where a recipient would be likely to look for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+
+This Source Code Form is "Incompatible With Secondary Licenses", as defined
+by the Mozilla Public License, v. 2.0.
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,55 @@
+# secure-memory
+
+Securely allocated and deallocated memory.
+
+When handling sensitive data in your program, you want to be extra
+careful and make sure that it is gone as soon as you are done working
+with it. In a garbage-collected language like Haskell this is not so easy,
+since the garbage collector can move your bytes around and create copies
+of it. In addition to that, even if the memory gets eventually deallocated,
+it is not guaranteed that the data will actually be zeroed-out or overriden.
+
+To make matters even worse, if the operating system runs out of RAM while
+your sensitive data remains in the memory, the page that contains your data
+can get swapped out and, thus, end up on the disk, which you, of course,
+absolutely want to never happen.
+
+This library provides a (relatively) easy to use interface for working
+with data allocated in a secure memory location that is guaranteed to never
+end up on the disk and that will be zeroed-out as soon as you finish using it.
+
+## Use
+
+### Get it
+
+Add [`secure-memory`][hackage:secure-memory] to the dependencies of your package.
+
+The current implementation requires `libsodium`, which is a bit unfortunate and,
+hopefully, this dependency will be removed in a future version.
+
+### Data types
+
+Use the `SensitiveBytes` data type provided by this package.
+
+The primary interface for interacting with values of this type is the instance of the
+`ByteArrayAccess` class from the [`memory`][hackage:memory] package.
+Keep in mind that this instance allow you (or a function that you are passing
+the values to) to freely read the sensitive bytes, so it is your responsibility to
+make sure that these bytes do not get copied elsewhere.
+Remember: this library only makes sure that `SensitiveBytes` are allocated in a secure
+memory location and that the garbage collector will not touch them; but there
+is nothing to prevent you from copying them to an insecure location.
+
+[hackage:memory]: https://hackage.haskell.org/package/memory
+
+See the module documentation for the exact guarantees that are provided
+and note that the kinds of protections available differ by the operating
+system.
+
+### Documentation
+
+All documentation exists is in the form of Haddock comments, you can
+find them in the source code or [browse on Hackage][hackage:secure-memory].
+
+
+[hackage:secure-memory]: https://hackage.haskell.org/package/secure-memory
diff --git a/app/checkpw/Main.hs b/app/checkpw/Main.hs
new file mode 100644
--- /dev/null
+++ b/app/checkpw/Main.hs
@@ -0,0 +1,18 @@
+-- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+module Main (main) where
+
+import Data.ByteArray (constEq)
+import Data.SensitiveBytes (withSecureMemory)
+import Data.SensitiveBytes.IO (withUserPassword)
+
+
+main :: IO ()
+main = withSecureMemory $ do
+  withUserPassword 128 (Just "Password: ") $ \pw1 ->
+    withUserPassword 128 (Just "Repeat password: ") $ \pw2 ->
+      if pw1 `constEq` pw2
+      then putStrLn "You are super!"
+      else putStrLn "Passwords do not match."
diff --git a/cbits/readline_max.c b/cbits/readline_max.c
new file mode 100644
--- /dev/null
+++ b/cbits/readline_max.c
@@ -0,0 +1,115 @@
+// SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
+//
+// SPDX-License-Identifier: MPL-2.0
+
+#include <errno.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+#include <wchar.h>
+
+
+/* Read a newline-terminated string into `buf`
+ * from `fin` respecting the locale encoding.
+ *
+ * If the text that the user enters before ending the line does not fit
+ * into the buffer, the extra characters will be silently discarded
+ * (similar to what `readpassphrase` does on BSD).
+ *
+ * Now, here is a plot twist: this function (hopefully) works correctly
+ * with any character encoding and treats multi-byte characters correctly.
+ * It _guarantees_ that the last character will not be broken apart if
+ * it does not fit completely – instead it will discard the entire codepoint.
+ *
+ * Another plot twist is that the text that the user enters
+ * will be represented in their locale encoding, in other words,
+ * pressing the same sequence of buttons on the keyboard on different
+ * systems might result in different byte sequences.
+ * This is extremely tricky and, no, ignoring the locale when reading
+ * the user’s input will make things even worse. If it is desired
+ * that the resulting byte sequences are independent of the system locale,
+ * then it is a good idea to decode the bytes that this function returns
+ * using the system locale encoding and then re-encode them as UTF-8.
+ * Would be cool to do it right here, but this is C :).
+ * (Note that this means that the resulting byte strings can _still_
+ * turn out different if the user’s input fits into the buffer on one
+ * system, but does not fit on another, so allocate generously.)
+ *
+ * The resulting string is not null-terminated.
+ *
+ * Returns the size (in bytes) of the string read or a negative
+ * number if an error occurred.
+ *
+ * -1 = there was an error when reading (see fgetwc).
+ * -2 = something is off with the locale (see wctomb). This is actually impossible.
+ * In either case, `errno` will contain information on the actual error.
+ *
+ * A noteworthy case is when this function returns -1 and errno == EILSEQ,
+ * which means that the user’s terminal is sending bytes that are invalid
+ * in their configured system locale encoding, so their setup is messed up
+ * and there is absolutely no way for us to interpret their input. Too bad.
+ */
+int readline_max(int fd, char *buf, int buf_size) {
+  // On Windows ignore `fd` and always read from `stdin`.
+  #if defined(_WIN32) /* windows */
+    #define READWCHAR() _getwch()
+    #define CLOSE() {}
+  #else /* not windows => unix */
+    FILE* fin = fdopen(fd, "rt");
+    setvbuf(fin, 0, _IONBF, 0);  // disable buffering
+    #define READWCHAR() fgetwc(fin)
+    #define CLOSE() fclose(fin)
+  #endif
+
+  // TODO: Ok, we also need to handle SIGTSTP (and other signals) to restore
+  // terminal echo and, if our process will be resumed, forget all the input
+  // and start reading it from scratch (since the user, obviously, does not
+  // remember what they entered before pausing the process) – that is what
+  // `sudo` does. This means that we need to move the echo control and initial
+  // prompt from Haskell to C, at which point we are already looking at an
+  // entire C library :(.
+  // https://github.com/serokell/haskell-crypto/issues/27
+
+  char *p = (char*)buf;
+  int  no_more_space = 0;
+
+  errno = 0;
+
+  wint_t wc;
+  char encoded[MB_CUR_MAX];
+  while ((wc = READWCHAR()) != WEOF) {
+    // Read a unicode codepoint and see if it is a line terminator
+    // (note: we only accept these two, not what Unicode defines)
+    if (wc == L'\n' || wc == L'\r') {
+        break;
+    } else if (no_more_space) {
+      // we have decided that we are discarding the rest
+      continue;
+    } else {
+      // Now we encode the codepoint back into bytes.
+      // XXX: wchar_t is messed up on Windows, no idea if this
+      // actually works there. All I know is POSIX says it has to :/.
+      int size = wctomb(encoded, (wchar_t)wc);
+      if (size < 0) {
+        CLOSE();
+        return -2;
+      }
+      if (p + size <= buf + buf_size) {
+        // it still fits!
+        memcpy(p, encoded, size);
+        p += size;
+      } else {
+        // discard the remainder
+        no_more_space = 1;
+      }
+    }
+  }
+
+  CLOSE();
+  if (errno != 0) {
+    return -1;
+  } else {
+    return p - buf;
+  }
+}
diff --git a/lib/Data/SensitiveBytes.hs b/lib/Data/SensitiveBytes.hs
new file mode 100644
--- /dev/null
+++ b/lib/Data/SensitiveBytes.hs
@@ -0,0 +1,39 @@
+-- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | The sensitive data type.
+--
+-- A typical usage looks something like this:
+--
+-- @
+-- import Data.SensitiveBytes (SensitiveBytes, withSecureMemory, withSensitiveBytes)
+--
+-- your_function = 'withSecureMemory' $ do
+--  {- some optional initialisation -}
+--  'withSensitiveBytes' 128 $ \sb -> do
+--    {- work with sb using its 'Data.ByteArray.ByteArrayAccess' instance -}
+-- @
+--
+-- Note that 'withSensitiveBytes' can only be called withing a code block
+-- passed to 'withSecureMemory' and its type will prevent your from doing
+-- otherwise.
+--
+-- You will typically read sensitive data into 'SensitiveBytes' using functions
+-- in "Data.SensitiveBytes.IO" and then pass to some other function that
+-- will work with it using the 'Data.ByteArray.ByteArrayAccess' instance. Just make sure
+-- the function you pass it to does not copy the data and does not convert
+-- it to some other insecure byte-array-like type.
+module Data.SensitiveBytes
+  ( -- * Library initialisation
+    withSecureMemory
+  , WithSecureMemory
+  , SecureMemoryInitException
+
+    -- * Allocation
+  , SensitiveBytes
+  , withSensitiveBytes
+  , SensitiveBytesAllocException
+  ) where
+
+import Data.SensitiveBytes.Internal
diff --git a/lib/Data/SensitiveBytes/IO.hs b/lib/Data/SensitiveBytes/IO.hs
new file mode 100644
--- /dev/null
+++ b/lib/Data/SensitiveBytes/IO.hs
@@ -0,0 +1,60 @@
+-- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | Reading and writing sensitive data.
+module Data.SensitiveBytes.IO
+  ( withUserPassword
+  ) where
+
+import Prelude hiding (length)
+
+import Control.Exception.Safe (MonadMask)
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import System.IO (stdin, stdout)
+
+import Data.SensitiveBytes (WithSecureMemory)
+import Data.SensitiveBytes.Internal (SensitiveBytes (..), resized, withSensitiveBytes)
+import Data.SensitiveBytes.IO.Internal.Password (readPassword)
+
+-- | Ask the user to enter their password and read it securely.
+--
+-- “Securely” means “following all the best pracrices”, such as:
+--
+-- * Disable echoing the entered characters back to the terminal.
+-- * Enable some sort of secure input mode, if the OS supports it.
+-- * Store it in a secure region of memory.
+--
+-- Since this function reads the data into securely allocated memory,
+-- which is very expensive to allocate, it needs to know the maximum
+-- possible length of the password to be read.
+-- If the user enters something longer, it will be silently discarded
+-- (similar to @readpassphrase@ on BSD).
+-- In the future it is possible that this limitation will be removed
+-- at the cost of performing multiple expensive allocations.
+--
+-- This function always writes prompt to @stdout@ and then reads from @stdin@.
+--
+-- Example:
+--
+-- @
+-- 'Data.SensitiveBytes.withSecureMemory' $
+--   'withUserPassword' 128 (Just "Enter your password: ") $ \pw -> do
+--     {- hash the @pw@ or do something else with it -}
+-- @
+withUserPassword
+  :: forall m s r. (MonadIO m, MonadMask m, WithSecureMemory)
+  => Int  -- ^ Maximum possible length of the password to read (in bytes).
+  -> Maybe Text  -- ^ Prompt (defaults to "Password: ").
+  -> (SensitiveBytes s -> m r)  -- ^ Action to perform with the password.
+  -> m r
+withUserPassword maxLength mprompt act =
+    withSensitiveBytes allocSize $ \sb@SensitiveBytes{ bufPtr } -> do
+      size <- liftIO $ readPassword stdin stdout prompt bufPtr allocSize
+      act (resized size sb)
+  where
+    defaultPrompt = "Password: "
+    prompt = fromMaybe defaultPrompt mprompt
+    allocSize = maxLength  -- the C function does not null-terminate the string
diff --git a/lib/Data/SensitiveBytes/IO/Internal/Password.hs b/lib/Data/SensitiveBytes/IO/Internal/Password.hs
new file mode 100644
--- /dev/null
+++ b/lib/Data/SensitiveBytes/IO/Internal/Password.hs
@@ -0,0 +1,91 @@
+-- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE InterruptibleFFI #-}
+
+-- | Internal utilities for reading passwords.
+module Data.SensitiveBytes.IO.Internal.Password
+  ( readPassword
+  ) where
+
+import Control.Exception.Safe (MonadMask, bracket)
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Data.Text (Text)
+import qualified Data.Text.IO as T
+import Foreign.C.Error (eILSEQ, getErrno)
+import Foreign.C.Types (CInt (..))
+import Foreign.Ptr (Ptr)
+import System.IO (Handle, hFlush)
+
+#if defined(mingw32_HOST_OS)
+#else
+import Data.Coerce (coerce)
+import System.Posix.IO (handleToFd)
+import System.Posix.Types (Fd (Fd))
+import qualified System.Posix.Terminal as Term
+#endif
+
+
+foreign import ccall interruptible "readline_max"
+  c_readLineMax :: CInt -> Ptr () -> CInt -> IO CInt
+
+-- | A quick wrapper around the C function that turns the Haskell IO
+-- 'Handle' into a system-dependent handle/fd.
+readLineMax :: Handle -> Ptr () -> CInt -> IO CInt
+#if defined(mingw32_HOST_OS)
+readLineMax _ bufPtr maxLength = do
+  c_readLineMax 0 bufPtr maxLength
+#else
+readLineMax hIn bufPtr maxLength = do
+  fdIn <- handleToFd hIn
+  c_readLineMax (coerce fdIn) bufPtr maxLength
+#endif
+
+
+-- | Flush stdout, disable echo, and read user input from stdin.
+readPassword
+  :: Handle  -- ^ Input file handle.
+  -> Handle  -- ^ Output file handle.
+  -> Text  -- ^ Prompt.
+  -> Ptr ()  -- ^ Target buffer.
+  -> Int  -- ^ Target buffer size.
+  -> IO Int
+readPassword hIn hOut prompt bufPtr allocSize = do
+  T.hPutStr hOut prompt
+  withEchoDisabled hIn $ do
+    hFlush hOut  -- need to flush _after_ echo is disabled
+    -- TODO: Do we also want to install signal handlers?
+    res <- readLineMax hIn bufPtr (fromIntegral allocSize)
+    if res >= 0
+    then do
+      T.hPutStrLn hOut ""
+      pure $ fromIntegral res
+    else do
+      errno <- getErrno
+      -- TODO: Maybe return a Maybe or throw a proper exception?
+      case res of
+        -1 -> do
+          if errno == eILSEQ
+          then error "readPassword: locale/terminal misconfiguration"
+          else error "readPassword: read error"
+        _ -> error $ "readPassword: impossible error happened: " <> show res
+
+-- | Run an action with terminal echo off (and then restore it).
+withEchoDisabled :: (MonadIO m, MonadMask m) => Handle -> m r -> m r
+#if defined(mingw32_HOST_OS)
+withEchoDisabled _ = id  -- on Windows our @c_readLineMax@ does not echo anyway
+#else
+withEchoDisabled hIn act = do
+  fin <- liftIO $ handleToFd hIn
+  liftIO (Term.queryTerminal fin) >>= \case
+    False -> act
+    True -> do
+      attrs <- liftIO $ Term.getTerminalAttributes fin
+      let attrsNoEcho = Term.withoutMode attrs Term.EnableEcho
+      bracket
+        (liftIO $ Term.setTerminalAttributes fin attrsNoEcho Term.WhenFlushed)
+        (\_ -> liftIO $ Term.setTerminalAttributes fin attrs Term.Immediately)
+        (\_ -> act)
+#endif
diff --git a/lib/Data/SensitiveBytes/Internal.hs b/lib/Data/SensitiveBytes/Internal.hs
new file mode 100644
--- /dev/null
+++ b/lib/Data/SensitiveBytes/Internal.hs
@@ -0,0 +1,213 @@
+-- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+{-# LANGUAGE ConstraintKinds, RankNTypes #-}
+
+-- | The sensitive data type internals.
+module Data.SensitiveBytes.Internal
+  ( withSecureMemory
+  , WithSecureMemory
+  , SodiumInitialised
+  , SecureMemoryInitException
+
+  , SensitiveBytes (..)
+  , allocate
+  , free
+  , unsafePtr
+  , resized
+
+  , withSensitiveBytes
+  , SensitiveBytesAllocException
+  ) where
+
+import Control.Exception.Safe (Exception, MonadMask, bracket, throwIO)
+import Control.Monad.IO.Class (MonadIO, liftIO)
+import Data.ByteArray (ByteArrayAccess (length, withByteArray))
+import Data.Reflection (Given, give, given)
+import Foreign.Ptr (Ptr, castPtr, nullPtr)
+import Libsodium (sodium_free, sodium_init, sodium_malloc, sodium_memzero)
+
+
+-- | A trivial proof that @sodium_init@ has been called.
+data SodiumInitialised = SodiumInitialised
+
+-- | A constraint for functions that require access to secure memory.
+-- The only way to satisfy it is to call 'withSecureMemory'.
+type WithSecureMemory = Given SodiumInitialised
+
+
+-- | This function performs the initialisation steps
+-- required for allocating data in secure memory regions.
+--
+-- The basic usage is to call this function and provide to it
+-- a block of code that will be allocating memory for sensitive
+-- data. The type of 'withSensitiveBytes' is such that it can
+-- only be called withing such a code block.
+--
+-- Ideally, you should call 'withSecureMemory' only once and deal
+-- with all your sensitive data within this single code block,
+-- however it is not a requirement – you can call it as many
+-- times as you wish and the only downside to doing so is that
+-- it will incur a tiny performance penalty.
+--
+-- In some rare circumstances this function secure memory initialisation
+-- may fail, in which case this function will throw
+-- 'SecureMemoryInitException'.
+withSecureMemory
+  :: forall m r. MonadIO m
+  => (WithSecureMemory => m r)  -- ^ Action to perform.
+  -> m r
+withSecureMemory act = do
+  liftIO $ sodium_init >>= \case
+    0 ->
+      -- Ok
+      pure ()
+    1 ->
+      -- Already initialised, ok
+      pure ()
+    _ ->
+      -- sodium_init failed, not good
+      throwIO SodiumInitFailed
+  give SodiumInitialised act
+
+-- | Exception thrown by 'withSecureMemory'.
+data SecureMemoryInitException
+  = SodiumInitFailed  -- ^ libsodium failed to initialise.
+
+instance Show SecureMemoryInitException where
+  show SodiumInitFailed =
+    "Failed to initialise a secure memory region"
+
+instance Exception SecureMemoryInitException
+
+
+-- | Bytes that will be allocated in a secure memory location
+-- such that they will never be moved by the garbage collector
+-- and, hopefully, never swapped out to the disk (if the
+-- operating system supports this kind of protection).
+data SensitiveBytes s = SensitiveBytes
+  { allocSize :: Int  -- ^ Size of the allocated buffer.
+  , dataSize :: Int  -- ^ Size of the actual data stored.
+  , bufPtr :: Ptr ()  -- ^ Buffer pointer.
+  }
+
+instance ByteArrayAccess (SensitiveBytes s) where
+  length SensitiveBytes{ dataSize } = dataSize
+  withByteArray SensitiveBytes{ bufPtr } act = act (castPtr bufPtr)
+
+-- | Get the underlying data pointer.
+--
+-- This function is unsafe, because it discards the second-order context
+-- and thus can allow the pointer to escape its scope and be used after free.
+unsafePtr :: SensitiveBytes s -> Ptr ()
+unsafePtr = bufPtr
+
+
+-- | Allocate bytes in a protected memory region.
+--
+-- Just as regular @malloc@, this function can fail, for example,
+-- if there is not enough memory. In this case, it will throw
+-- 'SensitiveBytesAllocException'.
+allocate
+  :: forall s m. (MonadIO m, WithSecureMemory)
+  => Int  -- ^ Size of the array (in bytes).
+  -> m (SensitiveBytes s)
+allocate size = requiringSecureMemory . liftIO $ do
+  res <- sodium_malloc (fromIntegral size)
+  if res == nullPtr then
+    throwIO SodiumMallocFailed
+  else
+    pure $ SensitiveBytes size size res
+
+-- | Free bytes previously allocated in a protected memory region.
+free
+  :: forall s m. (MonadIO m, WithSecureMemory)
+  => SensitiveBytes s
+  -> m ()
+free SensitiveBytes{ bufPtr } = requiringSecureMemory $
+  liftIO $ sodium_free bufPtr
+
+-- | Zero-out memory.
+memzero
+  :: forall s m. (MonadIO m)
+  => SensitiveBytes s
+  -> m ()
+memzero SensitiveBytes{ allocSize, bufPtr } =
+  liftIO $ sodium_memzero bufPtr (fromIntegral allocSize)
+
+-- | Rewrite the recorded size of the data.
+--
+-- This is a very dangerous internal-only function. It is essentially
+-- a hack that allows other functions exported from this library to
+-- efficiently read data of unknown size by first allocating a large buffer
+-- and then tweaking the 'ByteArrayAccess' instance to return the size that
+-- is smaller than what was actually allocated.
+resized
+  :: forall s. ()
+  => Int  -- ^ New data size.
+  -> SensitiveBytes s  -- ^ What to resize.
+  -> SensitiveBytes s
+resized newSize sb@SensitiveBytes{ allocSize }
+  | newSize <= allocSize = sb{ dataSize = newSize }
+  | otherwise = error "SensitiveBytes.Internal.resized: the new size is too large"
+
+
+-- | Allocate a byte array in a secure memory region.
+--
+-- This function guarantees that:
+--
+-- 1. The garbage collector will not touch the allocated memory and
+--    will not try to copy the sensitive data.
+-- 2. The memory will be zeroed-out and freed as soon as the computation
+--    finishes.
+--
+-- Additionally, it will try its best (subject to the support from
+-- the operating system) to do the following:
+--
+-- 1. Allocate the buffer at the end of a page and make sure that the
+--    following page is not mapped, so trying to access past the end of
+--    the buffer will crash the program.
+-- 2. Place a canary immediately before the buffer, check that it was not
+--    modified before deallocating the memory, and crash the program otherwise.
+-- 3. @mlock@ the memory to make sure it will not be paged to the disk.
+-- 4. Ask the operating system not to include this memory in core dumps.
+--
+-- Just as with regular @malloc@, allocation can fail, for example,
+-- if there is not enough memory. In this case, the function will throw
+-- 'SensitiveBytesAllocException'.
+withSensitiveBytes
+  :: forall s m r. (MonadIO m, MonadMask m, WithSecureMemory)
+  => Int  -- ^ Size of the array (in bytes).
+  -> (SensitiveBytes s -> m r)  -- ^ Action to perform with memory allocated.
+  -> m r
+-- TODO: libsodium docs also say something about the allocated size being
+-- a multiple of the required alignment, but it is not clear what the
+-- implications are (I added a test, just in case).
+withSensitiveBytes size = bracket (allocate size) finalise
+  where
+    -- OK, this is weird, but libsodium has a whole bunch of ifdefs that
+    -- control the logic of @sodium_free@ and, for some reason, if it does
+    -- not @HAVE_ALIGNED_MALLOC@, it will not zero-out the memory.
+    -- Cool story, but this makes no sense, so we zero-out it ourselves
+    -- in case we are on such a system.
+    finalise sb = memzero sb *> free sb
+
+-- | Exception thrown by 'withSensitiveBytes'.
+data SensitiveBytesAllocException
+  = SodiumMallocFailed  -- ^ @sodium_malloc@ returned NULL.
+
+instance Show SensitiveBytesAllocException where
+  show SodiumMallocFailed =
+    "Failed to allocate secure memory"
+
+instance Exception SensitiveBytesAllocException
+
+
+
+-- | An internal helper that fakes needing "WithSecureMemory".
+--
+-- It is a complete no-op and exists only to silence the unused constraint
+-- warning. Hopefully, it will get optimised away every time.
+requiringSecureMemory :: r -> (WithSecureMemory => r)
+requiringSecureMemory act = (\_ -> act) (given :: SodiumInitialised)
diff --git a/secure-memory.cabal b/secure-memory.cabal
new file mode 100644
--- /dev/null
+++ b/secure-memory.cabal
@@ -0,0 +1,192 @@
+cabal-version: 1.18
+
+-- This file has been generated from package.yaml by hpack version 0.34.4.
+--
+-- see: https://github.com/sol/hpack
+
+name:           secure-memory
+version:        0.0.0.1
+synopsis:       Securely allocated and deallocated memory.
+description:    Securely allocated and deallocated memory.
+                .
+                When handling sensitive data in your program, you want to be extra
+                careful and make sure that it is gone as soon as you are done working
+                with it. In a garbage-collected language like Haskell this is not so easy,
+                since the garbage collector can move your bytes around and create copies
+                of it. In addition to that, even if the memory gets eventually deallocated,
+                it is not guaranteed that the data will actually be zeroed-out or overriden.
+                .
+                To make matters even worse, if the operating system runs out of RAM while
+                your sensitive data remains in the memory, the page that contains your data
+                can get swapped out and, thus, end up on the disk, which you, of course,
+                absolutely want to never happen.
+                .
+                This library provides a (relatively) easy to use interface for working
+                with data allocated in a secure memory location that is guaranteed to never
+                end up on the disk and that will be zeroed-out as soon as you finish using it.
+category:       Cryptography, Memory
+homepage:       https://github.com/serokell/haskell-crypto#readme
+bug-reports:    https://github.com/serokell/haskell-crypto/issues
+author:         Kirill Elagin <kirelagin@serokell.io>
+maintainer:     Serokell <libraries@serokell.io>
+copyright:      2021 Serokell
+license:        MPL-2.0
+license-file:   LICENSES/MPL-2.0.txt
+build-type:     Simple
+extra-doc-files:
+    CHANGELOG.md
+    README.md
+
+source-repository head
+  type: git
+  location: https://github.com/serokell/haskell-crypto
+
+library
+  exposed-modules:
+      Data.SensitiveBytes
+      Data.SensitiveBytes.Internal
+      Data.SensitiveBytes.IO
+      Data.SensitiveBytes.IO.Internal.Password
+  other-modules:
+      Paths_secure_memory
+  hs-source-dirs:
+      lib
+  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
+  c-sources:
+      ./cbits/readline_max.c
+  build-depends:
+      base >=4.10 && <4.15
+    , bytestring >=0.9 && <0.11
+    , libsodium >=1.0.18.2
+    , memory >=0.14.15 && <0.16
+    , reflection >=1.2.0.1 && <2.2
+    , safe-exceptions ==0.1.*
+    , text >=0.7 && <1.3
+  if !os(windows)
+    build-depends:
+        unix >=2.0 && <2.8
+  default-language: Haskell2010
+
+executable checkpw
+  main-is: Main.hs
+  other-modules:
+      Paths_secure_memory
+  hs-source-dirs:
+      app/checkpw
+  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:
+      base >=4.10 && <4.15
+    , bytestring >=0.9 && <0.11
+    , libsodium >=1.0.11 && <2
+    , memory >=0.14.15 && <0.16
+    , safe-exceptions ==0.1.*
+    , secure-memory
+  default-language: Haskell2010
+
+test-suite test-pipe
+  type: exitcode-stdio-1.0
+  main-is: Test.hs
+  other-modules:
+      Test.Data.SensitiveBytes.IO.Password
+      Paths_secure_memory
+  hs-source-dirs:
+      test/pipe
+  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
+    , async
+    , base >=4.10 && <4.15
+    , bytestring >=0.9 && <0.11
+    , hedgehog
+    , libsodium >=1.0.11 && <2
+    , memory >=0.14.15 && <0.16
+    , safe-exceptions ==0.1.*
+    , secure-memory
+    , tasty
+    , tasty-hedgehog
+    , tasty-hunit
+    , unix
+  if os(windows)
+    buildable: False
+  default-language: Haskell2010
+
+test-suite test-simple
+  type: exitcode-stdio-1.0
+  main-is: Test.hs
+  other-modules:
+      Test.Data.SensitiveBytes
+      Paths_secure_memory
+  hs-source-dirs:
+      test/simple
+  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
+    , bytestring >=0.9 && <0.11
+    , libsodium >=1.0.11 && <2
+    , memory >=0.14.15 && <0.16
+    , safe-exceptions ==0.1.*
+    , secure-memory
+    , tasty
+    , tasty-hunit
+  default-language: Haskell2010
diff --git a/test/pipe/Test.hs b/test/pipe/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/pipe/Test.hs
@@ -0,0 +1,6 @@
+{- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+{-# OPTIONS_GHC -F -pgmF tasty-discover -optF --tree-display #-}
diff --git a/test/pipe/Test/Data/SensitiveBytes/IO/Password.hs b/test/pipe/Test/Data/SensitiveBytes/IO/Password.hs
new file mode 100644
--- /dev/null
+++ b/test/pipe/Test/Data/SensitiveBytes/IO/Password.hs
@@ -0,0 +1,125 @@
+-- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+-- | Tests for reading passwords.
+module Test.Data.SensitiveBytes.IO.Password where
+
+import Control.Concurrent.Async (waitBoth, withAsync)
+import Control.Exception.Safe (bracket, onException, uninterruptibleMask)
+import Control.Monad.IO.Class (liftIO)
+import Data.ByteArray (allocRet)
+import Data.ByteString (ByteString)
+import qualified Data.ByteString as BS
+import System.IO (Handle, hClose)
+
+import Data.Maybe (fromMaybe)
+import System.Posix.IO (closeFd, createPipe, fdToHandle)
+import System.Timeout (timeout)
+
+import Hedgehog (MonadGen, Property, (===), forAll, property, withTests)
+import qualified Hedgehog.Gen as G
+import qualified Hedgehog.Range as R
+import Test.Tasty.HUnit ((@?=))
+
+import Data.SensitiveBytes.IO.Internal.Password (readPassword)
+
+
+-- | Read a user-provided password into a 'ByteString'.
+-- This is a terrible, terrible idea, since it copies
+-- the password from the secure memory into the regular
+-- GC-managed heap. Never ever do this (except for tests).
+unsafeReadPassword :: Handle -> Handle -> Int -> IO ByteString
+unsafeReadPassword hIn hOut maxLength = do
+  (size, bs) <- allocRet maxLength $ \ba ->
+    readPassword hIn hOut "Password: " ba maxLength
+  pure $ BS.take size bs
+
+-- | Read a password from the 'ByteString' provided.
+--
+-- Make sure the input bytes can be read using current locale.
+-- Unfortunately, this function only works on Unix, since I don’t know
+-- any easy way to create a pipe on Windows.
+--
+-- This requires a threaded runtime due to the use of async.
+unsafeReadPasswordFrom :: ByteString -> Int -> IO (ByteString, ByteString)
+unsafeReadPasswordFrom input maxLength = fmap orDie $ timeout one_second $
+    -- Create two pipes: one for stdin, one for stdout.
+    withPipeHandles $ \(hInRead, hInWrite) ->
+     withPipeHandles $ \(hOutRead, hOutWrite) -> do
+      -- Feed password to stdin. Let’s hope the pipe is large enough
+      -- and will not block.
+      BS.hPutStr hInWrite (input <> "\n")
+      hClose hInWrite
+
+      -- This thread will capture the stdout.
+      withAsync (readHandle hOutRead) $ \aStdoutReader -> do
+        -- This thread will read the password.
+        withAsync (readPassword' hInRead hOutWrite) $ \aPasswordReader -> do
+          -- XXX: Ideally, _this_ is where we want to start feeding
+          -- the password to stdin, however GHC’s thread scheduler is
+          -- having a tough time interacting with a blocking C function.
+          -- At this point, I suspect it will be easier to just reimplement
+          -- the entire thing in Haskell.
+
+          -- Now we wait for everyone else to finish.
+          waitBoth aStdoutReader aPasswordReader
+  where
+    one_second = 1000000
+    orDie = fromMaybe (error "timeout")
+
+    withPipeHandles act =
+        bracket
+          openPipeHandles
+          (\(hRead, hWrite) -> hClose hRead >> hClose hWrite)
+          act
+      where
+        -- This is a bit tricky, since we need to 'closeFd' the descriptor on exception,
+        -- but after it has been converted to a handle, we no longer need to 'closeFd' it.
+        openPipeHandles = uninterruptibleMask $ \restore -> do
+          (fdRead, fdWrite) <- createPipe
+          hRead <- restore (fdToHandle fdRead) `onException` (closeFd fdRead >> closeFd fdWrite)
+          hWrite <- restore (fdToHandle fdWrite) `onException` (hClose hRead >> closeFd fdWrite)
+          pure (hRead, hWrite)
+
+    readHandle h = BS.hGetContents h <* hClose h
+
+    readPassword' hIn hOut =
+      unsafeReadPassword hIn hOut maxLength <* hClose hOut <* hClose hIn
+
+-----------------------------------------
+
+-- | A generator for a printable ASCII character.
+asciiPrintable :: MonadGen m => m Char
+asciiPrintable = G.element ['\32' .. '\126']
+
+-----------------------------------------
+
+unit_test_unsafe_read :: IO ()
+unit_test_unsafe_read = do
+  (stdoutBs, pass) <- unsafeReadPasswordFrom "hello" 16
+  stdoutBs @?= "Password: \n"
+  pass @?= "hello"
+
+-- | Test for the test itself (there was a race condition in the piping code).
+-- Also conveniently tests that we do not leak handles and descriptors :).
+hprop_test_test :: Property
+hprop_test_test = withTests 10000 $ property $ do
+  (stdoutBs, pass) <- liftIO $ unsafeReadPasswordFrom "hello" 16
+  stdoutBs === "Password: \n"
+  pass === "hello"
+
+hprop_ascii :: Property
+hprop_ascii = property $ do
+  input <- forAll $ G.utf8 (R.linear 0 100) asciiPrintable
+  (_, pass) <- liftIO $ unsafeReadPasswordFrom input 100
+  pass === input
+
+hprop_ascii_longer :: Property
+hprop_ascii_longer = property $ do
+  size <- forAll $ G.integral (R.linear 0 100)
+  extra <- forAll $ G.integral (R.linear 1 100)
+  -- input is longer than the allocated buffer by `extra`
+  input <- forAll $ G.utf8 (R.singleton $ size + extra) asciiPrintable
+  (_, pass) <- liftIO $ unsafeReadPasswordFrom input size
+  pass === BS.take size input
diff --git a/test/simple/Test.hs b/test/simple/Test.hs
new file mode 100644
--- /dev/null
+++ b/test/simple/Test.hs
@@ -0,0 +1,6 @@
+{- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
+ -
+ - SPDX-License-Identifier: MPL-2.0
+ -}
+
+{-# OPTIONS_GHC -F -pgmF tasty-discover -optF --tree-display #-}
diff --git a/test/simple/Test/Data/SensitiveBytes.hs b/test/simple/Test/Data/SensitiveBytes.hs
new file mode 100644
--- /dev/null
+++ b/test/simple/Test/Data/SensitiveBytes.hs
@@ -0,0 +1,20 @@
+-- SPDX-FileCopyrightText: 2021 Serokell <https://serokell.io/>
+--
+-- SPDX-License-Identifier: MPL-2.0
+
+module Test.Data.SensitiveBytes where
+
+import qualified Data.SensitiveBytes as SB
+
+
+unit_nop :: IO ()
+unit_nop = SB.withSecureMemory $ SB.withSensitiveBytes 128 (\_ptr -> pure ())
+
+unit_unaligned :: IO ()
+unit_unaligned = SB.withSecureMemory $ SB.withSensitiveBytes 126 (\_ptr -> pure ())
+
+unit_withSecureMemory_twice :: IO ()
+unit_withSecureMemory_twice
+  = SB.withSecureMemory
+  $ SB.withSecureMemory  -- this has to be fine
+  $ SB.withSensitiveBytes 128 (\_ptr -> pure ())
