sel-0.1.0.0: src/Sel/SecretKey/Stream.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RoleAnnotations #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- |
--
-- Module: Sel.SecretKey.Stream
-- Description: Encrypted Streams with XChaCha20-Poly1305
-- Copyright: (C) Hécate Moonlight 2024
-- License: BSD-3-Clause
-- Maintainer: The Haskell Cryptography Group
-- Portability: GHC only
module Sel.SecretKey.Stream
( -- ** Introduction
-- $introduction
-- ** Usage
-- $usage
-- ** Stream operations
-- *** Linked List operations
encryptList
, decryptList
-- *** Chunk operations
, Multipart
, encryptStream
, encryptChunk
, decryptStream
, decryptChunk
-- ** Secret Key
, SecretKey
, newSecretKey
, secretKeyFromHexByteString
, unsafeSecretKeyToHexByteString
-- ** Header
, Header
, headerToHexByteString
, headerFromHexByteString
-- ** Message Tags
, MessageTag (..)
-- ** Additional data (AD)
, AdditionalData (..)
, AdditionalDataHexDecodingError (..)
, additionalDataFromHexByteString
, additionalDataToBinary
, additionalDataToHexByteString
, additionalDataToHexText
-- ** Ciphertext
, Ciphertext
, ciphertextFromHexByteString
, ciphertextToBinary
, ciphertextToHexByteString
, ciphertextToHexText
-- ** Exceptions
, StreamInitEncryptionException
, StreamEncryptionException
, StreamDecryptionException
) where
import Control.Exception (Exception, throw)
import Control.Monad (forM, when)
import Control.Monad.IO.Class (MonadIO, liftIO)
import Data.Base16.Types (Base16)
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 BSI
import qualified Data.ByteString.Unsafe as BSU
import Data.Kind (Type)
import qualified Data.List as List
import Data.Text (Text)
import qualified Data.Text as Text
import qualified Data.Text.Builder.Linear as Builder
import Data.Text.Display (Display (..), OpaqueInstance (..), ShowInstance (..))
import Foreign (ForeignPtr, Ptr)
import qualified Foreign
import Foreign.C (CChar, CSize, CUChar, CULLong)
import Foreign.C.Error (throwErrno)
import LibSodium.Bindings.SecretStream
( CryptoSecretStreamXChaCha20Poly1305State
, cryptoSecretStreamXChaCha20Poly1305ABytes
, cryptoSecretStreamXChaCha20Poly1305HeaderBytes
, cryptoSecretStreamXChaCha20Poly1305InitPull
, cryptoSecretStreamXChaCha20Poly1305InitPush
, cryptoSecretStreamXChaCha20Poly1305KeyBytes
, cryptoSecretStreamXChaCha20Poly1305KeyGen
, cryptoSecretStreamXChaCha20Poly1305Pull
, cryptoSecretStreamXChaCha20Poly1305Push
, cryptoSecretStreamXChaCha20Poly1305StateBytes
, cryptoSecretStreamXChaCha20Poly1305TagFinal
, cryptoSecretStreamXChaCha20Poly1305TagMessage
, cryptoSecretStreamXChaCha20Poly1305TagPush
, cryptoSecretStreamXChaCha20Poly1305TagRekey
)
import LibSodium.Bindings.SecureMemory (finalizerSodiumFree, sodiumMalloc)
import System.IO.Unsafe (unsafeDupablePerformIO)
import Sel.Internal (allocateWith, foreignPtrEq, foreignPtrOrd)
import Sel.Internal.Sodium (binaryToHex)
-- $introduction
-- This high-level API encrypts a sequence of messages, or a single message split into an arbitrary number of chunks, using a secret key, with the following properties:
--
-- * Messages cannot be truncated, removed, reordered, duplicated or modified without this being detected by the decryption functions.
-- * The same sequence encrypted twice will produce different ciphertexts.
-- * An authentication tag is added to each encrypted message: stream corruption will be detected early, without having to read the stream until the end.
-- * Each message can include additional data (ex: timestamp, protocol version) in the computation of the authentication tag.
-- * Messages can have different sizes.
-- * There are no practical limits to the total length of the stream, or to the total number of individual messages.
--
-- It uses the [XChaCha20-Poly1305 algorithm](https://en.wikipedia.org/wiki/ChaCha20-Poly1305).
-- $usage
--
-- >>> secretKey <- Stream.newSecretKey
-- >>> (header, ciphertexts) <- Stream.encryptStream secretKey $ \multipartState -> do -- we are in MonadIO
-- ... message1 <- getMessage -- This is your way to fetch a message from outside
-- ... encryptedChunk1 <- Stream.encryptChunk multipartState Stream.messag message1
-- ... message2 <- getMessage
-- ... encryptedChunk2 <- Stream.encryptChunk multipartState Stream.Final message2
-- ... pure [encryptedChunk1, encryptedChunk2]
-- >>> result <- Stream.decryptStream secretKey header $ \multipartState-> do
-- ... forM encryptedMessages $ \ciphertext -> do
-- ... decryptChunk multipartState ciphertext
-- | 'Multipart' is the cryptographic context for stream encryption.
--
-- @since 0.0.1.0
newtype Multipart s = Multipart (Ptr CryptoSecretStreamXChaCha20Poly1305State)
type role Multipart nominal
-- | Perform streaming encryption with a 'Multipart' cryptographic context.
--
-- Use 'Stream.encryptChunk' within the continuation.
--
-- The context is safely allocated first, then the continuation is run
-- and then it is deallocated after that.
--
-- @since 0.0.1.0
encryptStream
:: forall (a :: Type) (m :: Type -> Type)
. MonadIO m
=> SecretKey
-- ^ Generated with 'newSecretKey'.
-> (forall s. Multipart s -> m a)
-- ^ Continuation that gives you access to a 'Multipart' cryptographic context
-> m (Header, a)
encryptStream (SecretKey secretKeyForeignPtr) actions = allocateWith cryptoSecretStreamXChaCha20Poly1305StateBytes $ \statePtr -> do
headerPtr <- liftIO $ sodiumMalloc cryptoSecretStreamXChaCha20Poly1305HeaderBytes
headerForeignPtr <- liftIO $ Foreign.newForeignPtr finalizerSodiumFree headerPtr
when (headerPtr == Foreign.nullPtr) $ liftIO (throwErrno "sodium_malloc")
liftIO $ Foreign.withForeignPtr secretKeyForeignPtr $ \secretKeyPtr -> do
result <-
cryptoSecretStreamXChaCha20Poly1305InitPush
statePtr
headerPtr
secretKeyPtr
when (result /= 0) $ throw StreamInitEncryptionException
let part = Multipart statePtr
let header = Header headerForeignPtr
result <- actions part
pure (header, result)
-- | Add a message portion (/chunk/) to be encrypted.
--
-- Use it within 'encryptStream'.
--
-- This function can throw 'StreamEncryptionException' upon an error in the underlying implementation.
--
-- @since 0.0.1.0
encryptChunk
:: forall m s
. MonadIO m
=> Multipart s
-- ^ Cryptographic context
-> MessageTag
-- ^ Tag that will be associated with the message. See the documentation of 'MessageTag' to know which to choose when.
-> Maybe AdditionalData
-- ^ Additional data (AD) to be authenticated.
-> StrictByteString
-- ^ Message to encrypt.
-> m Ciphertext
encryptChunk (Multipart statePtr) messageTag mbAd message = liftIO $
BSU.unsafeUseAsCStringLen message $ \(messageCString, messageCStringLen) ->
BSU.unsafeUseAsCStringLen (maybe BS.empty additionalDataToBinary mbAd) $ \(adCString, adCStringLen) -> do
let messagePtr = Foreign.castPtr @CChar @CUChar messageCString
messageLen = fromIntegral @Int @CULLong messageCStringLen
adPtr = Foreign.castPtr @CChar @CUChar adCString
adLen = fromIntegral @Int @CULLong adCStringLen
ciphertextFPtr <- Foreign.mallocForeignPtrBytes (messageCStringLen + fromIntegral cryptoSecretStreamXChaCha20Poly1305ABytes)
Foreign.withForeignPtr ciphertextFPtr $ \ciphertextBuffer -> do
result <-
cryptoSecretStreamXChaCha20Poly1305Push
statePtr
ciphertextBuffer
Foreign.nullPtr -- default size of messageLen + 'cryptoSecretStreamXChaCha20Poly1305ABytes'
messagePtr
messageLen
adPtr
adLen
(messageTagToConstant messageTag)
when (result /= 0) $ throw StreamEncryptionException
pure $ Ciphertext (fromIntegral messageCStringLen) ciphertextFPtr
-- | Perform streaming encryption of a finite list.
--
-- This function can throw 'StreamEncryptionException' upon an error in the underlying implementation.
--
-- @since 0.0.1.0
encryptList :: forall m. MonadIO m => SecretKey -> [(Maybe AdditionalData, StrictByteString)] -> m (Header, [Ciphertext])
encryptList secretKey messages = encryptStream secretKey $ \multipart -> go multipart messages []
where
go :: Multipart s -> [(Maybe AdditionalData, StrictByteString)] -> [Ciphertext] -> m [Ciphertext]
go multipart [(mbLastAd, lastMsg)] acc = do
encryptedChunk <- encryptChunk multipart Final mbLastAd lastMsg
pure $ List.reverse $ encryptedChunk : acc
go multipart ((mbAd, msg) : rest) acc = do
encryptedChunk <- encryptChunk multipart Message mbAd msg
go multipart rest (encryptedChunk : acc)
go _ [] acc = pure acc
-- | Perform streaming decryption with a 'Multipart' cryptographic context.
--
-- Use 'Stream.decryptChunk' within the continuation.
--
-- The context is safely allocated first, then the continuation is run
-- and then it is deallocated after that.
--
-- @since 0.0.1.0
decryptStream
:: forall (a :: Type) (m :: Type -> Type)
. MonadIO m
=> SecretKey
-> Header
-- ^ Header used by the encrypting party. See its documentation
-> (forall s. Multipart s -> m a)
-- ^ Continuation that gives you access to a 'Multipart' cryptographic context
-> m (Maybe a)
decryptStream (SecretKey secretKeyForeignPtr) (Header headerForeignPtr) actions = allocateWith cryptoSecretStreamXChaCha20Poly1305StateBytes $ \statePtr -> do
result <- liftIO $ Foreign.withForeignPtr secretKeyForeignPtr $ \secretKeyPtr -> do
Foreign.withForeignPtr headerForeignPtr $ \headerPtr -> do
cryptoSecretStreamXChaCha20Poly1305InitPull
statePtr
headerPtr
secretKeyPtr
if result /= 0
then pure Nothing
else do
let part = Multipart statePtr
Just <$> actions part
-- | Add a message portion (/chunk/) to be decrypted.
--
-- Use this function within 'decryptStream'.
--
-- This function can throw 'StreamDecryptionException' if the chunk is invalid, incomplete, or corrupted.
--
-- @since 0.0.1.0
decryptChunk
:: forall m s
. MonadIO m
=> Multipart s
-- ^ Cryptographic context
-> Maybe AdditionalData
-- ^ Additional data (AD) to be authenticated.
-> Ciphertext
-- ^ Encrypted message portion to decrypt
-> m StrictByteString
-- ^ Decrypted message portion
decryptChunk (Multipart statePtr) mbAd Ciphertext{messageLength, ciphertextForeignPtr} = do
clearTextForeignPtr <- liftIO $ Foreign.mallocForeignPtrBytes (fromIntegral messageLength)
let ciphertextLen = messageLength + fromIntegral cryptoSecretStreamXChaCha20Poly1305ABytes
liftIO $ Foreign.withForeignPtr ciphertextForeignPtr $ \ciphertextBuffer -> do
BSU.unsafeUseAsCStringLen (maybe BS.empty additionalDataToBinary mbAd) $ \(adCString, adCStringLen) -> do
let adPtr = Foreign.castPtr @CChar @CUChar adCString
adLen = fromIntegral @Int @CULLong adCStringLen
liftIO $ Foreign.withForeignPtr clearTextForeignPtr $ \clearTextBuffer -> do
tagBuffer <- sodiumMalloc 1
result <-
cryptoSecretStreamXChaCha20Poly1305Pull
statePtr
clearTextBuffer
Foreign.nullPtr
tagBuffer
ciphertextBuffer
ciphertextLen
adPtr
adLen
when (result /= 0) $ throw StreamDecryptionException
bsPtr <- Foreign.mallocBytes (fromIntegral messageLength)
Foreign.copyBytes bsPtr (Foreign.castPtr clearTextBuffer) (fromIntegral messageLength)
BSU.unsafePackMallocCStringLen (bsPtr, fromIntegral messageLength)
-- | Perform streaming decryption of a finite Linked List.
--
-- This function can throw 'StreamDecryptionException' if the chunk is invalid, incomplete, or corrupted.
--
-- @since 0.0.1.0
decryptList :: forall m. MonadIO m => SecretKey -> Header -> [(Maybe AdditionalData, Ciphertext)] -> m (Maybe [StrictByteString])
decryptList secretKey header encryptedMessages =
decryptStream secretKey header $ \multipart -> do
forM encryptedMessages $ \(mbAd, ciphertext) -> do
decryptChunk multipart mbAd ciphertext
-- | A secret key of size 'cryptoSecretStreamXChaCha20Poly1305KeyBytes'.
--
-- @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) =
foreignPtrEq hk1 hk2 cryptoSecretStreamXChaCha20Poly1305KeyBytes
-- | @since 0.0.1.0
instance Ord SecretKey where
compare (SecretKey hk1) (SecretKey hk2) =
foreignPtrOrd hk1 hk2 cryptoSecretStreamXChaCha20Poly1305KeyBytes
-- | > 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 cryptoSecretStreamXChaCha20Poly1305KeyGen
-- | 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
-- 'cryptoSecretStreamXChaCha20Poly1305KeyBytes'.
--
-- @since 0.0.1.0
secretKeyFromHexByteString :: Base16 StrictByteString -> Either Text SecretKey
secretKeyFromHexByteString hexSecretKey = unsafeDupablePerformIO $
case Base16.decodeBase16Untyped (Base16.extractBase16 hexSecretKey) of
Right bytestring ->
if BS.length bytestring == fromIntegral cryptoSecretStreamXChaCha20Poly1305KeyBytes
then BSU.unsafeUseAsCStringLen bytestring $ \(outsideSecretKeyPtr, _) -> do
secretKey <- newSecretKeyWith $ \secretKeyPtr ->
Foreign.copyArray
(Foreign.castPtr @CUChar @CChar secretKeyPtr)
outsideSecretKeyPtr
(fromIntegral cryptoSecretStreamXChaCha20Poly1305KeyBytes)
pure $ Right secretKey
else pure $ Left $ Text.pack ("Secret Key is not of size " <> show cryptoSecretStreamXChaCha20Poly1305KeyBytes)
Left msg -> pure $ Left msg
-- | Convert a 'SecretKey' to a hexadecimal-encoded 'StrictByteString' in constant time.
--
-- ⚠️ Be prudent as to where you store it!
--
-- @since 0.0.1.0
unsafeSecretKeyToHexByteString :: SecretKey -> Base16 StrictByteString
unsafeSecretKeyToHexByteString (SecretKey secretKeyForeignPtr) =
Base16.assertBase16 $ binaryToHex secretKeyForeignPtr cryptoSecretStreamXChaCha20Poly1305KeyBytes
-- 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 :: (Ptr CUChar -> IO ()) -> IO SecretKey
newSecretKeyWith action = do
ptr <- sodiumMalloc cryptoSecretStreamXChaCha20Poly1305KeyBytes
when (ptr == Foreign.nullPtr) $ throwErrno "sodium_malloc"
fPtr <- Foreign.newForeignPtr finalizerSodiumFree ptr
action ptr
pure $ SecretKey fPtr
-- | An encrypted stream starts with a 'Header' of size 'cryptoSecretStreamXChaCha20Poly1305HeaderBytes'.
--
-- That header must be sent/stored before the sequence of encrypted messages, as it is required to decrypt the stream.
--
-- The header content doesn’t have to be secret and decryption with a different header will fail.
--
-- @since 0.0.1.0
newtype Header = Header (ForeignPtr CUChar)
-- | @since 0.0.1.0
instance Show Header where
show = BSI.unpackChars . Base16.extractBase16 . headerToHexByteString
-- | @since 0.0.1.0
instance Display Header where
displayBuilder = Builder.fromText . Base16.extractBase16 . headerToHexText
-- | @since 0.0.1.0
instance Eq Header where
(Header header1) == (Header header2) =
foreignPtrEq header1 header2 cryptoSecretStreamXChaCha20Poly1305HeaderBytes
-- | @since 0.0.1.0
instance Ord Header where
compare (Header header1) (Header header2) =
foreignPtrOrd header1 header2 cryptoSecretStreamXChaCha20Poly1305HeaderBytes
-- | Convert a 'Header' to a hexadecimal-encoded 'StrictByteString' in constant time.
--
-- @since 0.0.1.0
headerToHexByteString :: Header -> Base16 StrictByteString
headerToHexByteString (Header headerForeignPtr) =
Base16.assertBase16 $ binaryToHex headerForeignPtr cryptoSecretStreamXChaCha20Poly1305HeaderBytes
-- | Build a 'Header' from a base16-encoded 'StrictByteString'
--
-- @since 0.0.1.0
headerFromHexByteString :: Base16 StrictByteString -> Either Text Header
headerFromHexByteString hexHeader = unsafeDupablePerformIO $
case Base16.decodeBase16Untyped (Base16.extractBase16 hexHeader) of
Right bytestring ->
if BS.length bytestring == fromIntegral cryptoSecretStreamXChaCha20Poly1305HeaderBytes
then BSU.unsafeUseAsCStringLen bytestring $ \(outsideHeaderPtr, _) -> do
let headerLength = fromIntegral cryptoSecretStreamXChaCha20Poly1305HeaderBytes
headerForeignPtr <- Foreign.mallocForeignPtrBytes (fromIntegral cryptoSecretStreamXChaCha20Poly1305HeaderBytes)
Foreign.withForeignPtr headerForeignPtr $ \headerPtr -> do
Foreign.copyBytes headerPtr (Foreign.castPtr outsideHeaderPtr) headerLength
pure $ Right $ Header headerForeignPtr
else pure $ Left $ Text.pack ("Secret Key is not of size " <> show cryptoSecretStreamXChaCha20Poly1305HeaderBytes)
Left msg -> pure $ Left msg
-- | Convert a 'Header' to a hexadecimal-encoded 'Text'.
--
-- @since 0.0.1.0
headerToHexText :: Header -> Base16 Text
headerToHexText = Base16.encodeBase16 . Base16.extractBase16 . headerToHexByteString
-- | Each encrypted message is associated with a tag.
--
-- A typical encrypted stream simply attaches 'Message' as a tag to all messages,
-- except the last one which is tagged as 'Final'.
--
-- @since 0.0.1.0
data MessageTag
= -- | The most common tag, that doesn’t add any information about the nature of the message.
Message
| -- | Indicates that the message marks the end of the stream, and erases the secret key used to encrypt the previous sequence.
Final
| -- | Indicates that the message marks the end of a set of messages, but not the end of the stream.
Push
| -- | “Forget” the key used to encrypt this message and the previous ones, and derive a new secret key.
Rekey
-- | Convert a 'MessageTag' to its corresponding constant.
--
-- @since 0.0.1.0
messageTagToConstant :: MessageTag -> CUChar
messageTagToConstant = \case
Message -> fromIntegral cryptoSecretStreamXChaCha20Poly1305TagMessage
Final -> fromIntegral cryptoSecretStreamXChaCha20Poly1305TagFinal
Push -> fromIntegral cryptoSecretStreamXChaCha20Poly1305TagPush
Rekey -> fromIntegral cryptoSecretStreamXChaCha20Poly1305TagRekey
-- | Additional data (AD). Also known as \"additional authenticated data\"
-- (AAD).
--
-- This refers to non-confidential data which is authenticated along with the
-- message, but not encrypted (i.e. its integrity is protected, but it is not
-- made confidential).
--
-- A typical use case for additional data is to authenticate protocol-specific
-- metadata about a message, such as its length and encoding.
newtype AdditionalData = AdditionalData StrictByteString
deriving stock (Eq, Show)
deriving (Display) via (ShowInstance AdditionalData)
-- | Convert an 'AdditionalData' value to hexadecimal-encoded 'Text'.
additionalDataToHexText :: AdditionalData -> Base16 Text
additionalDataToHexText = Base16.encodeBase16 . additionalDataToBinary
-- | Convert an 'AdditionalData' value to a hexadecimal-encoded
-- 'StrictByteString'.
additionalDataToHexByteString :: AdditionalData -> Base16 StrictByteString
additionalDataToHexByteString = Base16.encodeBase16' . additionalDataToBinary
-- | Convert an 'AdditionalData' value to a raw binary 'StrictByteString'.
additionalDataToBinary :: AdditionalData -> StrictByteString
additionalDataToBinary (AdditionalData bs) = bs
-- | Error decoding 'AdditionalData' from hexadecimal-encoded bytes.
newtype AdditionalDataHexDecodingError = AdditionalDataHexDecodingError Text
deriving stock (Eq, Show)
-- | Construct an 'AdditionalData' value from a hexadecimal-encoded
-- 'StrictByteString' that you have obtained on your own, usually from the
-- network or disk.
additionalDataFromHexByteString
:: Base16 StrictByteString
-> Either AdditionalDataHexDecodingError AdditionalData
additionalDataFromHexByteString hexBs =
case Base16.decodeBase16Untyped (Base16.extractBase16 hexBs) of
Left err -> Left (AdditionalDataHexDecodingError err)
Right bs -> Right (AdditionalData bs)
-- | An encrypted message. It is guaranteed to be of size:
-- @original_message_length + 'cryptoSecretStreamXChaCha20Poly1305ABytes'@
--
-- @since 0.0.1.0
data Ciphertext = Ciphertext
{ messageLength :: CULLong
, ciphertextForeignPtr :: ForeignPtr CUChar
}
-- |
--
-- @since 0.0.1.0
instance Eq Ciphertext where
(Ciphertext ciphertextLength1 h1) == (Ciphertext ciphertextLength2 h2) =
let
textLength = ciphertextLength1 == ciphertextLength2
content =
foreignPtrEq
h1
h2
(fromIntegral ciphertextLength1 + cryptoSecretStreamXChaCha20Poly1305ABytes)
in
textLength && content
-- | @since 0.0.1.0
instance Ord Ciphertext where
compare (Ciphertext ciphertextLength1 c1) (Ciphertext ciphertextLength2 c2) =
let
textLength = compare ciphertextLength1 ciphertextLength2
content =
foreignPtrOrd
c1
c2
(fromIntegral ciphertextLength1 + cryptoSecretStreamXChaCha20Poly1305ABytes)
in
textLength <> content
-- | @since 0.0.1.0
instance Display Ciphertext where
displayBuilder = Builder.fromText . Base16.extractBase16 . ciphertextToHexText
-- | @since 0.0.1.0
instance Show Ciphertext where
show = BSI.unpackChars . Base16.extractBase16 . ciphertextToHexByteString
-- | Create a 'Ciphertext' from a binary 'StrictByteString' that you have obtained on your own,
-- usually from the network or disk. It must be a valid ciphertext built from the concatenation
-- of the encrypted message and the authentication tag.
--
-- The input ciphertext must at least of length 'cryptoSecretStreamXChaCha20Poly1305ABytes'
--
-- @since 0.0.1.0
ciphertextFromHexByteString :: Base16 StrictByteString -> Either Text Ciphertext
ciphertextFromHexByteString hexCiphertext = unsafeDupablePerformIO $
case Base16.decodeBase16Untyped (Base16.extractBase16 hexCiphertext) of
Right bytestring ->
if BS.length bytestring >= fromIntegral cryptoSecretStreamXChaCha20Poly1305ABytes
then BSU.unsafeUseAsCStringLen bytestring $ \(outsideCiphertextPtr, outsideCiphertextLength) -> do
ciphertextFPtr <- BSI.mallocByteString @CChar outsideCiphertextLength -- The foreign pointer that will receive the ciphertext data.
Foreign.withForeignPtr ciphertextFPtr $ \ciphertextPtr ->
-- We copy bytes from 'outsideCiphertextPtr' to 'ciphertextPtr.
Foreign.copyArray ciphertextPtr outsideCiphertextPtr outsideCiphertextLength
pure $
Right $
Ciphertext
(fromIntegral @Int @CULLong outsideCiphertextLength - fromIntegral @CSize @CULLong cryptoSecretStreamXChaCha20Poly1305ABytes)
(Foreign.castForeignPtr @CChar @CUChar ciphertextFPtr)
else pure $ Left $ Text.pack "Ciphertext is too short"
Left msg -> pure $ Left msg
-- | Convert a 'Ciphertext' to a hexadecimal-encoded 'Text'.
--
-- ⚠️ Be prudent as to where you store it!
--
-- @since 0.0.1.0
ciphertextToHexText :: Ciphertext -> Base16 Text
ciphertextToHexText = Base16.encodeBase16 . ciphertextToBinary
-- | Convert a 'Ciphertext' to a hexadecimal-encoded 'StrictByteString' in constant time.
--
-- ⚠️ Be prudent as to where you store it!
--
-- @since 0.0.1.0
ciphertextToHexByteString :: Ciphertext -> Base16 StrictByteString
ciphertextToHexByteString (Ciphertext ciphertextLength fPtr) =
Base16.assertBase16 $ binaryToHex fPtr (cryptoSecretStreamXChaCha20Poly1305ABytes + fromIntegral ciphertextLength)
-- | Convert a 'Ciphertext' to a binary 'StrictByteString' in constant time.
--
-- ⚠️ Be prudent as to where you store it!
--
-- @since 0.0.1.0
ciphertextToBinary :: Ciphertext -> StrictByteString
ciphertextToBinary (Ciphertext ciphertextLength fPtr) =
BSI.fromForeignPtr0
(Foreign.castForeignPtr fPtr)
(fromIntegral ciphertextLength + fromIntegral cryptoSecretStreamXChaCha20Poly1305ABytes)
-- | @since 0.0.1.0
data StreamEncryptionException = StreamEncryptionException
deriving stock (Eq, Ord, Show)
deriving anyclass (Exception)
-- | @since 0.0.1.0
data StreamInitEncryptionException = StreamInitEncryptionException
deriving stock (Eq, Ord, Show)
deriving anyclass (Exception)
-- | @since 0.0.1.0
data StreamDecryptionException = StreamDecryptionException
deriving stock (Eq, Ord, Show)
deriving anyclass (Exception)