hpdft-0.4.6.0: src/PDF/Encrypt.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeApplications #-}
module PDF.Encrypt
( Security
, securityFromEncryptDict
, decryptString
, decryptStream
) where
import PDF.Definition
import qualified Data.Text as T
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import Data.Bits (shiftL, shiftR, xor, (.&.))
import Data.List (foldl')
import qualified Data.Map as M
import Control.Applicative ((<|>))
import Data.Word (Word8, Word32)
import Numeric (readHex)
import Crypto.Hash (hash, MD5(..), Digest)
import Crypto.Cipher.AES (AES128)
import Crypto.Cipher.Types (BlockCipher, cipherInit, ecbDecrypt)
import Crypto.Error (onCryptoFailure)
import Data.ByteArray (convert)
padString :: BS.ByteString
padString = BS.pack
[ 0x28, 0xBF, 0x4E, 0x5E, 0x4E, 0x75, 0x8A, 0x41
, 0x64, 0x00, 0x4E, 0x56, 0xFF, 0xFA, 0x01, 0x08
, 0x2E, 0x2E, 0x00, 0xB6, 0xD0, 0x68, 0x3E, 0x80
, 0x2F, 0x0C, 0xA9, 0xFE, 0x64, 0x53, 0x69, 0x7A
]
data Security = Security
{ secRevision :: Int
, secVersion :: Int
, secKey :: BS.ByteString
, secKeyLength :: Int
, secAES :: Bool
} deriving Show
securityFromEncryptDict :: Dict -> Dict -> Maybe String -> Maybe Security
securityFromEncryptDict encDict trailer password = do
r <- dictInt encDict "/R"
v <- dictInt encDict "/V"
o <- dictBytes encDict "/O"
u <- dictBytes encDict "/U"
p <- dictInt encDict "/P"
fileId <- dictFirstId trailer
let pw = maybe BS.empty BSC.pack password
aes = v >= 4 || usesAES encDict
metaEnc = encryptMetadata encDict
keyLen = case dictInt encDict "/Length" of
Just n -> max 5 (n `div` 8)
Nothing -> if r >= 3 || v >= 2 then 16 else 5
key <- authenticateFileKey pw o u p fileId r v aes metaEnc keyLen
return $ Security r v key keyLen aes
-- | Try owner password first (Algorithm 7), then user password (Algorithm 6).
authenticateFileKey :: BS.ByteString -> BS.ByteString -> BS.ByteString -> Int -> BS.ByteString -> Int -> Int -> Bool -> Bool -> Int -> Maybe BS.ByteString
authenticateFileKey pw o u p fileId r v aes metaEnc keyLen =
ownerPasswordKey pw o u p fileId r aes metaEnc keyLen
<|> userPasswordKey pw o u p fileId r aes metaEnc keyLen
userPasswordKey :: BS.ByteString -> BS.ByteString -> BS.ByteString -> Int -> BS.ByteString -> Int -> Bool -> Bool -> Int -> Maybe BS.ByteString
userPasswordKey pw o u p fileId r aes metaEnc keyLen =
let key = fileKeyFromPassword pw o p fileId r aes metaEnc keyLen
in if verifyUserPassword r fileId key u then Just key else Nothing
ownerPasswordKey :: BS.ByteString -> BS.ByteString -> BS.ByteString -> Int -> BS.ByteString -> Int -> Bool -> Bool -> Int -> Maybe BS.ByteString
ownerPasswordKey ownerPw o u p fileId r aes metaEnc keyLen =
let oKey = computeOwnerValueKey ownerPw r keyLen
userPw = decryptOToUserPassword oKey r o
in userPasswordKey userPw o u p fileId r aes metaEnc keyLen
fileKeyFromPassword :: BS.ByteString -> BS.ByteString -> Int -> BS.ByteString -> Int -> Bool -> Bool -> Int -> BS.ByteString
fileKeyFromPassword pw o p fileId r aes metaEnc keyLen =
if r >= 4 || aes
then computeFileKeyAES pw o p fileId r metaEnc
else computeFileKey pw o p fileId r keyLen metaEnc
padPassword :: BS.ByteString -> BS.ByteString
padPassword pw = BS.take 32 $ pw `BS.append` padString
-- Algorithm 3 (a–d): RC4 key from owner password for decrypting /O.
computeOwnerValueKey :: BS.ByteString -> Int -> Int -> BS.ByteString
computeOwnerValueKey ownerPassword r keyLen =
let hashed = iterate md5 (md5 (padPassword ownerPassword)) !! (if r >= 3 then 50 else 0)
in BS.take keyLen hashed
-- Algorithm 7 (b): recover padded user password from /O.
decryptOToUserPassword :: BS.ByteString -> Int -> BS.ByteString -> BS.ByteString
decryptOToUserPassword oKey r o
| r <= 2 = rc4Decrypt oKey o
| otherwise = foldl' (\ct m -> rc4Decrypt (xorKey oKey m) ct) o [19,18..0]
usesAES :: Dict -> Bool
usesAES d = case dictLookup d "/CF" of
Just (PdfDict cf) -> case dictLookup cf "/StdCF" of
Just (PdfDict std) -> case dictLookup std "/CFM" of
Just (PdfName "/AESV2") -> True
Just (PdfName "/AESV3") -> True
_ -> False
_ -> False
_ -> False
encryptMetadata :: Dict -> Bool
encryptMetadata d = case dictLookup d "/EncryptMetadata" of
Just (PdfBool False) -> False
_ -> True
dictLookup :: Dict -> T.Text -> Maybe Obj
dictLookup d name = M.lookup name d
dictInt :: Dict -> T.Text -> Maybe Int
dictInt d name = case dictLookup d name of
Just (PdfNumber n) -> Just (truncate n)
_ -> Nothing
dictBytes :: Dict -> T.Text -> Maybe BS.ByteString
dictBytes d name = case dictLookup d name of
Just (PdfText s) -> Just (BSC.pack (T.unpack s))
Just (PdfHex h) -> hexToBytes h
_ -> Nothing
dictFirstId :: Dict -> Maybe BS.ByteString
dictFirstId d = case dictLookup d "/ID" of
Just (PdfArray (entry:_)) -> idEntryBytes entry
_ -> Nothing
idEntryBytes :: Obj -> Maybe BS.ByteString
idEntryBytes (PdfHex h) = hexToBytes h
idEntryBytes (PdfText s) = hexToBytes s <|> Just (BSC.pack (T.unpack s))
idEntryBytes _ = Nothing
hexToBytes :: T.Text -> Maybe BS.ByteString
hexToBytes h
| T.null h = Just BS.empty
| otherwise = Just $ BS.pack $ map (fromIntegral . fst . head . readHex) (pairs (T.unpack h))
where
pairs [] = []
pairs s = take 2 s : pairs (drop 2 s)
md5 :: BS.ByteString -> BS.ByteString
md5 bs = convert (hash bs :: Digest MD5)
computeFileKey :: BS.ByteString -> BS.ByteString -> Int -> BS.ByteString -> Int -> Int -> Bool -> BS.ByteString
computeFileKey password o p fileId r keyLen metaEnc =
let padded = BS.take 32 $ password `BS.append` padString
suffix = if r >= 4 && not metaEnc then BS.pack [0xFF, 0xFF, 0xFF, 0xFF] else BS.empty
base = padded `BS.append` o `BS.append` int32LE p `BS.append` fileId `BS.append` suffix
hashed = iterate (\h -> md5 (BS.take keyLen h)) (md5 base) !! (if r >= 3 then 50 else 0)
in BS.take keyLen hashed
computeFileKeyAES :: BS.ByteString -> BS.ByteString -> Int -> BS.ByteString -> Int -> Bool -> BS.ByteString
computeFileKeyAES password o p fileId r metaEnc =
let padded = BS.take 32 $ password `BS.append` padString
suffix = if r >= 4 && not metaEnc then BS.pack [0xFF, 0xFF, 0xFF, 0xFF] else BS.empty
base = padded `BS.append` o `BS.append` int32LE p `BS.append` fileId `BS.append` suffix
hashed = (!! 50) $ iterate (\h -> md5 (BS.take 16 h)) (md5 base)
in BS.take 16 hashed
int32LE :: Int -> BS.ByteString
int32LE n =
let w = fromIntegral n :: Word32
in BS.pack
[ fromIntegral (w .&. 0xff)
, fromIntegral ((w `shiftR` 8) .&. 0xff)
, fromIntegral ((w `shiftR` 16) .&. 0xff)
, fromIntegral ((w `shiftR` 24) .&. 0xff)
]
verifyUserPassword :: Int -> BS.ByteString -> BS.ByteString -> BS.ByteString -> Bool
verifyUserPassword r fileId key u =
let computed = computeU r fileId key
in if r >= 3
then BS.take 16 computed == BS.take 16 u
else BS.take 32 computed == BS.take 32 u
computeU :: Int -> BS.ByteString -> BS.ByteString -> BS.ByteString
computeU r fileId key
| r <= 2 = rc4Decrypt key padString
| otherwise =
let hashed = md5 (padString `BS.append` fileId)
encrypted = rc4Decrypt key hashed
finished = foldl' (\ct i -> rc4Decrypt (xorKey key i) ct) encrypted [1..19]
in BS.append (BS.take 16 finished) (BS.replicate 16 0)
xorKey :: BS.ByteString -> Int -> BS.ByteString
xorKey key i = BS.pack $ map (xor (fromIntegral i)) (BS.unpack key)
saltAES :: BS.ByteString
saltAES = BSC.pack "sAlT"
objectKey :: Security -> Int -> Int -> BS.ByteString
objectKey sec objNum genNum =
let n = secKeyLength sec
ext = BS.take n (secKey sec)
`BS.append` int24LE objNum
`BS.append` int16LE genNum
in if secAES sec
then BS.take (min (n + 5) 16) (md5 (ext `BS.append` saltAES))
else BS.take (min (n + 5) 16) (md5 ext)
int24LE :: Int -> BS.ByteString
int24LE n =
let w = fromIntegral n :: Word32
in BS.pack
[ fromIntegral (w .&. 0xff)
, fromIntegral ((w `shiftR` 8) .&. 0xff)
, fromIntegral ((w `shiftR` 16) .&. 0xff)
]
int16LE :: Int -> BS.ByteString
int16LE n =
let w = fromIntegral n :: Word32
in BS.pack
[ fromIntegral (w .&. 0xff)
, fromIntegral ((w `shiftR` 8) .&. 0xff)
]
rc4Decrypt :: BS.ByteString -> BS.ByteString -> BS.ByteString
rc4Decrypt key ciphertext =
let ks = rc4KeyStream key (BS.length ciphertext)
in BS.pack $ zipWith xor (BS.unpack ciphertext) ks
rc4DecryptRev3 :: Int -> BS.ByteString -> BS.ByteString -> BS.ByteString
rc4DecryptRev3 keyLen key ciphertext =
foldl' (\ct m -> rc4Decrypt (BS.take keyLen (md5 (key `BS.append` BS.pack [fromIntegral m]))) ct)
ciphertext
[19,18..0]
rc4KeyStream :: BS.ByteString -> Int -> [Word8]
rc4KeyStream key nbytes =
let keyInts = map fromIntegral (BS.unpack key) :: [Int]
state = ksaInit keyInts
in map fromIntegral $ take nbytes $ prga state
where
ksaInit k' =
let len = length k'
step (st, j) i =
let j' = (j + st !! i + k' !! (i `mod` len)) `mod` 256
st' = swap st i j'
in (st', j')
in fst $ foldl' step ([0..255], 0) [0..255]
prga st = reverse $ go st 0 0 []
where go sbox i j out
| length out >= nbytes = out
| otherwise =
let i' = (i + 1) `mod` 256
j' = (j + sbox !! i') `mod` 256
s' = swap sbox i' j'
byte = s' !! ((s' !! i' + s' !! j') `mod` 256)
in go s' i' j' (byte:out)
swap s i j =
[ if x == i then s !! j else if x == j then s !! i else v
| (x, v) <- zip [0..] s ]
decryptString :: Maybe Security -> Int -> Int -> BS.ByteString -> BS.ByteString
decryptString Nothing _ _ bs = bs
decryptString (Just sec) objNum genNum bs =
let ok = objectKey sec objNum genNum
in case () of
_ | secAES sec -> stripPkcs7 $ aesDecrypt ok bs
_ -> rc4Decrypt ok bs
decryptStream :: Maybe Security -> Int -> Int -> BS.ByteString -> BS.ByteString
decryptStream Nothing _ _ bs = bs
decryptStream (Just sec) objNum genNum bs =
let ok = objectKey sec objNum genNum
in case () of
_ | secAES sec -> stripPkcs7 $ aesDecrypt ok bs
_ -> rc4Decrypt ok bs
stripPkcs7 :: BS.ByteString -> BS.ByteString
stripPkcs7 bs
| BS.null bs = bs
| otherwise =
let pad = fromIntegral (BS.last bs)
len = BS.length bs
in if pad >= 1 && pad <= 16 && len >= pad
&& BS.all (== BS.last bs) (BS.drop (len - pad) bs)
then BS.take (len - pad) bs
else bs
aesDecrypt :: BS.ByteString -> BS.ByteString -> BS.ByteString
aesDecrypt _ bs | BS.length bs < 16 = bs
aesDecrypt key bs =
onCryptoFailure (const bs) id $
cipherInit key >>= \cipher ->
let iv = BS.take 16 bs
body = BS.drop 16 bs
in return $ cbcDecrypt iv cipher body
cbcDecrypt :: BS.ByteString -> AES128 -> BS.ByteString -> BS.ByteString
cbcDecrypt _ _ bs | BS.null bs = BS.empty
cbcDecrypt prev cipher bs =
let (block, rest) = BS.splitAt 16 bs
plain = ecbDecrypt cipher block
out = BS.pack $ zipWith xor (BS.unpack plain) (BS.unpack prev)
in if BS.length block < 16
then BS.empty
else out `BS.append` cbcDecrypt block cipher rest