packages feed

secure-memory (empty) → 0.0.0.1

raw patch · 14 files changed

+1262/−0 lines, 14 filesdep +HUnitdep +asyncdep +base

Dependencies added: HUnit, async, base, bytestring, hedgehog, libsodium, memory, reflection, safe-exceptions, secure-memory, tasty, tasty-hedgehog, tasty-hunit, text, unix

Files

+ CHANGELOG.md view
@@ -0,0 +1,10 @@+# Changelog++## Unreleased++### Added++* The `SensitiveBytes` data type.+  * `withSecureMemory`+  * `withSensitiveBytes`+* IO: `withUserPassword`
+ LICENSES/MPL-2.0.txt view
@@ -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.
+ README.md view
@@ -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
+ app/checkpw/Main.hs view
@@ -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."
+ cbits/readline_max.c view
@@ -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;+  }+}
+ lib/Data/SensitiveBytes.hs view
@@ -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
+ lib/Data/SensitiveBytes/IO.hs view
@@ -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
+ lib/Data/SensitiveBytes/IO/Internal/Password.hs view
@@ -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
+ lib/Data/SensitiveBytes/Internal.hs view
@@ -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)
+ secure-memory.cabal view
@@ -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
+ test/pipe/Test.hs view
@@ -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 #-}
+ test/pipe/Test/Data/SensitiveBytes/IO/Password.hs view
@@ -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
+ test/simple/Test.hs view
@@ -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 #-}
+ test/simple/Test/Data/SensitiveBytes.hs view
@@ -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 ())