dtab-1.1: src/Data/DTA/Crypt.hs
-- | The encryption scheme used for DTB files found on game discs.
module Data.DTA.Crypt
( oldCrypt, newCrypt
, decrypt, encrypt
, decryptFile, encryptFile
, decryptHandle, encryptHandle
, Key, Crypt
) where
import Control.Monad (forM_, liftM2, liftM3)
import Control.Monad.ST.Lazy (ST, runST)
import Data.Array.ST (STArray, newArray, readArray,
writeArray)
import Data.Bits (shiftR, xor, (.&.), (.|.))
import Data.STRef.Lazy (STRef, newSTRef, readSTRef, writeSTRef)
import Data.Word (Word32, Word8)
import System.IO (Handle)
import Data.Binary.Get (getRemainingLazyByteString, getWord32le,
runGet)
import Data.Binary.Put (putLazyByteString, putWord32le, runPut)
import qualified Data.ByteString.Lazy as BL
-- | An encryption/decryption key.
type Key = Word32
-- | Using a key to generate an infinite stream of crypt bytes.
type Crypt = Key -> [Word8]
{- |
The way both the new and old DTB encryption algorithms work is by using the key
to generate a stream of bytes. Each of these bytes is then XOR'd with the
corresponding bytes in the source file. The same algorithm is both the
decryption and encryption; this is because @(A xor B) xor B == A@.
-}
crypt :: Crypt -> Key -> BL.ByteString -> BL.ByteString
crypt cry key = BL.pack . zipWith xor (cry key) . BL.unpack
-- | Take the first four bytes of the string as the key, and decrypt the rest of
-- the file.
decrypt :: Crypt -> BL.ByteString -> BL.ByteString
decrypt cry = runGet $ liftM2 (crypt cry) getWord32le getRemainingLazyByteString
-- | Encrypt a string with a key, and append the key to the encrypted string.
encrypt :: Crypt -> Key -> BL.ByteString -> BL.ByteString
encrypt cry key input
= runPut $ putWord32le key >> putLazyByteString (crypt cry key input)
-- | Decrypt an encrypted DTB file using the given crypt method.
decryptFile :: Crypt -> FilePath -> FilePath -> IO ()
decryptFile cry fi fo = BL.readFile fi >>= BL.writeFile fo . decrypt cry
-- | Encrypt an unencrypted DTB file using the given crypt method and key.
encryptFile :: Crypt -> Key -> FilePath -> FilePath -> IO ()
encryptFile cry key fi fo = BL.readFile fi >>= BL.writeFile fo . encrypt cry key
-- | Decrypt an encrypted DTB file across two handles.
decryptHandle :: Crypt -> Handle -> Handle -> IO ()
decryptHandle cry hi ho = BL.hGetContents hi >>= BL.hPutStr ho . decrypt cry
-- | Encrypt an unencrypted DTB files across two handles.
encryptHandle :: Crypt -> Key -> Handle -> Handle -> IO ()
encryptHandle cry key hi ho = BL.hGetContents hi >>= BL.hPutStr ho . encrypt cry key
-- New (Rock Band) encryption
{-
From xorloser's dtbcrypt:
unsigned int dtb_xor_x360(unsigned int data)
{
int val1 = (data / 0x1F31D) * 0xB14;
int val2 = (data - ((data / 0x1F31D) * 0x1F31D)) * 0x41A7;
val2 = val2 - val1;
if(val2 <= 0)
val2 += 0x7FFFFFFF;
return val2;
}
-}
-- | The key iteration function for new DTB encryption/decryption.
dtbXor360 :: Word32 -> Word32
dtbXor360 d = let
q = quot d 0x1F31D
v = (d - (q * 0x1F31D)) * 0x41A7 - q * 0xB14 in
if v > 0x7FFFFFFF then v + 0x7FFFFFFF else v
-- | The lazy infinite list of crypt bytes for new-style encryption.
newCrypt :: Crypt
newCrypt key = fmap fromIntegral $ tail $ iterate dtbXor360 key
-- Old (PS2 Guitar Hero) encryption
-- This algorithm uses a large table to produce the XOR bytes, which is
-- implemented using an STArray in the lazy ST monad.
data CryptTable s = CryptTable
{ idx1 :: STRef s Word8
, idx2 :: STRef s Word8
, table :: STArray s Word8 Word32 }
cryptTable :: Key -> ST s (CryptTable s)
cryptTable key = do
v1ref <- newSTRef key
tbl <- newArray (0, 0xF8) 0
forM_ [0..0xF8] $ \i -> do
v1 <- readSTRef v1ref
let v2 = (v1 * 0x41C64E6D) + 0x3039
let v1' = (v2 * 0x41C64E6D) + 0x3039
writeSTRef v1ref v1'
writeArray tbl i $ (v1' .&. 0x7FFF0000) .|. (v2 `shiftR` 16)
liftM3 CryptTable (newSTRef 0) (newSTRef 0x67) (return tbl)
oldNext :: CryptTable s -> ST s Word8
oldNext (CryptTable { idx1 = i1ref, idx2 = i2ref, table = tbl }) = do
i1 <- readSTRef i1ref
i2 <- readSTRef i2ref
next <- liftM2 xor (readArray tbl i1) (readArray tbl i2)
writeArray tbl i1 next
writeSTRef i1ref $ if i1 == 0xF8 then 0 else i1 + 1
writeSTRef i2ref $ if i2 == 0xF8 then 0 else i2 + 1
return $ fromIntegral next
-- The fromIntegral chops each Word32 down to the least significant byte.
-- | The lazy infinite list of crypt bytes for old-style encryption.
oldCrypt :: Crypt
oldCrypt key = runST $ cryptTable key >>= sequence . repeat . oldNext