packages feed

hOpenPGP-3.3: Codec/Encryption/OpenPGP/SEIPDv1.hs

-- SEIPDv1.hs: OpenPGP (RFC9580) legacy MDC/SEIPDv1
-- Copyright © 2026  Clint Adams
-- This software is released under the terms of the Expat license.
-- (See the LICENSE file).
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}

module Codec.Encryption.OpenPGP.SEIPDv1
    ( MDCFailure (..)
    , mdcTrailerForSEIPDv1
    , renderMDCFailure
    , seipdv1NonceFromIV
    , validateSEIPD1MDC
    , calculateMDC
    ) where

import Control.Error.Util (note)
import Control.Monad (when)
import qualified Crypto.Hash as CH
import qualified Crypto.Hash.Algorithms as CHA
import qualified Data.ByteArray as BA
import qualified Data.ByteString as B
import qualified Data.ByteString.Lazy as BL

import Codec.Encryption.OpenPGP.Types

{- | Compute the MDC trailer appended to SEIPDv1 plaintext before encryption.
The trailer is: @0xd3 0x14 SHA1(nonce || plaintext || 0xd3 0x14)@.
-}
mdcTrailerForSEIPDv1 :: IV -> B.ByteString -> B.ByteString
mdcTrailerForSEIPDv1 iv plaintext = mdcHeader <> digest
  where
    mdcHeader = B.pack [0xd3, 0x14]
    nonce = seipdv1NonceFromIV iv
    digest =
        BA.convert
            (CH.hash (nonce <> plaintext <> mdcHeader) :: CH.Digest CHA.SHA1)

-- | The SEIPDv1 nonce: the IV bytes followed by its last two bytes (resync prefix).
seipdv1NonceFromIV :: IV -> B.ByteString
seipdv1NonceFromIV (IV ivBytes) = ivBytes <> B.drop (B.length ivBytes - 2) ivBytes

calculateMDC
    :: B.ByteString -> B.ByteString -> Maybe BL.ByteString
calculateMDC nonce garbage
    | B.length garbage < 23 = Nothing
    | otherwise =
        let digest =
                CH.hash
                    ( nonce
                        <> B.take (B.length garbage - 22) garbage
                        <> B.pack [211, 20]
                    )
                    :: CH.Digest CHA.SHA1
         in Just (BL.fromStrict (BA.convert digest :: B.ByteString))

data MDCFailure
    = MDCTrailerMissing
    | MDCTrailerCorrupted
    | MDCDigestMismatch
    deriving (Eq, Show)

renderMDCFailure :: MDCFailure -> String
renderMDCFailure MDCTrailerMissing = "MDC trailer missing"
renderMDCFailure MDCTrailerCorrupted = "MDC trailer corrupted"
renderMDCFailure MDCDigestMismatch = "MDC digest mismatch"

{- | Verify the MDC trailer of a decrypted SEIPDv1 payload.
Takes the CFB nonce (blockSize+2 prefix bytes retained from decryption)
and the full decrypted bytes (payload + MDC packet), and returns the
payload without the MDC trailer on success.
-}
validateSEIPD1MDC
    :: B.ByteString -> B.ByteString -> Either MDCFailure B.ByteString
validateSEIPD1MDC nonce decrypted = do
    when (B.length decrypted < 22) $
        Left MDCTrailerMissing
    let (payload, trailer) = B.splitAt (B.length decrypted - 22) decrypted
    when (B.take 2 trailer /= B.pack [211, 20]) $
        Left MDCTrailerCorrupted
    expectedMdc <-
        note MDCTrailerMissing (calculateMDC nonce decrypted)
    let actualMdc = BL.fromStrict (B.drop 2 trailer)
    when (expectedMdc /= actualMdc) $
        Left MDCDigestMismatch
    Right payload