{-# LANGUAGE DataKinds #-} {-# LANGUAGE DerivingVia #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -- | -- -- Module: Sel.SecretKey.Cipher -- Description: Authenticated Encryption with Poly1305 MAC and XSalsa20 -- Copyright: (C) Hécate Moonlight 2022 -- License: BSD-3-Clause -- Maintainer: The Haskell Cryptography Group -- Portability: GHC only module Sel.SecretKey.Cipher ( -- ** Introduction -- $introduction -- ** Usage -- $usage -- ** Encryption and Decryption encrypt , decrypt -- ** Secret Key , SecretKey , newSecretKey , secretKeyFromHexByteString , unsafeSecretKeyToHexByteString -- ** Nonce , Nonce , nonceFromHexByteString , nonceToHexByteString -- ** Hash , Hash , hashFromHexByteString , hashToBinary , hashToHexByteString , hashToHexText ) where import Control.Monad (void, when) import qualified Data.Base16.Types as Base16 import Data.ByteString (StrictByteString) import qualified Data.ByteString as BS import qualified Data.ByteString.Base16 as Base16 import qualified Data.ByteString.Internal as BS import qualified Data.ByteString.Unsafe as BS import Data.Text (Text) import qualified Data.Text as Text import Data.Text.Display (Display (displayBuilder), OpaqueInstance (..), ShowInstance (..)) import qualified Data.Text.Lazy.Builder as Builder import Data.Word (Word8) import Foreign (ForeignPtr) import qualified Foreign import Foreign.C (CChar, CSize, CUChar, CULLong, throwErrno) import GHC.IO.Handle.Text (memcpy) import System.IO.Unsafe (unsafeDupablePerformIO) import LibSodium.Bindings.Random (randombytesBuf) import LibSodium.Bindings.Secretbox ( cryptoSecretboxEasy , cryptoSecretboxKeyBytes , cryptoSecretboxKeygen , cryptoSecretboxMACBytes , cryptoSecretboxNonceBytes , cryptoSecretboxOpenEasy ) import LibSodium.Bindings.SecureMemory import Sel.Internal -- $introduction -- "Authenticated Encryption" uses a secret key along with a single-use number -- called a "nonce" to encrypt a message. -- The resulting hash is accompanied by an authentication tag. -- -- Encryption is done with the XSalsa20 stream cipher and authentication is done with the -- Poly1305 MAC hash. -- $usage -- -- > import qualified Sel.SecretKey.Cipher as Cipher -- > -- > main = do -- > -- We get the secretKey from the other party or with 'newSecretKey'. -- > -- We get the nonce from the other party with the message, or with 'encrypt' and our own message. -- > -- Do not reuse a nonce with the same secret key! -- > (nonce, encryptedMessage) <- Cipher.encrypt "hello hello" secretKey -- > let result = Cipher.decrypt encryptedMessage secretKey nonce -- > print result -- > -- "Just \"hello hello\"" -- | A secret key of size 'cryptoSecretboxKeyBytes'. -- -- @since 0.0.1.0 newtype SecretKey = SecretKey (ForeignPtr CUChar) deriving ( Display -- ^ @since 0.0.1.0 -- > display secretKey == "[REDACTED]" ) via (OpaqueInstance "[REDACTED]" SecretKey) -- | -- -- @since 0.0.1.0 instance Eq SecretKey where (SecretKey hk1) == (SecretKey hk2) = unsafeDupablePerformIO $ foreignPtrEq hk1 hk2 cryptoSecretboxKeyBytes -- | -- -- @since 0.0.1.0 instance Ord SecretKey where compare (SecretKey hk1) (SecretKey hk2) = unsafeDupablePerformIO $ foreignPtrOrd hk1 hk2 cryptoSecretboxKeyBytes -- | > show secretKey == "[REDACTED]" -- -- @since 0.0.1.0 instance Show SecretKey where show _ = "[REDACTED]" -- | Generate a new random secret key. -- -- @since 0.0.1.0 newSecretKey :: IO SecretKey newSecretKey = newSecretKeyWith cryptoSecretboxKeygen -- | Create a 'SecretKey' from a binary 'StrictByteString' that you have obtained on your own, -- usually from the network or disk. -- -- The input secret key, once decoded from base16, must be of length -- 'cryptoSecretboxKeyBytes'. -- -- @since 0.0.1.0 secretKeyFromHexByteString :: StrictByteString -> Either Text SecretKey secretKeyFromHexByteString hexNonce = unsafeDupablePerformIO $ case Base16.decodeBase16Untyped hexNonce of Right bytestring -> if BS.length bytestring == fromIntegral cryptoSecretboxKeyBytes then BS.unsafeUseAsCStringLen bytestring $ \(outsideSecretKeyPtr, _) -> fmap Right $ newSecretKeyWith $ \secretKeyPtr -> Foreign.copyArray (Foreign.castPtr @CUChar @CChar secretKeyPtr) outsideSecretKeyPtr (fromIntegral cryptoSecretboxKeyBytes) else pure $ Left $ Text.pack "Secret Key is too short" Left msg -> pure $ Left msg -- | Prepare memory for a 'SecretKey' and use the provided action to fill it. -- -- Memory is allocated with 'LibSodium.Bindings.SecureMemory.sodiumMalloc' (see the note attached there). -- A finalizer is run when the key is goes out of scope. -- -- @since 0.0.1.0 newSecretKeyWith :: (Foreign.Ptr CUChar -> IO ()) -> IO SecretKey newSecretKeyWith action = do ptr <- sodiumMalloc cryptoSecretboxKeyBytes when (ptr == Foreign.nullPtr) $ do throwErrno "sodium_malloc" fPtr <- Foreign.newForeignPtr finalizerSodiumFree ptr action ptr pure $ SecretKey fPtr -- | Convert a 'SecretKey' to a hexadecimal-encoded 'StrictByteString'. -- -- ⚠️ Be prudent as to where you store it! -- -- @since 0.0.1.0 unsafeSecretKeyToHexByteString :: SecretKey -> StrictByteString unsafeSecretKeyToHexByteString (SecretKey secretKeyForeignPtr) = Base16.extractBase16 . Base16.encodeBase16' $ BS.fromForeignPtr0 (Foreign.castForeignPtr @CUChar @Word8 secretKeyForeignPtr) (fromIntegral @CSize @Int cryptoSecretboxKeyBytes) -- | A random number that must only be used once per exchanged message. -- It does not have to be confidential. -- It is of size 'cryptoSecretboxNonceBytes'. -- -- @since 0.0.1.0 newtype Nonce = Nonce (ForeignPtr CUChar) deriving ( Display -- ^ @since 0.0.1.0 ) via (ShowInstance Nonce) -- | -- -- @since 0.0.1.0 instance Eq Nonce where (Nonce hk1) == (Nonce hk2) = unsafeDupablePerformIO $ foreignPtrEq hk1 hk2 cryptoSecretboxNonceBytes -- | -- -- @since 0.0.1.0 instance Ord Nonce where compare (Nonce hk1) (Nonce hk2) = unsafeDupablePerformIO $ foreignPtrOrd hk1 hk2 cryptoSecretboxNonceBytes -- | -- -- @since 0.0.1.0 instance Show Nonce where show = show . nonceToHexByteString -- | Generate a new random nonce. -- Only use it once per exchanged message. -- -- Do not use this outside of hash creation! newNonce :: IO Nonce newNonce = do (fPtr :: ForeignPtr CUChar) <- Foreign.mallocForeignPtrBytes (fromIntegral cryptoSecretboxNonceBytes) Foreign.withForeignPtr fPtr $ \ptr -> randombytesBuf (Foreign.castPtr @CUChar @Word8 ptr) cryptoSecretboxNonceBytes pure $ Nonce fPtr -- | Create a 'Nonce' from a binary 'StrictByteString' that you have obtained on your own, -- usually from the network or disk. -- Once decoded from hexadecimal, it must be of length 'cryptoSecretboxNonceBytes'. -- -- @since 0.0.1.0 nonceFromHexByteString :: StrictByteString -> Either Text Nonce nonceFromHexByteString hexNonce = unsafeDupablePerformIO $ case Base16.decodeBase16Untyped hexNonce of Right bytestring -> if BS.length bytestring == fromIntegral @CSize @Int cryptoSecretboxNonceBytes then BS.unsafeUseAsCStringLen bytestring $ \(outsideNoncePtr, _) -> do nonceForeignPtr <- BS.mallocByteString @CChar (fromIntegral cryptoSecretboxNonceBytes) Foreign.withForeignPtr nonceForeignPtr $ \noncePtr -> Foreign.copyArray noncePtr outsideNoncePtr (fromIntegral cryptoSecretboxNonceBytes) pure $ Right $ Nonce (Foreign.castForeignPtr @CChar @CUChar nonceForeignPtr) else pure $ Left $ Text.pack "Nonce is too short" Left msg -> pure $ Left msg -- | Convert a 'Nonce' to a hexadecimal-encoded 'StrictByteString'. -- -- @since 0.0.1.0 nonceToHexByteString :: Nonce -> StrictByteString nonceToHexByteString (Nonce nonceForeignPtr) = Base16.extractBase16 . Base16.encodeBase16' $ BS.fromForeignPtr0 (Foreign.castForeignPtr @CUChar @Word8 nonceForeignPtr) (fromIntegral @CSize @Int cryptoSecretboxNonceBytes) -- | A ciphertext consisting of an encrypted message and an authentication tag. -- -- @since 0.0.1.0 data Hash = Hash { messageLength :: CULLong , hashForeignPtr :: ForeignPtr CUChar } -- | -- -- @since 0.0.1.0 instance Eq Hash where (Hash messageLength1 hk1) == (Hash messageLength2 hk2) = unsafeDupablePerformIO $ do result1 <- foreignPtrEq hk1 hk2 (fromIntegral messageLength1 + cryptoSecretboxMACBytes) pure $ (messageLength1 == messageLength2) && result1 -- | -- -- @since 0.0.1.0 instance Ord Hash where compare (Hash messageLength1 hk1) (Hash messageLength2 hk2) = unsafeDupablePerformIO $ do result1 <- foreignPtrOrd hk1 hk2 (fromIntegral messageLength1 + cryptoSecretboxMACBytes) pure $ compare messageLength1 messageLength2 <> result1 -- | ⚠️ Be prudent as to what you do with it! -- -- @since 0.0.1.0 instance Display Hash where displayBuilder = Builder.fromText . hashToHexText -- | ⚠️ Be prudent as to what you do with it! -- -- @since 0.0.1.0 instance Show Hash where show = BS.unpackChars . hashToHexByteString -- | Create a 'Hash' from a binary 'StrictByteString' that you have obtained on your own, -- usually from the network or disk. It must be a valid hash built from the concatenation -- of the encrypted message and the authentication tag. -- -- The input hash must at least of length 'cryptoSecretboxMACBytes' -- -- @since 0.0.1.0 hashFromHexByteString :: StrictByteString -> Either Text Hash hashFromHexByteString hexHash = unsafeDupablePerformIO $ case Base16.decodeBase16Untyped hexHash of Right bytestring -> if BS.length bytestring >= fromIntegral cryptoSecretboxMACBytes then BS.unsafeUseAsCStringLen bytestring $ \(outsideHashPtr, outsideHashLength) -> do hashForeignPtr <- BS.mallocByteString @CChar outsideHashLength -- The foreign pointer that will receive the hash data. Foreign.withForeignPtr hashForeignPtr $ \hashPtr -> -- We copy bytes from 'outsideHashPtr' to 'hashPtr'. Foreign.copyArray hashPtr outsideHashPtr outsideHashLength pure $ Right $ Hash (fromIntegral @Int @CULLong outsideHashLength - fromIntegral @CSize @CULLong cryptoSecretboxMACBytes) (Foreign.castForeignPtr @CChar @CUChar hashForeignPtr) else pure $ Left $ Text.pack "Hash is too short" Left msg -> pure $ Left msg -- | Convert a 'Hash' to a hexadecimal-encoded 'Text'. -- -- ⚠️ Be prudent as to where you store it! -- -- @since 0.0.1.0 hashToHexText :: Hash -> Text hashToHexText = Base16.extractBase16 . Base16.encodeBase16 . hashToBinary -- | Convert a 'Hash' to a hexadecimal-encoded 'StrictByteString'. -- -- ⚠️ Be prudent as to where you store it! -- -- @since 0.0.1.0 hashToHexByteString :: Hash -> StrictByteString hashToHexByteString = Base16.extractBase16 . Base16.encodeBase16' . hashToBinary -- | Convert a 'Hash' to a binary 'StrictByteString'. -- -- ⚠️ Be prudent as to where you store it! -- -- @since 0.0.1.0 hashToBinary :: Hash -> StrictByteString hashToBinary (Hash messageLength fPtr) = BS.fromForeignPtr0 (Foreign.castForeignPtr fPtr) (fromIntegral messageLength + fromIntegral cryptoSecretboxMACBytes) -- | Create an authenticated hash from a message, a secret key, -- and a one-time cryptographic nonce that must never be re-used with the same -- secret key to encrypt another message. -- -- @since 0.0.1.0 encrypt :: StrictByteString -- ^ Message to encrypt. -> SecretKey -- ^ Secret key generated with 'newSecretKey'. -> IO (Nonce, Hash) encrypt message (SecretKey secretKeyForeignPtr) = BS.unsafeUseAsCStringLen message $ \(cString, cStringLen) -> do (Nonce nonceForeignPtr) <- newNonce hashForeignPtr <- Foreign.mallocForeignPtrBytes (cStringLen + fromIntegral cryptoSecretboxMACBytes) Foreign.withForeignPtr hashForeignPtr $ \hashPtr -> Foreign.withForeignPtr secretKeyForeignPtr $ \secretKeyPtr -> Foreign.withForeignPtr nonceForeignPtr $ \noncePtr -> do void $ cryptoSecretboxEasy hashPtr (Foreign.castPtr @CChar @CUChar cString) (fromIntegral @Int @CULLong cStringLen) noncePtr secretKeyPtr let hash = Hash (fromIntegral @Int @CULLong cStringLen) hashForeignPtr pure (Nonce nonceForeignPtr, hash) -- | Decrypt a hashed and authenticated message with the shared secret key and the one-time cryptographic nonce. -- -- @since 0.0.1.0 decrypt :: Hash -- ^ Encrypted message you want to decrypt. -> SecretKey -- ^ Secret key used for encrypting the original message. -> Nonce -- ^ Nonce used for encrypting the original message. -> Maybe StrictByteString decrypt Hash{messageLength, hashForeignPtr} (SecretKey secretKeyForeignPtr) (Nonce nonceForeignPtr) = unsafeDupablePerformIO $ do messagePtr <- Foreign.mallocBytes (fromIntegral @CULLong @Int messageLength) Foreign.withForeignPtr hashForeignPtr $ \hashPtr -> Foreign.withForeignPtr secretKeyForeignPtr $ \secretKeyPtr -> Foreign.withForeignPtr nonceForeignPtr $ \noncePtr -> do result <- cryptoSecretboxOpenEasy messagePtr hashPtr (messageLength + fromIntegral cryptoSecretboxMACBytes) noncePtr secretKeyPtr case result of (-1) -> pure Nothing _ -> do bsPtr <- Foreign.mallocBytes (fromIntegral messageLength) memcpy bsPtr (Foreign.castPtr messagePtr) (fromIntegral messageLength) Just <$> BS.unsafePackMallocCStringLen (Foreign.castPtr @CChar bsPtr, fromIntegral messageLength)