packages feed

pdf-toolbox-core-0.1.1: lib/Pdf/Core/Encryption.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Basic support for encrypted PDF files

module Pdf.Core.Encryption
( Decryptor
, DecryptorScope(..)
, defaultUserPassword
, mkStandardDecryptor
, decryptObject
)
where

import Pdf.Core.Object
import Pdf.Core.Object.Util
import Pdf.Core.Util

import qualified Data.Traversable as Traversable
import Data.Bits (xor)
import Data.IORef
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BSL
import Data.ByteString.Builder
import qualified Data.Vector as Vector
import qualified Data.HashMap.Strict as HashMap
import Control.Monad
import System.IO.Streams (InputStream)
import qualified System.IO.Streams as Streams
import qualified Crypto.Cipher.RC4 as RC4
import qualified Crypto.Cipher.AES as AES
import qualified Crypto.Hash.MD5 as MD5
import qualified Crypto.Padding as Padding

-- | Encryption handler may specify different encryption keys for strings
-- and streams
data DecryptorScope
  = DecryptString
  | DecryptStream

-- | Decrypt input stream
type Decryptor
  =  Ref
  -> DecryptorScope
  -> InputStream ByteString
  -> IO (InputStream ByteString)

-- | Decrypt object with the decryptor
decryptObject :: Decryptor -> Ref -> Object -> IO Object
decryptObject decryptor ref (String str)
  = String <$> decryptStr decryptor ref str
decryptObject decryptor ref (Dict dict)
  = Dict <$> decryptDict decryptor ref dict
decryptObject decryptor ref (Array arr)
  = Array <$> decryptArray decryptor ref arr
decryptObject _ _ o = return o

decryptStr :: Decryptor -> Ref -> ByteString -> IO ByteString
decryptStr decryptor ref str = do
  is <- Streams.fromList [str]
  res <- decryptor ref DecryptString is >>= Streams.toList
  return $ BS.concat res

decryptDict :: Decryptor -> Ref -> Dict -> IO Dict
decryptDict decryptor ref vals = Traversable.forM vals $
  decryptObject decryptor ref

decryptArray :: Decryptor -> Ref -> Array -> IO Array
decryptArray decryptor ref vals = Vector.forM vals decr
  where
  decr = decryptObject decryptor ref

-- | The default user password
defaultUserPassword :: ByteString
defaultUserPassword = 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
  ]

-- | Standard decryptor, RC4
mkStandardDecryptor :: Dict
                    -- ^ document trailer
                    -> Dict
                    -- ^ encryption dictionary
                    -> ByteString
                    -- ^ user password (32 bytes exactly,
                    -- see 7.6.3.3 Encryption Key Algorithm)
                    -> Either String (Maybe Decryptor)
mkStandardDecryptor tr enc pass = do
  filterType <-
    case HashMap.lookup "Filter" enc of
      Just o -> nameValue o `notice` "Filter should be a name"
      _ -> Left "Filter missing"
  unless (filterType == "Standard") $
    Left ("Unsupported encryption handler: " ++ show filterType)

  v <-
    case HashMap.lookup "V" enc of
      Just n -> intValue n `notice` "V should be an integer"
      _ -> Left "V is missing"

  if v == 4
    then mk4
    else mk12 v

  where
  mk12 v = do
    n <-
      case v of
        1 -> Right 5
        2 -> do
          case HashMap.lookup "Length" enc of
            Just o -> fmap (`div` 8) (intValue o
                        `notice` "Length should be an integer")
            Nothing -> Left "Length is missing"
        _ -> Left ("Unsuported encryption handler version: " ++ show v)

    ekey <- mkKey tr enc pass n
    ok <- verifyKey tr enc ekey
    return $
      if not ok
        then Nothing
        else Just $ \ref _ is -> mkDecryptor V2 ekey n ref is

  mk4 = do
    cryptoFilters <-
      case HashMap.lookup "CF" enc of
        Nothing -> Left "CF is missing in crypt handler V4"
        Just o -> dictValue o `notice` "CF should be a dictionary"
    keysMap <- Traversable.forM cryptoFilters $ \obj -> do
      dict <- dictValue obj `notice` "Crypto filter should be a dictionary"
      n <-
        case HashMap.lookup "Length" dict of
          Nothing -> Left "Crypto filter without Length"
          Just o -> intValue o `notice` "Crypto filter length should be int"
      algName <-
        case HashMap.lookup "CFM" dict of
          Nothing -> Left "CFM is missing"
          Just o -> nameValue o `notice` "CFM should be a name"
      alg <-
        case algName of
          "V2" -> return V2
          "AESV2" -> return AESV2
          _ -> Left $ "Unknown crypto method: " ++ show algName
      ekey <- mkKey tr enc pass n
      return (ekey, n, alg)

    (stdCfKey, _, _) <- HashMap.lookup "StdCF" keysMap
      `notice` "StdCF is missing"
    ok <- verifyKey tr enc stdCfKey
    if not ok
      then return Nothing

      else do
        strFName <- (HashMap.lookup "StrF" enc >>= nameValue)
          `notice` "StrF is missing"
        (strFKey, strFN, strFAlg) <- HashMap.lookup strFName keysMap
          `notice` ("Crypto filter not found: " ++ show strFName)

        stmFName <- (HashMap.lookup "StmF" enc >>= nameValue)
          `notice` "StmF is missing"
        (stmFKey, stmFN, stmFAlg) <- HashMap.lookup stmFName keysMap
          `notice` ("Crypto filter not found: " ++ show stmFName)

        return $ Just $ \ref scope is ->
          case scope of
            DecryptString -> mkDecryptor strFAlg strFKey strFN ref is
            DecryptStream -> mkDecryptor stmFAlg stmFKey stmFN ref is

mkKey :: Dict -> Dict -> ByteString -> Int -> Either String ByteString
mkKey tr enc pass n = do
  oVal <- do
    o <- HashMap.lookup "O" enc `notice` "O is missing"
    stringValue o `notice` "o should be a string"

  pVal <- do
    o <- HashMap.lookup "P" enc `notice` "P is missing"
    i <- intValue o `notice` "P should be an integer"
    Right . BS.pack . BSL.unpack . toLazyByteString
          . word32LE . fromIntegral $ i

  idVal <- do
    ids <- (HashMap.lookup "ID" tr >>= arrayValue)
        `notice` "ID should be an array"
    case (Vector.toList ids) of
      [] -> Left "ID array is empty"
      (x:_) -> stringValue x
                  `notice` "The first element if ID should be a string"

  rVal <- (HashMap.lookup "R" enc >>= intValue)
      `notice` "R should be an integer"

  encMD <-
    case HashMap.lookup "EncryptMetadata" enc of
      Nothing -> return True
      Just o -> boolValue o `notice` "EncryptMetadata should be a bool"

  let ekey' = BS.take n $ MD5.hash $ BS.concat [pass, oVal, pVal, idVal, pad]
      pad =
        if rVal < 4 || encMD
          then BS.empty
          else BS.pack (replicate 4 255)
      ekey =
        if rVal < 3
           then ekey'
           else foldl (\bs _ -> BS.take n $ MD5.hash bs)
                      ekey'
                      [1 :: Int .. 50]

  return ekey

verifyKey :: Dict -> Dict -> ByteString -> Either String Bool
verifyKey tr enc ekey = do
  rVal <- (HashMap.lookup "R" enc >>= intValue)
      `notice` "R should be an integer"

  idVal <- do
    ids <- (HashMap.lookup "ID" tr >>= arrayValue)
        `notice` "ID should be an array"
    case (Vector.toList ids) of
      [] -> Left "ID array is empty"
      (x:_) -> stringValue x
                  `notice` "The first element if ID should be a string"

  uVal <- (HashMap.lookup "U" enc >>= stringValue)
      `notice` "U should be a string"

  return $
    case rVal of
      2 ->
        let uVal' = snd $ RC4.combine (RC4.initCtx ekey)
                                      defaultUserPassword
        in uVal == uVal'
      _ ->
        let pass1 = snd $ RC4.combine (RC4.initCtx ekey)
                        $ BS.take 16 $ MD5.hash
                        $ BS.concat [defaultUserPassword, idVal]
            uVal' = loop 1 pass1
            loop 20 input = input
            loop i input = loop (i + 1) $ snd $ RC4.combine (RC4.initCtx
                                        $ BS.map (`xor` i) ekey) input
        in BS.take 16 uVal == BS.take 16 uVal'

data Algorithm
  = V2
  | AESV2
  deriving (Show)

mkDecryptor
  :: Algorithm
  -> ByteString
  -> Int
  -> Ref
  -> InputStream ByteString
  -> IO (InputStream ByteString)
mkDecryptor alg ekey n (R index gen) is = do
  let key = BS.take (16 `min` n + 5) $ MD5.hash $ BS.concat
        [ ekey
        , BS.pack $ take 3 $ BSL.unpack $ toLazyByteString
                  $ int32LE $ fromIntegral index
        , BS.pack $ take 2 $ BSL.unpack $ toLazyByteString
                  $ int32LE $ fromIntegral gen
        , salt alg
        ]
      salt V2 = ""
      salt AESV2 = "sAlT"

  case alg of
    V2 -> do
      ioRef <- newIORef $ RC4.initCtx key
      let readNext = do
            chunk <- Streams.read is
            case chunk of
              Nothing -> return Nothing
              Just c -> do
                ctx' <- readIORef ioRef
                let (ctx'', res) = RC4.combine ctx' c
                writeIORef ioRef ctx''
                return (Just res)
      Streams.makeInputStream readNext

    AESV2 -> do
      content <- BS.concat <$> Streams.toList is
      let initV = BS.take 16 content
          aes = AES.initAES key
          decrypted = AES.decryptCBC aes initV $ BS.drop 16 content
      Streams.fromByteString $ Padding.unpadPKCS5 decrypted