packages feed

age-0.0.1.0: src/Crypto/Age/Recipient.hs

-- | age file recipients.
module Crypto.Age.Recipient
  ( -- * Recipients
    Recipients (..)
    -- * @scrypt@
  , ScryptRecipient (..)
    -- * X25519
  , X25519Recipient (..)
  , bytesToX25519Recipient
  , x25519RecipientToBytes
  , encodeX25519Recipient
  , DecodeX25519RecipientError (..)
  , decodeX25519Recipient
  ) where

import qualified Codec.Binary.Bech32 as Bech32
import Control.Monad ( when )
import Crypto.Age.Scrypt ( Passphrase, Salt, WorkFactor )
import qualified Crypto.Error as Crypto
import qualified Crypto.PubKey.Curve25519 as Curve25519
import Data.Bifunctor ( first )
import qualified Data.ByteArray as BA
import Data.ByteString ( ByteString )
import Data.List.NonEmpty ( NonEmpty )
import Data.Text ( Text )
import Prelude

-- | [@scrypt@ recipient](https://github.com/C2SP/C2SP/blob/91935d7157cb3860351ffebbad1e6f6153e8efc8/age.md#the-scrypt-recipient-type).
data ScryptRecipient = ScryptRecipient
  { -- | Passphrase.
    srPassphrase :: !Passphrase
  , -- | Salt.
    srSalt :: !Salt
  , -- | @scrypt@ work factor.
    srWorkFactor :: !WorkFactor
  } deriving stock (Show, Eq)

-- | [X25519 recipient](https://github.com/C2SP/C2SP/blob/91935d7157cb3860351ffebbad1e6f6153e8efc8/age.md#the-x25519-recipient-type).
newtype X25519Recipient = X25519Recipient
  { -- | Recipient's Curve25519 public key.
    unX25519Recipient :: Curve25519.PublicKey
  } deriving stock (Show, Eq)

-- | Construct an 'X25519Recipient' from the raw bytes of a Curve25519 public
-- key.
--
-- If the provided byte string does not have a length of 32 (256 bits),
-- 'Nothing' is returned.
bytesToX25519Recipient :: ByteString -> Maybe X25519Recipient
bytesToX25519Recipient =
  (X25519Recipient <$>)
    . Crypto.maybeCryptoError
    . Curve25519.publicKey

-- | Get the raw Curve25519 public key bytes associated with an
-- 'X25519Recipient'.
x25519RecipientToBytes :: X25519Recipient -> ByteString
x25519RecipientToBytes = BA.convert . unX25519Recipient

x25519RecipientBech32Hrp :: Bech32.HumanReadablePart
x25519RecipientBech32Hrp =
  case Bech32.humanReadablePartFromText "age" of
    Left _ -> error "x25519RecipientBech32Hrp: impossible: \"age\" is an invalid HRP"
    Right hrp -> hrp

-- | Encode an 'X25519Recipient' as
-- [Bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki).
encodeX25519Recipient :: X25519Recipient -> Either Bech32.EncodingError Text
encodeX25519Recipient r =
  Bech32.encode
    x25519RecipientBech32Hrp
    (Bech32.dataPartFromBytes $ x25519RecipientToBytes r)

-- | Error decoding an 'X25519Recipient' from Bech32.
data DecodeX25519RecipientError
  = -- | Bech32 decoding error.
    DecodeX25519RecipientBech32DecodingError !Bech32.DecodingError
  | -- | Invalid Bech32 human-readable part.
    DecodeX25519RecipientInvalidHumanReadablePartError
      -- | Expected Bech32 human-readable part.
      !Bech32.HumanReadablePart
      -- | Actual Bech32 human-readable part.
      !Bech32.HumanReadablePart
  | -- | Invalid Bech32 data part.
    DecodeX25519RecipientInvalidDataPartError
  | -- | Invalid Curve25519 secret key size.
    DecodeX25519RecipientInvalidSecretKeySizeError
  deriving stock (Show, Eq)

-- | Decode an 'X25519Recipient' from
-- [Bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki).
decodeX25519Recipient :: Text -> Either DecodeX25519RecipientError X25519Recipient
decodeX25519Recipient t = do
  (hrp, dp) <- first DecodeX25519RecipientBech32DecodingError (Bech32.decode t)
  when
    (x25519RecipientBech32Hrp /= hrp)
    (Left $ DecodeX25519RecipientInvalidHumanReadablePartError x25519RecipientBech32Hrp hrp)
  dpBs <-
    case Bech32.dataPartToBytes dp of
      Nothing -> Left DecodeX25519RecipientInvalidDataPartError
      Just bs -> Right bs
  case bytesToX25519Recipient dpBs of
    Nothing -> Left DecodeX25519RecipientInvalidSecretKeySizeError
    Just r -> Right r

-- | Collection of age file recipients.
data Recipients
  = -- | @scrypt@ recipient.
    --
    -- As noted in the
    -- [age specification](https://github.com/C2SP/C2SP/blob/34a9210873230d2acaa4a4c9c5d4d1119b2ee77d/age.md#scrypt-recipient-stanza),
    -- no other stanzas can be specified in the header when there is an
    -- @scrypt@ stanza. This is to uphold an expectation of authentication that
    -- is implicit in password-based encryption.
    --
    -- As a result, only one @scrypt@ recipient can be specified.
    RecipientsScrypt !ScryptRecipient
  | -- | X25519 recipients.
    RecipientsX25519 !(NonEmpty X25519Recipient)
  deriving stock (Show, Eq)