openpgp-asciiarmor-1.0/0000755000000000000000000000000007346545000013342 5ustar0000000000000000openpgp-asciiarmor-1.0/Codec/Encryption/OpenPGP/0000755000000000000000000000000007346545000017761 5ustar0000000000000000openpgp-asciiarmor-1.0/Codec/Encryption/OpenPGP/ASCIIArmor.hs0000644000000000000000000000124307346545000022146 0ustar0000000000000000-- ASCIIArmor.hs: OpenPGP (RFC9580) ASCII armor implementation -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file). module Codec.Encryption.OpenPGP.ASCIIArmor ( decode , decodeLazy , decodeWith , decodeLazyWith , encode , encodeLazy , encodeWith , encodeLazyWith , parseArmor , multipartMerge ) where import Codec.Encryption.OpenPGP.ASCIIArmor.Decode (decode, decodeLazy, decodeWith, decodeLazyWith, parseArmor) import Codec.Encryption.OpenPGP.ASCIIArmor.Encode (encode, encodeLazy, encodeWith, encodeLazyWith) import Codec.Encryption.OpenPGP.ASCIIArmor.Multipart (multipartMerge) openpgp-asciiarmor-1.0/Codec/Encryption/OpenPGP/ASCIIArmor/0000755000000000000000000000000007346545000021612 5ustar0000000000000000openpgp-asciiarmor-1.0/Codec/Encryption/OpenPGP/ASCIIArmor/Decode.hs0000644000000000000000000002624007346545000023335 0ustar0000000000000000{-# LANGUAGE OverloadedStrings #-} -- ASCIIArmor/Decode.hs: OpenPGP (RFC9580) ASCII armor implementation -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file). module Codec.Encryption.OpenPGP.ASCIIArmor.Decode ( parseArmor , decode , decodeLazy , decodeWith , decodeLazyWith ) where import Codec.Encryption.OpenPGP.ASCIIArmor.Types import Codec.Encryption.OpenPGP.ASCIIArmor.Utils import Control.Applicative (many, (<|>), optional) import qualified Data.Attoparsec.Combinator as AC import Data.Attoparsec.ByteString (Parser, many1, string, inClass, notInClass, satisfy, word8, ()) import qualified Data.Attoparsec.ByteString as AS import qualified Data.Attoparsec.ByteString.Lazy as AL import Data.Attoparsec.ByteString.Char8 (isDigit_w8, anyChar) import Data.Bits (shiftL) import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Base64 as Base64 import Data.Char (isAlphaNum, isSpace) import Data.Digest.CRC24 (crc24) import Data.Binary.Get (Get, runGetOrFail, getWord8) import Data.Functor (($>)) import Data.List (dropWhileEnd) import Data.String (IsString, fromString) import Data.Word (Word32) decode :: IsString e => B.ByteString -> Either e [Armor] decode = decodeWith defaultDecodeOptions decodeWith :: IsString e => DecodeOptions -> B.ByteString -> Either e [Armor] decodeWith options bs = go (AS.parse (parseArmors options) bs) where go (AS.Fail _ _ e) = Left (fromString e) go (AS.Partial cont) = go (cont B.empty) go (AS.Done _ r) = Right r decodeLazy :: IsString e => BL.ByteString -> Either e [Armor] decodeLazy = decodeLazyWith defaultDecodeOptions decodeLazyWith :: IsString e => DecodeOptions -> BL.ByteString -> Either e [Armor] decodeLazyWith options bs = go (AL.parse (parseArmors options) bs) where go (AL.Fail _ _ e) = Left (fromString e) go (AL.Done _ r) = Right r parseArmors :: DecodeOptions -> Parser [Armor] parseArmors options = many (parseArmorWith options) parseArmor :: Parser Armor parseArmor = parseArmorWith defaultDecodeOptions parseArmorWith :: DecodeOptions -> Parser Armor parseArmorWith options = prefixed (clearsigned options <|> armor options) "armor" clearsigned :: DecodeOptions -> Parser Armor clearsigned options = do _ <- string "-----BEGIN PGP SIGNED MESSAGE-----" "clearsign header" _ <- lineEnding "line ending" headers <- cleartextHeaders options "clearsign headers" _ <- blankishLine "blank line" cleartext <- dashEscapedCleartext sig <- armor options return $ ClearSigned headers cleartext sig armor :: DecodeOptions -> Parser Armor armor options = do atype <- beginLine "begin line" headers <- armorHeaders "headers" _ <- blankishLine "blank line" payload <- base64Data options "base64 data" _ <- endLine atype "end line" return $ Armor atype headers payload beginLine :: Parser ArmorType beginLine = do _ <- string "-----BEGIN PGP " "leading minus-hyphens" atype <- pubkey <|> privkey <|> parts <|> message <|> signature _ <- string "-----" "trailing minus-hyphens" _ <- many (satisfy (inClass " \t")) "whitespace" _ <- lineEnding "line ending" return atype where message = string "MESSAGE" $> ArmorMessage pubkey = string "PUBLIC KEY BLOCK" $> ArmorPublicKeyBlock privkey = string "PRIVATE KEY BLOCK" $> ArmorPrivateKeyBlock signature = string "SIGNATURE" $> ArmorSignature parts = string "MESSAGE, PART " *> (partsdef <|> partsindef) partsdef = do firstnum <- num _ <- word8 (fromIntegral . fromEnum $ '/') secondnum <- num return $ ArmorSplitMessage (BL.pack firstnum) (BL.pack secondnum) partsindef = ArmorSplitMessageIndefinite . BL.pack <$> num num = many1 (satisfy isDigit_w8) "number" lineEnding :: Parser B.ByteString lineEnding = string "\n" <|> string "\r\n" cleartextHeaders :: DecodeOptions -> Parser [(String, String)] cleartextHeaders options | shouldRequireHashOnlyHeaders options = many hashHeader | otherwise = armorHeaders where hashHeader = do hdr@(k, v) <- armorHeader if k == "Hash" && isConformantHashHeader v then return hdr else fail "Only well-formed Hash headers are allowed before cleartext payload" isConformantHashHeader :: String -> Bool isConformantHashHeader v = not (null tokens) && all isValidToken tokens where tokens = map trim (splitOnComma v) splitOnComma [] = [""] splitOnComma xs = let (a, rest) = break (== ',') xs in a : case rest of [] -> [] (_:ys) -> splitOnComma ys trim = dropWhileEnd isSpace . dropWhile isSpace isValidToken [] = False isValidToken t = all (\c -> isAlphaNum c || c == '-') t armorHeaders :: Parser [(String, String)] armorHeaders = many armorHeader armorHeader :: Parser (String, String) armorHeader = do key <- many1 (satisfy (inClass "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz")) _ <- string ": " val <- many1 (satisfy (notInClass "\n\r")) _ <- lineEnding return (w8sToString key, w8sToString val) where w8sToString = BC8.unpack . B.pack blankishLine :: Parser B.ByteString blankishLine = many (satisfy (inClass " \t")) *> lineEnding endLine :: ArmorType -> Parser B.ByteString endLine atype = do _ <- string $ "-----END PGP " `B.append` aType atype `B.append` "-----" lineEnding aType :: ArmorType -> B.ByteString aType ArmorMessage = BC8.pack "MESSAGE" aType ArmorPublicKeyBlock = BC8.pack "PUBLIC KEY BLOCK" aType ArmorPrivateKeyBlock = BC8.pack "PRIVATE KEY BLOCK" aType (ArmorSplitMessage x y) = BC8.pack "MESSAGE, PART " `B.append` l2s x `B.append` BC8.singleton '/' `B.append` l2s y aType (ArmorSplitMessageIndefinite x) = BC8.pack "MESSAGE, PART " `B.append` l2s x aType ArmorSignature = BC8.pack "SIGNATURE" l2s :: BL.ByteString -> B.ByteString l2s = B.concat . BL.toChunks base64Data :: DecodeOptions -> Parser ByteString base64Data options = do ls <- many1 base64Line let payload = B.concat ls case crc24Validation options of DecodeCRC24ValidationStrict -> do decodedChecksum <- checksumLineStrict validateChecksum payload (Just decodedChecksum) True return (BL.fromStrict payload) DecodeCRC24ValidationPermissive -> do decodedChecksum <- optional checksumLinePermissive validateChecksum payload decodedChecksum False return (BL.fromStrict payload) DecodeCRC24ValidationAuto -> fail "Internal error: unresolved CRC24 validation mode" where base64Line :: Parser B.ByteString base64Line = do startsChecksum <- isChecksumLineStart if startsChecksum then fail "checksum line" else do b64line <- B.pack <$> many1 (satisfy (inClass "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/= \t")) _ <- lineEnding let cleaned = stripInlineWhitespace b64line case Base64.decode cleaned of Left err -> fail err Right bs -> return bs isChecksumLineStart :: Parser Bool isChecksumLineStart = AC.lookAhead $ do _ <- many (satisfy (inClass " \t")) c <- satisfy (notInClass "\n\r") return (c == fromIntegral (fromEnum '=')) checksumLineStrict :: Parser B.ByteString checksumLineStrict = do _ <- word8 (fromIntegral . fromEnum $ '=') b64 <- B.pack <$> many1 (satisfy (inClass "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/ \t")) _ <- lineEnding case Base64.decode (stripInlineWhitespace b64) of Left err -> fail err Right bs -> return bs checksumLinePermissive :: Parser B.ByteString checksumLinePermissive = do _ <- word8 (fromIntegral . fromEnum $ '=') raw <- B.pack <$> many (satisfy (notInClass "\n\r")) _ <- lineEnding case Base64.decode (stripInlineWhitespace raw) of Left _ -> return B.empty Right bs -> return bs validateChecksum :: B.ByteString -> Maybe B.ByteString -> Bool -> Parser () validateChecksum payload mDecodedChecksum requireValidCrc = case mDecodedChecksum of Nothing -> if requireValidCrc then fail "Missing CRC24 checksum line" else return () Just decodedChecksum -> if B.length decodedChecksum /= 3 then if requireValidCrc then fail "Malformed CRC24 checksum line" else return () else case runGetOrFail d24 (BL.fromStrict decodedChecksum) of Left (_,_,err) -> if requireValidCrc then fail err else return () Right (_,_,theircksum) -> let ourcksum = crc24 payload in if theircksum == ourcksum || not requireValidCrc then return () else fail ("CRC24 mismatch: " ++ show (B.unpack decodedChecksum) ++ "/" ++ show theircksum ++ " vs. " ++ show ourcksum) stripInlineWhitespace :: B.ByteString -> B.ByteString stripInlineWhitespace = B.filter (\w -> w /= 0x20 && w /= 0x09) crc24Validation :: DecodeOptions -> DecodeCRC24Validation crc24Validation options = case decodeCRC24Validation options of DecodeCRC24ValidationAuto -> case decodeConformanceProfile options of Strict_4880_AA -> DecodeCRC24ValidationStrict Strict_9580_AA -> DecodeCRC24ValidationPermissive Postel_AA -> DecodeCRC24ValidationPermissive explicitMode -> explicitMode shouldRequireHashOnlyHeaders :: DecodeOptions -> Bool shouldRequireHashOnlyHeaders options = case decodeCleartextHeaderPolicy options of DecodeCleartextHeaderPolicyAuto -> decodeConformanceProfile options == Strict_9580_AA DecodeCleartextHeaderPolicyLegacy -> False DecodeCleartextHeaderPolicyHashOnly -> True d24 :: Get Word32 d24 = do a <- getWord8 b <- getWord8 c <- getWord8 return $ shiftL (fromIntegral a :: Word32) 16 + shiftL (fromIntegral b :: Word32) 8 + (fromIntegral c :: Word32) prefixed :: Parser a -> Parser a prefixed end = end <|> anyChar *> prefixed end dashEscapedCleartext :: Parser ByteString dashEscapedCleartext = BL.fromStrict . crlfUnlines <$> go [] where go acc = do isSignatureStart <- AC.lookAhead ((string "-----BEGIN PGP " >> return True) <|> return False) if isSignatureStart then return (reverse acc) else do rawLine <- B.pack <$> many (satisfy (notInClass "\n\r")) _ <- lineEnding go (undash rawLine : acc) undash :: B.ByteString -> B.ByteString undash line | BC8.pack "- " `B.isPrefixOf` line = B.drop 2 line | otherwise = line openpgp-asciiarmor-1.0/Codec/Encryption/OpenPGP/ASCIIArmor/Encode.hs0000644000000000000000000000744207346545000023352 0ustar0000000000000000-- ASCIIArmor/Encode.hs: OpenPGP (RFC9580) ASCII armor implementation -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file). module Codec.Encryption.OpenPGP.ASCIIArmor.Encode ( encode , encodeLazy , encodeWith , encodeLazyWith ) where import Codec.Encryption.OpenPGP.ASCIIArmor.Types import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy.Char8 as BLC8 import qualified Data.ByteString.Base64 as Base64 import Data.Digest.CRC24 (crc24Lazy) import Data.Binary.Put (runPut, putWord32be) encode :: [Armor] -> B.ByteString encode = B.concat . BL.toChunks . encodeLazy encodeLazy :: [Armor] -> ByteString encodeLazy = encodeLazyWith defaultEncodeOptions encodeWith :: EncodeOptions -> [Armor] -> B.ByteString encodeWith options = B.concat . BL.toChunks . encodeLazyWith options encodeLazyWith :: EncodeOptions -> [Armor] -> ByteString encodeLazyWith options = BL.concat . map (armor options) armor :: EncodeOptions -> Armor -> ByteString armor options (Armor atype ahs bs) = beginLine atype `BL.append` armorHeaders ahs `BL.append` blankLine `BL.append` armorData bs `BL.append` checksumBlock `BL.append` endLine atype where checksumBlock | shouldEmitCRC24 options = armorChecksum bs | otherwise = BL.empty armor options (ClearSigned chs ctxt csig) = BLC8.pack "-----BEGIN PGP SIGNED MESSAGE-----\n" `BL.append` armorHeaders chs `BL.append` blankLine `BL.append` dashEscape ctxt `BL.append` armor options csig blankLine :: ByteString blankLine = BLC8.singleton '\n' beginLine :: ArmorType -> ByteString beginLine atype = BLC8.pack "-----BEGIN PGP " `BL.append` aType atype `BL.append` BLC8.pack "-----\n" endLine :: ArmorType -> ByteString endLine atype = BLC8.pack "-----END PGP " `BL.append` aType atype `BL.append` BLC8.pack "-----\n" aType :: ArmorType -> ByteString aType ArmorMessage = BLC8.pack "MESSAGE" aType ArmorPublicKeyBlock = BLC8.pack "PUBLIC KEY BLOCK" aType ArmorPrivateKeyBlock = BLC8.pack "PRIVATE KEY BLOCK" aType (ArmorSplitMessage x y) = BLC8.pack $ "MESSAGE, PART " ++ show x ++ "/" ++ show y aType (ArmorSplitMessageIndefinite x) = BLC8.pack $ "MESSAGE, PART " ++ show x aType ArmorSignature = BLC8.pack "SIGNATURE" armorHeaders :: [(String, String)] -> ByteString armorHeaders = BLC8.unlines . map armorHeader where armorHeader :: (String, String) -> ByteString armorHeader (k, v) = BLC8.pack k `BL.append` BLC8.pack ": " `BL.append` BLC8.pack v armorData :: ByteString -> ByteString armorData = BLC8.unlines . wordWrap 64 . BL.fromChunks . return . Base64.encode . B.concat . BL.toChunks wordWrap :: Int -> ByteString -> [ByteString] wordWrap lw bs | BL.null bs = [] | lw < 1 || lw > 76 = wordWrap 76 bs | otherwise = BL.take (fromIntegral lw) bs : wordWrap lw (BL.drop (fromIntegral lw) bs) armorChecksum :: ByteString -> ByteString armorChecksum = BLC8.cons '=' . armorData . BL.tail . runPut . putWord32be . crc24Lazy shouldEmitCRC24 :: EncodeOptions -> Bool shouldEmitCRC24 options = case encodeCRC24Emission options of EncodeCRC24EmissionAlways -> True EncodeCRC24EmissionNever -> False EncodeCRC24EmissionAuto -> case encodeConformanceProfile options of Strict_4880_AA -> True Strict_9580_AA -> False Postel_AA -> False dashEscape :: ByteString -> ByteString dashEscape = BLC8.unlines . map escapeLine . BLC8.lines where escapeLine :: ByteString -> ByteString escapeLine l | BLC8.singleton '-' `BL.isPrefixOf` l = BLC8.pack "- " `BL.append` l | BLC8.pack "From " `BL.isPrefixOf` l = BLC8.pack "- " `BL.append` l | otherwise = l openpgp-asciiarmor-1.0/Codec/Encryption/OpenPGP/ASCIIArmor/Multipart.hs0000644000000000000000000000205207346545000024126 0ustar0000000000000000-- ASCIIArmor/Multipart.hs: OpenPGP (legacy RFC4880) ASCII armor implementation -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file). module Codec.Encryption.OpenPGP.ASCIIArmor.Multipart ( multipartMerge ) where import Codec.Encryption.OpenPGP.ASCIIArmor.Types import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy as BL multipartMerge :: [Armor] -> Armor multipartMerge as' = go as' (Armor ArmorMessage [] BL.empty) where go :: [Armor] -> Armor -> Armor go [] state = state go (Armor at hs bs:as) state = go as (go' at hs bs state) go _ _ = error "This shouldn't happen." go' :: ArmorType -> [(String,String)] -> ByteString -> Armor -> Armor go' (ArmorSplitMessage _ _) hs bs (Armor _ ohs obs) = Armor ArmorMessage (ohs ++ hs) (obs `BL.append` bs) go' (ArmorSplitMessageIndefinite _) hs bs (Armor _ ohs obs) = Armor ArmorMessage (ohs ++ hs) (obs `BL.append` bs) go' _ _ _ state = state openpgp-asciiarmor-1.0/Codec/Encryption/OpenPGP/ASCIIArmor/Types.hs0000644000000000000000000000476007346545000023261 0ustar0000000000000000-- ASCIIArmor/Decode.hs: OpenPGP (RFC9580) ASCII armor implementation -- Copyright © 2012-2026 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file). module Codec.Encryption.OpenPGP.ASCIIArmor.Types ( Armor(..) , ArmorType(..) , ConformanceProfile(..) , DecodeOptions(..) , DecodeCRC24Validation(..) , DecodeCleartextHeaderPolicy(..) , defaultDecodeOptions , EncodeOptions(..) , EncodeCRC24Emission(..) , defaultEncodeOptions ) where import Data.ByteString.Lazy (ByteString) data Armor = Armor ArmorType [(String, String)] ByteString | ClearSigned [(String, String)] ByteString Armor deriving (Show, Eq) data ArmorType = ArmorMessage | ArmorPublicKeyBlock | ArmorPrivateKeyBlock -- RFC4880 legacy | ArmorSplitMessage ByteString ByteString -- RFC4880 legacy | ArmorSplitMessageIndefinite ByteString | ArmorSignature deriving (Show, Eq) data ConformanceProfile = Strict_4880_AA | Strict_9580_AA | Postel_AA deriving (Show, Eq) data DecodeCRC24Validation = DecodeCRC24ValidationAuto | DecodeCRC24ValidationStrict | DecodeCRC24ValidationPermissive deriving (Show, Eq) data DecodeCleartextHeaderPolicy = DecodeCleartextHeaderPolicyAuto | DecodeCleartextHeaderPolicyLegacy | DecodeCleartextHeaderPolicyHashOnly deriving (Show, Eq) data DecodeOptions = DecodeOptions { decodeConformanceProfile :: ConformanceProfile , decodeCRC24Validation :: DecodeCRC24Validation , decodeCleartextHeaderPolicy :: DecodeCleartextHeaderPolicy } deriving (Show, Eq) defaultDecodeOptions :: DecodeOptions defaultDecodeOptions = DecodeOptions { decodeConformanceProfile = Postel_AA , decodeCRC24Validation = DecodeCRC24ValidationAuto , decodeCleartextHeaderPolicy = DecodeCleartextHeaderPolicyAuto } data EncodeCRC24Emission = EncodeCRC24EmissionAuto | EncodeCRC24EmissionAlways | EncodeCRC24EmissionNever deriving (Show, Eq) data EncodeOptions = EncodeOptions { encodeConformanceProfile :: ConformanceProfile , encodeCRC24Emission :: EncodeCRC24Emission } deriving (Show, Eq) defaultEncodeOptions :: EncodeOptions defaultEncodeOptions = EncodeOptions { encodeConformanceProfile = Strict_9580_AA , encodeCRC24Emission = EncodeCRC24EmissionAuto } openpgp-asciiarmor-1.0/Codec/Encryption/OpenPGP/ASCIIArmor/Utils.hs0000644000000000000000000000146207346545000023251 0ustar0000000000000000-- ASCIIArmor/Utils.hs: OpenPGP (RFC4880) ASCII armor implementation -- Copyright © 2012 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file). module Codec.Encryption.OpenPGP.ASCIIArmor.Utils ( crlfUnlines , crlfUnlinesLazy ) where import Data.ByteString (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy.Char8 as BLC8 import Data.List (intersperse) crlfUnlines :: [ByteString] -> ByteString crlfUnlines [] = B.empty crlfUnlines ss = B.concat $ intersperse (BC8.pack "\r\n") ss crlfUnlinesLazy :: [BL.ByteString] -> BL.ByteString crlfUnlinesLazy [] = BL.empty crlfUnlinesLazy ss = BL.concat $ intersperse (BLC8.pack "\r\n") ss openpgp-asciiarmor-1.0/Data/Digest/0000755000000000000000000000000007346545000015432 5ustar0000000000000000openpgp-asciiarmor-1.0/Data/Digest/CRC24.hs0000644000000000000000000000161707346545000016550 0ustar0000000000000000-- CRC24.hs: OpenPGP (RFC4880) CRC24 implementation -- Copyright © 2012-2019 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file). module Data.Digest.CRC24 ( crc24 , crc24Lazy ) where import Data.Bits (shiftL, (.&.), xor) import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import Data.Word (Word8, Word32) crc24Init :: Word32 crc24Init = 0xB704CE crc24Poly :: Word32 crc24Poly = 0x1864CFB crc24Update :: Word32 -> Word8 -> Word32 crc24Update c b = (last . take 9 $ iterate (\x -> if shiftL x 1 .&. 0x1000000 == 0x1000000 then shiftL x 1 `xor` crc24Poly else shiftL x 1) (c `xor` shiftL (fromIntegral b) 16)) .&. 0xFFFFFF crc24 :: B.ByteString -> Word32 crc24 bs = crc24Lazy . BL.fromChunks $ [bs] crc24Lazy :: ByteString -> Word32 crc24Lazy = BL.foldl' crc24Update crc24Init openpgp-asciiarmor-1.0/LICENSE0000644000000000000000000000206607346545000014353 0ustar0000000000000000Copyright © 2012-2019 Clint Adams Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. openpgp-asciiarmor-1.0/Setup.hs0000644000000000000000000000005607346545000014777 0ustar0000000000000000import Distribution.Simple main = defaultMain openpgp-asciiarmor-1.0/bench/0000755000000000000000000000000007346545000014421 5ustar0000000000000000openpgp-asciiarmor-1.0/bench/mark.hs0000644000000000000000000000074507346545000015715 0ustar0000000000000000-- mark.hs: openpgp-asciiarmor benchmark suite -- Copyright © 2018-2019 Clint Adams -- This software is released under the terms of the Expat license. -- (See the LICENSE file). {-# LANGUAGE FlexibleContexts, OverloadedStrings #-} import Criterion.Main import Data.Digest.CRC24 main :: IO () main = defaultMain [ bgroup "crc" [ bench "crc24" $ whnf crc24 "test" , bench "crc24Lazy" $ whnf crc24Lazy "test" ] ] openpgp-asciiarmor-1.0/openpgp-asciiarmor.cabal0000644000000000000000000000557007346545000020134 0ustar0000000000000000Name: openpgp-asciiarmor Version: 1.0 Synopsis: OpenPGP (RFC9580) ASCII Armor codec Description: OpenPGP (RFC9580) ASCII Armor codec with legacy (RFC4880) compatibility Homepage: https://salsa.debian.org/clint/openpgp-asciiarmor License: MIT License-file: LICENSE Author: Clint Adams Maintainer: Clint Adams Copyright: 2012-2026 Clint Adams Category: Codec, Data Build-type: Simple Extra-source-files: tests/suite.hs , tests/data/msg1.asc , tests/data/msg1a.asc , tests/data/msg1b.asc , tests/data/msg1c.asc , tests/data/msg1.gpg , tests/data/msg2.asc , tests/data/msg2.pgp , tests/data/msg3 , tests/data/msg3.asc , tests/data/msg3.sig , tests/data/msg4 , tests/data/msg4.asc , tests/data/msg4.sig , tests/data/seipdv2.pgp.aa , tests/data/seipdv2.pgp Cabal-version: >= 1.10 Library Exposed-modules: Codec.Encryption.OpenPGP.ASCIIArmor , Codec.Encryption.OpenPGP.ASCIIArmor.Decode , Codec.Encryption.OpenPGP.ASCIIArmor.Encode , Codec.Encryption.OpenPGP.ASCIIArmor.Types Other-Modules: Data.Digest.CRC24 , Codec.Encryption.OpenPGP.ASCIIArmor.Multipart , Codec.Encryption.OpenPGP.ASCIIArmor.Utils Ghc-options: -Wall Build-depends: attoparsec , base >= 4.7 && < 5 , base64-bytestring , binary >= 0.6.4.0 , bytestring >= 0.10 default-language: Haskell2010 Test-Suite tests type: exitcode-stdio-1.0 main-is: tests/suite.hs Ghc-options: -Wall Other-Modules: Codec.Encryption.OpenPGP.ASCIIArmor , Codec.Encryption.OpenPGP.ASCIIArmor.Decode , Codec.Encryption.OpenPGP.ASCIIArmor.Encode , Codec.Encryption.OpenPGP.ASCIIArmor.Types , Codec.Encryption.OpenPGP.ASCIIArmor.Multipart , Codec.Encryption.OpenPGP.ASCIIArmor.Utils , Data.Digest.CRC24 Build-depends: attoparsec , base > 4 && < 5 , base64-bytestring , binary , bytestring , tasty , tasty-hunit default-language: Haskell2010 Benchmark benchmark type: exitcode-stdio-1.0 main-is: bench/mark.hs Ghc-options: -Wall Build-depends: openpgp-asciiarmor , base , bytestring , criterion default-language: Haskell2010 source-repository head type: git location: https://salsa.debian.org/clint/openpgp-asciiarmor.git source-repository this type: git location: https://salsa.debian.org/clint/openpgp-asciiarmor.git tag: openpgp-asciiarmor/1.0 openpgp-asciiarmor-1.0/tests/data/0000755000000000000000000000000007346545000015415 5ustar0000000000000000openpgp-asciiarmor-1.0/tests/data/msg1.asc0000644000000000000000000000025107346545000016752 0ustar0000000000000000-----BEGIN PGP MESSAGE----- Version: OpenPrivacy 0.99 yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS vBSFjNSiVHsuAA== =njUN -----END PGP MESSAGE----- openpgp-asciiarmor-1.0/tests/data/msg1.gpg0000644000000000000000000000007207346545000016762 0ustar00000000000000008;mΉy% yy ٩ ũE@ҼԢT{.openpgp-asciiarmor-1.0/tests/data/msg1a.asc0000644000000000000000000000041107346545000017111 0ustar0000000000000000This file contains an ASCII-armored PGP message enclosed between -----BEGIN PGP MESSAGE----- Version: OpenPrivacy 0.99 yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS vBSFjNSiVHsuAA== =njUN -----END PGP MESSAGE----- two lines of arbitrary text. openpgp-asciiarmor-1.0/tests/data/msg1b.asc0000644000000000000000000000116707346545000017123 0ustar0000000000000000This file contains three ASCII-armored PGP messages interspersed -----BEGIN PGP MESSAGE----- Version: OpenPrivacy 0.99 yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS vBSFjNSiVHsuAA== =njUN -----END PGP MESSAGE----- -----BEGIN PGP MESSAGE----- Version: OpenPrivacy 0.99 yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS vBSFjNSiVHsuAA== =njUN -----END PGP MESSAGE----- with arbitrary text. -----BEGIN PGP MESSAGE----- Version: OpenPrivacy 0.99 yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS vBSFjNSiVHsuAA== =njUN -----END PGP MESSAGE----- All three messages are identical. openpgp-asciiarmor-1.0/tests/data/msg1c.asc0000644000000000000000000000077307346545000017126 0ustar0000000000000000-----BEGIN PGP MESSAGE----- Version: OpenPrivacy 0.99 yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS vBSFjNSiVHsuAA== =njUN -----END PGP MESSAGE----- -----BEGIN PGP MESSAGE----- Version: OpenPrivacy 0.99 yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS vBSFjNSiVHsuAA== =njUN -----END PGP MESSAGE----- -----BEGIN PGP MESSAGE----- Version: OpenPrivacy 0.99 yDgBO22WxBHv7O8X7O/jygAEzol56iUKiXmV+XmpCtmpqQUKiQrFqclFqUDBovzS vBSFjNSiVHsuAA== =njUN -----END PGP MESSAGE----- openpgp-asciiarmor-1.0/tests/data/msg2.asc0000644000000000000000000000056207346545000016760 0ustar0000000000000000-----BEGIN PGP MESSAGE, PART 01/02----- Version: ClosedPrivacy 0.99 pgAAAHnw/J+O5ibiYizWTVDTrl/FUe926aD7lhNS+qjFNASGRmSSvWqWyubVcW0Z YnVtKfpv03Y+zIUQv+TATmWcwpkzhQ9QeTk70ZBFFmNXsuM12dTQGkY8IDRsmUT9 =H7u4 -----END PGP MESSAGE, PART 01/02----- -----BEGIN PGP MESSAGE, PART 02/02----- m+f4GTQ2FwJzO0GeazzBV4ywKLqSnCQVFBNKhDnw =hLwC -----END PGP MESSAGE, PART 02/02----- openpgp-asciiarmor-1.0/tests/data/msg2.pgp0000644000000000000000000000017607346545000017001 0ustar0000000000000000y&b,MPӮ_QvR4Fdjqmbum)ov>̅Ne™3Py9;ѐEcW5F< 4lD46s;Ak$?D:PT/&ߐNca VxGnm驦d ÜHՅ5Ǎ]VȕK8OQmAHםV)'z?W89wu|!o7_ҵz@8#1vBv?6Y$6z[Wu-aOtt^eƺy`ҚJwsK (V)௨0! ףҴzF?topenpgp-asciiarmor-1.0/tests/data/msg40000644000000000000000000000003707346545000016212 0ustar0000000000000000This message is detach-signed. openpgp-asciiarmor-1.0/tests/data/msg4.asc0000644000000000000000000000150407346545000016757 0ustar0000000000000000-----BEGIN PGP SIGNATURE----- Version: GnuPG v1.4.12 (GNU/Linux) iQIcBAABAgAGBQJPmZE0AAoJEN/7iwtcb1WC6EcQAK+NpDfsgTJgq+5nhZLZQcDC +b+8K5eDn+Z/btFZz1h2mF3Y+MpJdgG5fvSSHsYRWiGuT8OBK5wm3vSYnSr8BeA5 JUJDdhasF7LVosb0ToNiWLbtj9D9iiqmCaPW56Y2u3Ktv5Y4nOAWZw31OGv21B1c ptYVv2iy1qPGgnHxYgM5ib37hzKlkTEGKFNMpfYqsMBZyXiKuLpVhEWelawMnCPJ 3b7loOhYvR1Odmolg0dmcFsay7s3uXC/nze+xqTM0Z1HnMxQVW54aixhOoajDO6V p7OUUN5g/PBXHvx/gh7gPqgRCazo93rHSgoP//AoEuljrU4W9iNpCEcE5HTdYx0b xh10WxWE4VHI8BVt5qhSzPe8BmFRWz+g9CmsJdmhw45W8XxRJMJwoTWzGPcmHTFk JTOgJ9/MKcBvYGsBd0wGYq82DbDPdtwGgh5Aa3nz1dxzLCq7qIhOa3VEYKAhCjTx UHWnKZrdjSKz8U4D6CECyxlK5UApPc4jWzn3XYCX8s2F84YW7htduE57Yf60bXom aefIbDWef1Q4MOUV10h1gyjXtdSiIHvJ0ItvGKmRiXztjhq6+azYN+4RaXzpF5/N pqG/DNeTouzMRSzEoLTQ2oBHB8VIbCnP6J2Ck3wPfJZfc6FyCv0gMPQ5pTToll97 6toTAyOHl3pI/inp7IGj =TJqj -----END PGP SIGNATURE----- openpgp-asciiarmor-1.0/tests/data/msg4.sig0000644000000000000000000000103707346545000016774 0ustar0000000000000000O4  \oUG72`gA+nYXv]Iv~Z!OÁ+&*9%BCvբNbX* 6r8g 8k\h֣Ƃqb921(SL*YxUE #ݾXNvj%Gfp[˻7p7ƤѝGPUnxj,a: P`W> zJ (cN#iGtct[QmRaQ[?)%١ÎV|Q$p5&1d%3')o`kwLb6 v@kys,*NkuD`! 4Pu)ݍ"N!J@)=#[9]ͅ]N{amz&il5T80Hu(׵Ԣ {Ћo|7i|ͦ דE,ĠڀGHl)蝂||_sr 094_{#zH)쁣openpgp-asciiarmor-1.0/tests/data/seipdv2.pgp0000644000000000000000000000033307346545000017500 0ustar0000000000000000m!ㄧ,AI&}[[(#RV AzX9أ-M\{Nj ШA;g `yT= 1l, Μ#{ grn!ЃtI=SCnNokK4[VdBZ openpgp-asciiarmor-1.0/tests/data/seipdv2.pgp.aa0000644000000000000000000000054007346545000020060 0ustar0000000000000000-----BEGIN PGP MESSAGE----- wW0GIQav44SnLNNBPMT5pEw1RZY8zjlKostWz+pDmoUF+5URxRkwyaitZyn0JaLC SYnwTOoiZgF9sz6WtxJJJrT8fRJbWyiTIxpSVr0gf/BBsHqDWDma0wfYo7QI+YIC 0i291xBNy1zBewWq0U6o0moCCQIG0KgYQaeiO2e9CrpgecrqyVQdPZv2H6UMMWws DM6crtsjhM572grlZ5Kzf55ybvQh0IOS9nQGi0naPQAI7VOrBENunE79qeW3z29r vZMR+ZZL3TRbqFaBZJy8tORC8NVaA6sLnZ4I -----END PGP MESSAGE----- openpgp-asciiarmor-1.0/tests/0000755000000000000000000000000007346545000014504 5ustar0000000000000000openpgp-asciiarmor-1.0/tests/suite.hs0000644000000000000000000002652407346545000016202 0ustar0000000000000000import Test.Tasty (defaultMain, testGroup, TestTree) import Test.Tasty.HUnit (Assertion, assertEqual, assertFailure, testCase) import Codec.Encryption.OpenPGP.ASCIIArmor (decode, decodeLazy, decodeLazyWith, encode, encodeLazy, encodeLazyWith, multipartMerge) import Codec.Encryption.OpenPGP.ASCIIArmor.Types import Codec.Encryption.OpenPGP.ASCIIArmor.Utils import Data.ByteString (ByteString) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Char8 as BC8 import qualified Data.ByteString.Lazy.Char8 as BLC8 import Data.Digest.CRC24 (crc24) import Data.List (find) import Data.Word (Word32) legacyDecodeOptions :: DecodeOptions legacyDecodeOptions = defaultDecodeOptions { decodeConformanceProfile = Strict_4880_AA } legacyEncodeOptions :: EncodeOptions legacyEncodeOptions = defaultEncodeOptions { encodeConformanceProfile = Strict_4880_AA } modernDecodeOptions :: DecodeOptions modernDecodeOptions = defaultDecodeOptions { decodeConformanceProfile = Strict_9580_AA } testCRC24 :: ByteString -> Word32 -> Assertion testCRC24 bs crc = assertEqual "crc24" crc (crc24 bs) testArmorDecode :: FilePath -> [FilePath] -> Assertion testArmorDecode fp targets = do bs <- BL.readFile $ "tests/data/" ++ fp tbss <- mapM (\target -> BL.readFile $ "tests/data/" ++ target) targets case decodeLazy bs of Left e -> assertFailure $ "Decode failed (" ++ e ++ ") on " ++ fp Right as -> assertEqual ("for " ++ fp) tbss (map getPayload as) where getPayload (Armor _ _ pl) = pl getPayload _ = error "This should not happen." testArmorMultipartDecode :: FilePath -> FilePath -> Assertion testArmorMultipartDecode fp target = do bs <- BL.readFile $ "tests/data/" ++ fp tbs <- BL.readFile $ "tests/data/" ++ target case decodeLazy bs of Left e -> assertFailure $ "Decode failed (" ++ e ++ ") on " ++ fp Right as -> assertEqual ("for " ++ fp) tbs (getPayload (multipartMerge as)) where getPayload (Armor _ _ pl) = pl getPayload _ = error "This should not happen." testClearsignedDecodeBody :: FilePath -> FilePath -> Assertion testClearsignedDecodeBody fp target = do bs <- BL.readFile $ "tests/data/" ++ fp tbs <- BL.readFile $ "tests/data/" ++ target case decodeLazy bs of Left e -> assertFailure $ "Decode failed (" ++ e ++ ") on " ++ fp Right [a] -> assertEqual ("for " ++ fp) (convertEndings tbs) (getBody a) _ -> assertFailure "This shouldn't happen." where getBody (ClearSigned _ txt _) = txt getBody _ = error "This should not happen." convertEndings = crlfUnlinesLazy . BLC8.lines testClearsignedDecodeSig :: FilePath -> FilePath -> Assertion testClearsignedDecodeSig fp target = do bs <- BL.readFile $ "tests/data/" ++ fp tbs <- BL.readFile $ "tests/data/" ++ target case decodeLazy bs of Left e -> assertFailure $ "Decode failed (" ++ e ++ ") on " ++ fp Right [a] -> assertEqual ("for " ++ fp) tbs (getSig a) _ -> assertFailure "This shouldn't happen." where getSig (ClearSigned _ _ (Armor _ _ sig)) = sig getSig _ = error "This should not happen." testLegacyArmorEncode :: [FilePath] -> FilePath -> Assertion testLegacyArmorEncode fps target = do bss <- mapM (\fp -> BL.readFile $ "tests/data/" ++ fp) fps tbs <- BL.readFile $ "tests/data/" ++ target assertEqual "literaldata" tbs (encodeLazyWith legacyEncodeOptions (map (Armor ArmorMessage [("Version","OpenPrivacy 0.99")]) bss)) testLegacyClearsignedEncode :: FilePath -> FilePath -> FilePath -> Assertion testLegacyClearsignedEncode ftxt fsig ftarget = do txt <- BL.readFile $ "tests/data/" ++ ftxt sig <- BL.readFile $ "tests/data/" ++ fsig target <- BL.readFile $ "tests/data/" ++ ftarget assertEqual "clearsigned encode" target (encodeLazyWith legacyEncodeOptions [ClearSigned [("Hash","SHA1")] txt (Armor ArmorSignature [("Version","OpenPrivacy 0.99")] sig)]) testStrictDecode :: FilePath -> Assertion testStrictDecode fp = do bs <- BL.readFile $ "tests/data/" ++ fp assertEqual "strict decode" (decodeLazy bs :: Either String [Armor]) (decode (B.concat . BL.toChunks $ bs) :: Either String [Armor]) testStrictEncode :: FilePath -> Assertion testStrictEncode fp = do bs <- BL.readFile $ "tests/data/" ++ fp let fakearmors = [Armor ArmorMessage [("Version","OpenPrivacy 0.99")] bs, ClearSigned [("Hash","SHA1")] bs (Armor ArmorSignature [("Version","OpenPrivacy 0.99")] bs)] assertEqual "strict encode" (encodeLazy fakearmors) (BL.fromChunks [encode fakearmors]) testModernDecodeAllowsMissingCRC :: Assertion testModernDecodeAllowsMissingCRC = do bs <- BL.readFile "tests/data/msg1.asc" let noCrc = removeLinePrefix (BLC8.pack "=") bs case decodeLazyWith defaultDecodeOptions noCrc of Left e -> assertFailure $ "modern decode should allow missing CRC24: " ++ e Right _ -> return () testLegacyDecodeRejectsMissingCRC :: Assertion testLegacyDecodeRejectsMissingCRC = do bs <- BL.readFile "tests/data/msg1.asc" let noCrc = removeLinePrefix (BLC8.pack "=") bs case (decodeLazyWith legacyDecodeOptions noCrc :: Either String [Armor]) of Left _ -> return () Right [] -> return () Right _ -> assertFailure "legacy decode should reject missing CRC24" testModernDecodeAllowsMalformedCRC :: Assertion testModernDecodeAllowsMalformedCRC = do bs <- BL.readFile "tests/data/msg1.asc" let malformed = replaceLinePrefix (BLC8.pack "=") (BLC8.pack "=??!!") bs case decodeLazyWith defaultDecodeOptions malformed of Left e -> assertFailure $ "modern decode should allow malformed CRC24: " ++ e Right _ -> return () testModernDecodeAllowsMismatchedCRC :: Assertion testModernDecodeAllowsMismatchedCRC = do bs <- BL.readFile "tests/data/msg1.asc" let mismatch = replaceLinePrefix (BLC8.pack "=") (BLC8.pack "=AAAA") bs case decodeLazyWith defaultDecodeOptions mismatch of Left e -> assertFailure $ "modern decode should allow mismatched CRC24: " ++ e Right _ -> return () testDecodeIgnoresBase64Whitespace :: Assertion testDecodeIgnoresBase64Whitespace = do bs <- BL.readFile "tests/data/msg1.asc" let whitespacey = addWhitespaceToBase64 bs assertEqual "whitespace-tolerant decode" (decodeLazy bs :: Either String [Armor]) (decodeLazy whitespacey :: Either String [Armor]) testModernCleartextHeaderStrictness :: Assertion testModernCleartextHeaderStrictness = do bs <- BL.readFile "tests/data/msg3.asc" let withVersionHeader = insertHeaderAfterPgpSigned (BLC8.pack "Version: Test 1.0") bs case decodeLazyWith legacyDecodeOptions withVersionHeader :: Either String [Armor] of Left e -> assertFailure $ "legacy decode should allow non-Hash cleartext headers: " ++ e Right as | any isClearSigned as -> return () | otherwise -> assertFailure "legacy decode should parse cleartext message" case decodeLazyWith modernDecodeOptions withVersionHeader :: Either String [Armor] of Left _ -> return () Right as | any isClearSigned as -> assertFailure "modern decode should reject non-Hash cleartext headers" | otherwise -> return () where isClearSigned (ClearSigned _ _ _) = True isClearSigned _ = False testEncodeModernOmitsCRC24 :: Assertion testEncodeModernOmitsCRC24 = do bs <- BL.readFile "tests/data/msg1.gpg" let encoded = encodeLazyWith defaultEncodeOptions [Armor ArmorMessage [("Version","OpenPrivacy 0.99")] bs] case find (BL.isPrefixOf (BLC8.pack "=")) (BLC8.lines encoded) of Nothing -> return () Just _ -> assertFailure "modern encode should omit CRC24 line" testEncodeLegacyIncludesCRC24 :: Assertion testEncodeLegacyIncludesCRC24 = do bs <- BL.readFile "tests/data/msg1.gpg" let encoded = encodeLazyWith legacyEncodeOptions [Armor ArmorMessage [("Version","OpenPrivacy 0.99")] bs] case find (BL.isPrefixOf (BLC8.pack "=")) (BLC8.lines encoded) of Nothing -> assertFailure "legacy encode should include CRC24 line" Just _ -> return () removeLinePrefix :: BL.ByteString -> BL.ByteString -> BL.ByteString removeLinePrefix prefix = BLC8.unlines . filter (not . BL.isPrefixOf prefix) . BLC8.lines replaceLinePrefix :: BL.ByteString -> BL.ByteString -> BL.ByteString -> BL.ByteString replaceLinePrefix prefix replacement = BLC8.unlines . map replace . BLC8.lines where replace line | BL.isPrefixOf prefix line = replacement | otherwise = line insertHeaderAfterPgpSigned :: BL.ByteString -> BL.ByteString -> BL.ByteString insertHeaderAfterPgpSigned header bs = case BLC8.lines bs of [] -> bs (x:xs) -> BLC8.unlines (x : header : xs) addWhitespaceToBase64 :: BL.ByteString -> BL.ByteString addWhitespaceToBase64 = BLC8.unlines . map tweak . BLC8.lines where tweak line | BL.null line = line | BL.isPrefixOf (BLC8.pack "-----") line = line | BLC8.elem ':' line = line | BL.isPrefixOf (BLC8.pack "=") line = BL.append (BLC8.pack "= ") (BL.drop 1 line) | otherwise = BL.append (BLC8.pack " ") line tests :: TestTree tests = testGroup "openpgp-asciiarmor" [ testGroup "CRC24" [ testCase "CRC24: A" (testCRC24 (BC8.pack "A") 16680698) , testCase "CRC24: Haskell" (testCRC24 (BC8.pack "Haskell") 15612750) , testCase "CRC24: hOpenPGP and friends" (testCRC24 (BC8.pack "hOpenPGP and friends") 11940960) ] , testGroup "ASCII armor" [ testCase "Decode sample armor" (testArmorDecode "msg1.asc" ["msg1.gpg"]) , testCase "Decode sample armor with cruft" (testArmorDecode "msg1a.asc" ["msg1.gpg"]) , testCase "Decode multiple sample armors" (testArmorDecode "msg1b.asc" ["msg1.gpg","msg1.gpg","msg1.gpg"]) , testCase "Decode detached signature" (testArmorDecode "msg4.asc" ["msg4.sig"]) , testCase "Decode multi-part armor" (testArmorMultipartDecode "msg2.asc" "msg2.pgp") , testCase "Decode body of clear-signed" (testClearsignedDecodeBody "msg3.asc" "msg3") , testCase "Decode sig of clear-signed" (testClearsignedDecodeSig "msg3.asc" "msg3.sig") , testCase "Encode (legacy) sample armor" (testLegacyArmorEncode ["msg1.gpg"] "msg1.asc") , testCase "Encode (legacy) multiple sample armors" (testLegacyArmorEncode ["msg1.gpg","msg1.gpg","msg1.gpg"] "msg1c.asc") , testCase "Encode (legacy) clear-signed sig" (testLegacyClearsignedEncode "msg3" "msg3.sig" "msg3.asc") , testCase "Decode from strict ByteString" (testStrictDecode "msg1.asc") , testCase "Encode to strict ByteString" (testStrictEncode "msg1.gpg") , testCase "Decode sample RFC9580 armor" (testArmorDecode "seipdv2.pgp.aa" ["seipdv2.pgp"]) , testCase "Modern decode accepts missing CRC24" testModernDecodeAllowsMissingCRC , testCase "Legacy decode rejects missing CRC24" testLegacyDecodeRejectsMissingCRC , testCase "Modern decode accepts malformed CRC24" testModernDecodeAllowsMalformedCRC , testCase "Modern decode accepts mismatched CRC24" testModernDecodeAllowsMismatchedCRC , testCase "Decode ignores base64 whitespace" testDecodeIgnoresBase64Whitespace , testCase "Modern cleartext profile rejects non-Hash headers" testModernCleartextHeaderStrictness , testCase "Modern encode omits CRC24" testEncodeModernOmitsCRC24 , testCase "Legacy encode includes CRC24" testEncodeLegacyIncludesCRC24 ] ] main :: IO () main = defaultMain tests