172 lines
6.2 KiB
Haskell
172 lines
6.2 KiB
Haskell
-- To compile into an executable:
|
|
-- ghc -main-is OneTimePad OneTimePad.hs
|
|
-- To run:
|
|
-- ./OneTimePad --help
|
|
|
|
module OneTimePad (main) where
|
|
|
|
import Control.Monad
|
|
import Data.Char
|
|
import Data.Function (on)
|
|
import qualified Data.Text as T
|
|
import qualified Data.Text.IO as TI
|
|
import Data.Time
|
|
import System.Console.GetOpt
|
|
import System.Environment
|
|
import System.Exit
|
|
import System.IO
|
|
|
|
-- Command-line options parsing
|
|
data Options = Options { optCommand :: String
|
|
, optInput :: IO T.Text
|
|
, optOutput :: T.Text -> IO ()
|
|
, optPad :: (IO T.Text, T.Text -> IO ())
|
|
, optLines :: Int
|
|
}
|
|
|
|
startOptions :: Options
|
|
startOptions = Options { optCommand = "decrypt"
|
|
, optInput = TI.getContents
|
|
, optOutput = TI.putStr
|
|
, optPad = (TI.getContents, TI.putStr)
|
|
, optLines = 0
|
|
}
|
|
|
|
options :: [ OptDescr (Options -> IO Options) ]
|
|
options =
|
|
[ Option "e" ["encrypt"]
|
|
(NoArg
|
|
(\opt -> return opt { optCommand = "encrypt" }))
|
|
"Encrypt file"
|
|
, Option "d" ["decrypt"]
|
|
(NoArg
|
|
(\opt -> return opt { optCommand = "decrypt" }))
|
|
"Decrypt file (default)"
|
|
, Option "g" ["generate"]
|
|
(NoArg
|
|
(\opt -> return opt { optCommand = "generate" }))
|
|
"Generate a one-time pad"
|
|
, Option "i" ["input"]
|
|
(ReqArg
|
|
(\arg opt -> return opt { optInput = TI.readFile arg })
|
|
"FILE")
|
|
"Input file (for decryption and encryption)"
|
|
, Option "o" ["output"]
|
|
(ReqArg
|
|
(\arg opt -> return opt { optOutput = TI.writeFile arg })
|
|
"FILE")
|
|
"Output file (for generation, decryption, and encryption)"
|
|
, Option "p" ["pad"]
|
|
(ReqArg
|
|
(\arg opt -> return opt { optPad = (TI.readFile arg,
|
|
TI.writeFile arg) })
|
|
"FILE")
|
|
"One-time pad to use (for decryption and encryption)"
|
|
, Option "l" ["lines"]
|
|
(ReqArg
|
|
(\arg opt -> return opt { optLines = read arg :: Int })
|
|
"LINES")
|
|
"New one-time pad's length (in lines of 48 characters) (for generation)"
|
|
, Option "V" ["version"]
|
|
(NoArg
|
|
(\_ -> do
|
|
hPutStrLn stderr "Version 0.01"
|
|
exitWith ExitSuccess))
|
|
"Print version"
|
|
, Option "h" ["help"]
|
|
(NoArg
|
|
(\_ -> do
|
|
prg <- getProgName
|
|
putStrLn "usage: OneTimePad [-h] [-V] [--lines LINES] [-i FILE] [-o FILE] [-p FILE] [--encrypt | --decrypt | --generate]"
|
|
hPutStrLn stderr (usageInfo prg options)
|
|
exitWith ExitSuccess))
|
|
"Show this help message and exit"
|
|
]
|
|
|
|
main :: IO ()
|
|
main = do
|
|
args <- getArgs
|
|
let (actions, nonOptions, errors) = getOpt RequireOrder options args
|
|
opts <- Prelude.foldl (>>=) (return startOptions) actions
|
|
let Options { optCommand = command
|
|
, optInput = input
|
|
, optOutput = output
|
|
, optPad = (inPad, outPad)
|
|
, optLines = linecnt } = opts
|
|
|
|
case command of
|
|
"generate" -> generate linecnt output
|
|
"encrypt" -> do
|
|
inputContents <- clean <$> input
|
|
padContents <- inPad
|
|
output $ format $ encrypt inputContents $ unformat $ T.concat
|
|
$ dropWhile (\t -> T.head t == '-' || T.head t == '#')
|
|
$ T.lines padContents
|
|
"decrypt" -> do
|
|
inputContents <- unformat <$> input
|
|
padContents <- inPad
|
|
output $ decrypt inputContents $ unformat $ T.concat
|
|
$ dropWhile (\t -> T.head t == '-' || T.head t == '#')
|
|
$ T.lines padContents
|
|
let discardLines = ceiling
|
|
$ ((/) `on` fromIntegral) (T.length inputContents) 48
|
|
outPad $ discard discardLines $ T.lines padContents
|
|
|
|
{- | Discard used pad lines. Is only called at decryption to enable using the
|
|
same pad file for both encryption and decryption.
|
|
-}
|
|
discard :: Int -> [T.Text] -> T.Text
|
|
discard 0 ts = T.unlines ts
|
|
discard x (t:ts) = if (T.head t == '-' || T.head t == '#')
|
|
then T.unlines [t, (discard x ts)]
|
|
else T.unlines [(T.append (T.pack "- ") t), (discard (x-1) ts)]
|
|
|
|
{- | Clean the text from symbols that cannot be encrypted.
|
|
-}
|
|
clean :: T.Text -> T.Text
|
|
clean = T.map toUpper . T.filter (\c -> let oc = ord c
|
|
in oc >= 65 && oc <= 122
|
|
&& (not $ oc >=91 && oc <= 96))
|
|
|
|
{- | Format text (usually encrypted text) for pretty-printing it in a similar
|
|
way to the example from Wikipedia (see Rosetta Code page for this task)
|
|
-}
|
|
format :: T.Text -> T.Text
|
|
format = T.unlines . map (T.intercalate (T.pack " ") . T.chunksOf 6)
|
|
. T.chunksOf 48
|
|
|
|
{- | Unformat encrypted text, getting rid of characters that are irrelevant for
|
|
decryption.
|
|
-}
|
|
unformat :: T.Text -> T.Text
|
|
unformat = T.filter (\c -> c/='\n' && c/=' ')
|
|
|
|
{- | Generate a one-time pad and write it to file (specified as second
|
|
parameter). Note: this only works on operating systems that have the
|
|
"/dev/random" file.
|
|
-}
|
|
generate :: Int -> (T.Text -> IO ()) -> IO ()
|
|
generate lines output = do
|
|
withBinaryFile "/dev/random" ReadMode
|
|
(\handle -> do
|
|
contents <- replicateM (48 * lines) $ hGetChar handle
|
|
time <- getCurrentTime
|
|
output
|
|
$ T.unlines [ T.pack
|
|
$ "# OTP pad, generated by https://github.com/kssytsrk/one-time-pad on "
|
|
++ show time
|
|
, format $ T.pack
|
|
$ map (chr . (65 +) . flip mod 26 . ord) contents
|
|
])
|
|
|
|
-- Helper function for encryption/decryption.
|
|
crypt :: (Int -> Int -> Int) -> T.Text -> T.Text -> T.Text
|
|
crypt f = T.zipWith ((chr .) . f `on` ord)
|
|
|
|
-- Encrypt first parameter's contents, using the second parameter as a key.
|
|
encrypt :: T.Text -> T.Text -> T.Text
|
|
encrypt = crypt ((((+65) . flip mod 26 . subtract 130) .) . (+))
|
|
|
|
-- Decrypt first parameter's contents, using the second parameter as a key.
|
|
decrypt :: T.Text -> T.Text -> T.Text
|
|
decrypt = crypt ((((+65) . flip mod 26) .) . (-))
|