packages feed

hOpenPGP-3.4: Codec/Encryption/OpenPGP/KeyGeneration.hs

-- KeyGeneration.hs: OpenPGP (RFC9580) key generation
-- Copyright © 2026  Clint Adams
-- This software is released under the terms of the Expat license.
-- (See the LICENSE file).
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

module Codec.Encryption.OpenPGP.KeyGeneration
    ( KeyGenSpec (..)
    , generateSecretKey
    ) where

import Control.Monad (unless)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except (ExceptT (..), throwE)
import qualified Crypto.Error as CE
import Crypto.Number.Serialize (os2ip)
import qualified Crypto.PubKey.Curve25519 as C25519
import qualified Crypto.PubKey.Curve448 as C448
import qualified Crypto.PubKey.Ed25519 as Ed25519
import qualified Crypto.PubKey.Ed448 as Ed448
import qualified Crypto.PubKey.RSA as RSA
import Crypto.Random.Types (MonadRandom, getRandomBytes)
import qualified Data.ByteArray as BA
import qualified Data.ByteString as B

import Codec.Encryption.OpenPGP.Types

class RSAKeyVersion (v :: KeyVersion) where
    rsaKeyVersion :: KeyVersion

instance RSAKeyVersion 'V4 where
    rsaKeyVersion = V4

instance RSAKeyVersion 'V6 where
    rsaKeyVersion = V6

data KeyGenSpec (v :: KeyVersion) where
    KeyGenRSA
        :: RSAKeyVersion v => ThirtyTwoBitTimeStamp -> Int -> KeyGenSpec v
    KeyGenEd25519 :: ThirtyTwoBitTimeStamp -> KeyGenSpec 'V6
    KeyGenEd448 :: ThirtyTwoBitTimeStamp -> KeyGenSpec 'V6
    KeyGenX25519 :: ThirtyTwoBitTimeStamp -> KeyGenSpec 'V6
    KeyGenX448 :: ThirtyTwoBitTimeStamp -> KeyGenSpec 'V6

generateSecretKey
    :: forall v m
     . MonadRandom m
    => KeyGenSpec v
    -> ExceptT String m (SomePKPayload, SKey)
generateSecretKey spec = case spec of
    KeyGenRSA ts keySizeBits -> rsaGenerate (rsaKeyVersion @v) ts keySizeBits
    KeyGenEd25519 ts -> ed25519Generate ts
    KeyGenEd448 ts -> ed448Generate ts
    KeyGenX25519 ts -> x25519Generate ts
    KeyGenX448 ts -> x448Generate ts
  where
    rsaGenerate kv ts keySizeBits = do
        unless (keySizeBits `mod` 8 == 0) $
            throwE "RSA key size must be a multiple of 8"
        let keySizeBytes = keySizeBits `div` 8
        (publicKey, privateKey) <- lift $ RSA.generate keySizeBytes 65537
        let pkey = RSAPubKey (RSA_PublicKey publicKey)
            skey = RSAPrivateKey (RSA_PrivateKey privateKey)
            pkp = case kv of
                DeprecatedV3 -> PKPayload DeprecatedV3 ts 0 RSA pkey
                V4 -> PKPayload V4 ts 0 RSA pkey
                V6 -> PKPayload V6 ts 0 RSA pkey
        pure (pkp, skey)
    ed25519Generate ts = do
        seed <- lift $ getRandomBytes 32
        secretKey <-
            either
                (throwE . ("Ed25519 key generation failed: " ++) . show)
                pure
                (CE.eitherCryptoError (Ed25519.secretKey seed))
        let pubBytes = BA.convert (Ed25519.toPublic secretKey) :: B.ByteString
            pkey =
                EdDSAPubKey
                    EdSigningCurve25519
                    (NativeEPoint (EPoint (os2ip pubBytes)))
            skey = Ed25519PrivateKey seed
            pkp = PKPayload V6 ts 0 Ed25519 pkey
        pure (pkp, skey)
    ed448Generate ts = do
        seed <- lift $ getRandomBytes 57
        secretKey <-
            either
                (throwE . ("Ed448 key generation failed: " ++) . show)
                pure
                (CE.eitherCryptoError (Ed448.secretKey seed))
        let pubBytes = BA.convert (Ed448.toPublic secretKey) :: B.ByteString
            pkey =
                EdDSAPubKey
                    EdSigningCurve448
                    (NativeEPoint (EPoint (os2ip pubBytes)))
            skey = Ed448PrivateKey seed
            pkp = PKPayload V6 ts 0 Ed448 pkey
        pure (pkp, skey)
    x25519Generate ts = do
        secretRaw <- lift $ getRandomBytes 32
        secretKey <-
            either
                (throwE . ("X25519 key generation failed: " ++) . show)
                pure
                (CE.eitherCryptoError (C25519.secretKey secretRaw))
        let pubRaw = BA.convert (C25519.toPublic secretKey) :: B.ByteString
            pkey =
                EdDSAPubKey
                    EdSigningCurve25519
                    (NativeEPoint (EPoint (os2ip pubRaw)))
            skey = X25519PrivateKey secretRaw
            pkp = PKPayload V6 ts 0 X25519 pkey
        pure (pkp, skey)
    x448Generate ts = do
        secretRaw <- lift $ getRandomBytes 56
        secretKey <-
            either
                (throwE . ("X448 key generation failed: " ++) . show)
                pure
                (CE.eitherCryptoError (C448.secretKey secretRaw))
        let pubRaw = BA.convert (C448.toPublic secretKey) :: B.ByteString
            pkey =
                EdDSAPubKey
                    EdSigningCurve448
                    (NativeEPoint (EPoint (os2ip pubRaw)))
            skey = X448PrivateKey secretRaw
            pkp = PKPayload V6 ts 0 X448 pkey
        pure (pkp, skey)