packages feed

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

module Crypto.Age.Identity
  ( -- * Identity
    Identity (..)

    -- ** @scrypt@
  , ScryptIdentity (..)

    -- ** X25519
  , X25519Identity (..)
  , bytesToX25519Identity
  , x25519IdentityToBytes
  , toX25519Recipient
  , generateX25519Identity
  , encodeX25519Identity
  , DecodeX25519IdentityError (..)
  , decodeX25519Identity
  ) where

import qualified Codec.Binary.Bech32 as Bech32
import Control.Monad ( when )
import Crypto.Age.Recipient ( X25519Recipient (..) )
import Crypto.Age.Scrypt ( Passphrase, WorkFactor )
import qualified Crypto.Error as Crypto
import qualified Crypto.PubKey.Curve25519 as Curve25519
import Data.Bifunctor ( first )
import Data.ByteArray ( ScrubbedBytes )
import qualified Data.ByteArray as BA
import Data.Text ( Text )
import qualified Data.Text as T
import Prelude

-- | [@scrypt@ identity](https://github.com/C2SP/C2SP/blob/91935d7157cb3860351ffebbad1e6f6153e8efc8/age.md#the-scrypt-recipient-type).
data ScryptIdentity = ScryptIdentity
  { -- | Passphrase.
    siPassphrase :: !Passphrase
  , -- | Maximum work factor permitted for this identity.
    siMaxWorkFactor :: !WorkFactor
  } deriving stock (Show, Eq)

-- | [X25519 identity](https://github.com/C2SP/C2SP/blob/91935d7157cb3860351ffebbad1e6f6153e8efc8/age.md#the-x25519-recipient-type).
--
-- Note that this type's 'Eq' instance performs a constant-time equality
-- check.
newtype X25519Identity = X25519Identity
  { unX25519Identity :: Curve25519.SecretKey }
  deriving newtype (Show, Eq)

-- | Construct an 'X25519Identity' from the raw bytes of a Curve25519 secret
-- key.
--
-- If the provided byte string does not have a length of 32 (256 bits),
-- 'Nothing' is returned.
bytesToX25519Identity :: ScrubbedBytes -> Maybe X25519Identity
bytesToX25519Identity =
  (X25519Identity <$>)
    . Crypto.maybeCryptoError
    . Curve25519.secretKey

-- | Get the raw Curve25519 secret key bytes associated with an
-- 'X25519Identity'.
x25519IdentityToBytes :: X25519Identity -> ScrubbedBytes
x25519IdentityToBytes = BA.convert . unX25519Identity

-- | Get the 'X25519Recipient' which corresponds to the given 'X25519Identity'.
toX25519Recipient :: X25519Identity -> X25519Recipient
toX25519Recipient (X25519Identity sk) =
  X25519Recipient (Curve25519.toPublic sk)

-- | Randomly generate a 'X25519Identity'.
generateX25519Identity :: IO X25519Identity
generateX25519Identity = X25519Identity <$> Curve25519.generateSecretKey

x25519IdentityBech32Hrp :: Bech32.HumanReadablePart
x25519IdentityBech32Hrp =
  case Bech32.humanReadablePartFromText "AGE-SECRET-KEY-" of
    Left _ -> error "x25519IdentityBech32Hrp: impossible: \"AGE-SECRET-KEY-\" is an invalid HRP"
    Right hrp -> hrp

-- | Encode an 'X25519Identity' as
-- [Bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki).
encodeX25519Identity :: X25519Identity -> Either Bech32.EncodingError Text
encodeX25519Identity i =
  T.toUpper
    <$> Bech32.encode
      x25519IdentityBech32Hrp
      (Bech32.dataPartFromBytes . BA.convert $ x25519IdentityToBytes i)

-- | Error decoding an 'X25519Identity' from Bech32.
data DecodeX25519IdentityError
  = -- | Bech32 decoding error.
    DecodeX25519IdentityBech32DecodingError !Bech32.DecodingError
  | -- | Invalid Bech32 human-readable part.
    DecodeX25519IdentityInvalidHumanReadablePartError
      -- | Expected Bech32 human-readable part.
      !Bech32.HumanReadablePart
      -- | Actual Bech32 human-readable part.
      !Bech32.HumanReadablePart
  | -- | Invalid Bech32 data part.
    DecodeX25519IdentityInvalidDataPartError
  | -- | Invalid Curve25519 secret key size.
    DecodeX25519IdentityInvalidSecretKeySizeError
  deriving stock (Show, Eq)

-- | Decode an 'X25519Identity' from
-- [Bech32](https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki).
decodeX25519Identity :: Text -> Either DecodeX25519IdentityError X25519Identity
decodeX25519Identity t = do
  (hrp, dp) <- first DecodeX25519IdentityBech32DecodingError (Bech32.decode t)
  when
    (x25519IdentityBech32Hrp /= hrp)
    (Left $ DecodeX25519IdentityInvalidHumanReadablePartError x25519IdentityBech32Hrp hrp)
  dpBs <-
    case Bech32.dataPartToBytes dp of
      Nothing -> Left DecodeX25519IdentityInvalidDataPartError
      Just bs -> Right bs
  case bytesToX25519Identity (BA.convert dpBs) of
    Nothing -> Left DecodeX25519IdentityInvalidSecretKeySizeError
    Just i -> Right i

-- | age identity.
data Identity
  = -- | @scrypt@ identity.
    IdentityScrypt !ScryptIdentity
  | -- | X25519 identity.
    IdentityX25519 !X25519Identity
  deriving stock (Show, Eq)