-- SPDX-FileCopyrightText: 2021 Serokell -- -- 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 act) where act = 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)