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 +10/−0
- LICENSES/MPL-2.0.txt +312/−0
- README.md +55/−0
- app/checkpw/Main.hs +18/−0
- cbits/readline_max.c +115/−0
- lib/Data/SensitiveBytes.hs +39/−0
- lib/Data/SensitiveBytes/IO.hs +60/−0
- lib/Data/SensitiveBytes/IO/Internal/Password.hs +91/−0
- lib/Data/SensitiveBytes/Internal.hs +213/−0
- secure-memory.cabal +192/−0
- test/pipe/Test.hs +6/−0
- test/pipe/Test/Data/SensitiveBytes/IO/Password.hs +125/−0
- test/simple/Test.hs +6/−0
- test/simple/Test/Data/SensitiveBytes.hs +20/−0
+ 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 ())