Implement a One-time pad, for encrypting and decrypting messages.

To keep it simple, we will be using letters only.

Sub-Tasks

 Generate the data for a One-time pad (user needs to specify a filename and length)
 The important part is to get "true random" numbers, e.g. from /dev/random
 encryption / decryption ( basically the same operation, much like Rot-13 )
 For this step, much of Vigenère cipher could be reused,
with the key to be read from the file containing the One-time pad.
 optional: management of One-time pads: list, mark as used, delete, etc.
 Somehow, the users needs to keep track which pad to use for which partner.
To support the management of pad-files:

 Such files have a file-extension ".1tp"
 Lines starting with "#" may contain arbitary meta-data (i.e. comments)
 Lines starting with "-" count as "used"
 Whitespace within the otp-data is ignored


For example, here is the data from Wikipedia:

# Example data - Wikipedia - 2014-11-13
-ZDXWWW EJKAWO FECIFE WSNZIP PXPKIY URMZHI JZTLBC YLGDYJ 
-HTSVTV RRYYEG EXNCGA GGQVRF FHZCIB EWLGGR BZXQDQ DGGIAK 
 YHJYEQ TDLCQT HZBSIZ IRZDYS RBYJFZ AIRCWI UCVXTW YKPQMK 
 CKHVEX VXYVCS WOGAAZ OUVVON GCNEVR LMBLYB SBDCDC PCGVJX 
 QXAUIP PXZQIJ JIUWYH COVWMJ UZOJHL DWHPER UBSRUJ HGAAPR 
 CRWVHI FRNTQW AJVWRT ACAKRD OZKIIB VIQGBK IJCWHF GTTSSE 
 EXFIPJ KICASQ IOUQTP ZSGXGH YTYCTI BAZSTN JKMFXI RERYWE 



See also
 one time pad encryption in Python
 snapfractalpop - One-Time-Pad Command-Line-Utility (C).
 Crypt-OTP-2.00 on CPAN (Perl)


import os, re, sequtils, strformat, strutils
import nimcrypto
 
# One-time pad file signature.
const Magic = "#one-time pad"
 
# Suffix for pad files.
const Suffix = ".1tp"
 
proc log(msg: string) =
  ## Log a message.
  stderr.write msg
  stderr.write '\n'
 
proc makeKeys(n, size: Positive): seq[string] =
  ## Generate "n" secure, random keys of "size" bytes.
 
  # We're generating and storing keys in their hexadecimal form to make
  # one-time pad files a little more human readable and to ensure a key
  # can not start with a hyphen.
  var bytes = newSeq[byte](size)
  for _ in 1..n:
    if randomBytes(bytes) != size:
      raise newException(ValueError, "unable to build keys.")
    result.add bytes.mapIt(it.toHex).join()
 
proc makePad(name: string; padSize, keySize: Positive): string =
  ## Create a new one-time pad identified by the given name.
  ## Args:
  ##     name: unique one-time pad identifier.
  ##     padSize: the number of keys (or pages) in the pad.
  ##     keySize: the number of bytes per key.
  ## Returns:
  ##     the new one-time pad as a string.
 
  let pad = @[Magic, &"#name={name}", &"#size={padSize}"] & makeKeys(padSize, keySize)
  result = pad.join("\n")
 
proc `xor`(message, key: string): string =
  ## Return "message" XOR-ed with "key".
  ##
  ## Args:
  ##     message: plaintext or cyphertext to be encrypted or decrypted.
  ##     key: encryption and decryption key.
  ## Returns:
  ##     plaintext or cyphertext as a string.
 
  if key.len < message.len:
    quit("Key size is too short to encrypt/decrypt the message.", QuitFailure)
  result = newStringOfCap(message.len)
  var keyIndex = 0
  for msgChar in message:
    result.add chr(ord(msgChar) xor ord(key[keyIndex]))
    inc keyIndex
 
proc useKey(pad: var string): string =
  ## Use the next available key from the given one-time pad.
  ##
  ## Args:
  ##     pad: a one-time pad, updated.
  ## Returns:
  ##     the key.
 
  var matches: array[1, string]
  let pos = pad.find(re"(?m)(^[A-F0-9]+$)", matches)
  if pos < 0:
    quit("Pad is all used up.", QuitFailure)
 
  pad.insert("-", pos)
  result = matches[0]
 
proc writePad(path: string; padSize, keySize: Positive) =
  ## Write a new one-time pad to the given path.
  ##
  ## Args:
  ##     path: path to write one-time pad to.
  ##     padSize: the number of keys (or pages) in the pad.
  ##     keySize: the number of bytes per key.
 
  if fileExists(path):
    quit("Pad " & path & " already exists", QuitFailure)
  try:
    path.writeFile(makePad(path.extractFilename(), padSize, keySize))
  except IOError:
    quit("Unable to write file " & path, QuitFailure)
  log("New one-time pad written to " & path)
 
proc process(pad, message: string; outfile: File) =
  ## Encrypt or decrypt "message" using the given pad.
  ##
  ## Args:
  ##     pad: path to one-time pad.
  ##     message: plaintext or ciphertext message to encrypt or decrypt.
  ##     outfile: file-like object to write to.
 
  if not fileExists(pad):
    quit("No such pad: " & pad, QuitFailure)
  let start = pad.readLines(1)
  if start.len == 0 or start[0] != Magic:
    quit(&"file '{pad}' does not look like a one-time pad", QuitFailure)
 
  # Rewrites the entire one-time pad every time
  var padData = pad.readFile()
  let key = padData.useKey().parseHexStr()
  pad.writeFile(padData)
 
  outfile.write(message xor key)
 
 
when isMainModule:
 
  import parseopt
 
  proc printUsage() =
    echo "Usage: ", getAppFilename().lastPathPart,
         " [-h] [--length LENGTH] [--key-size KEY_SIZE] [-o OUTFILE]"
    echo "                    [--encrypt FILE | --decrypt FILE] pad"
    echo ""
    echo """One-time pad.
 
positional arguments:
  pad                   Path to one-time pad. If neither --encrypt or --decrypt
                        are given, will create a new pad.
 
optional arguments:
  -h, --help            show this help message and exit
  --length LENGTH       Pad size. Ignored if --encrypt or --decrypt are given.
                        Defaults to 10.
  --key-size KEY_SIZE   Key size in bytes. Ignored if --encrypt or --decrypt
                        are given. Defaults to 64.
  --outfile OUTFILE     Write encoded/decoded message to a file. Ignored if
                        --encrypt or --decrypt is not given. Defaults to
                        stdout.
  --encrypt FILE        Encrypt FILE using the next available key from pad.
  --decrypt FILE        Decrypt FILE using the next available key from pad.
  """
 
  var
    parser = initOptParser(shortNoVal = {'h'}, longNoval = @["help"])
    padPath: string
    length = 10
    outpath = ""
    encryptPath = ""
    decryptPath = ""
    keySize = 64
    encrypt = false
    decrypt = false
 
  for kind, key, val in parser.getopt():
    case kind
 
    of cmdShortOption:
      printUsage()
      if key != "h":
        quit("Wrong option: " & key, QuitFailure)
      elif val.len != 0:
        quit("Wrong value for option -h", QuitFailure)
      else:
        quit(QuitSuccess)
 
    of cmdLongOption:
      case key
 
      of "help":
        printUsage()
        quit(QuitSuccess)
 
      of "length":
        try:
          length = parseInt(val)
          if length < 2: raise newException(ValueError, "")
        except ValueError:
          quit("Wrong length: " & val, QuitFailure)
 
      of "encrypt":
        encryptPath = val
        encrypt = true
 
      of "decrypt":
        decryptPath = val
        decrypt = true
 
      of "outfile":
        outPath = val
 
      of "key-size":
        try:
          keySize = parseInt(val)
          if length < 2: raise newException(ValueError, "")
        except ValueError:
          quit("Wrong key size: " & val, QuitFailure)
 
      else:
        quit("Invalid option: " & key, QuitFailure)
 
    of cmdArgument:
      padPath = if key.endsWith(Suffix): key else: key & Suffix
 
    of cmdEnd:
      discard   # Cannot not occur.
 
  if padPath.len == 0:
    quit("Missing pad file.", QuitFailure)
 
  if encrypt and decrypt:
    quit("Incompatible options: encrypt and decrypt", QuitFailure)
 
  if encrypt or decrypt:
 
    var outfile: File
    if outpath.len == 0:
      outfile = stdout
    else:
      try:
        outfile = outpath.open(fmWrite)
      except IOError:
        quit("Unable to open output file.", QuitFailure)
 
    let message = try:
                    if encrypt: encryptPath.readFile()
                    else: decryptPath.readFile()
                  except IOError:
                    quit("Unable to open file to encrypt or decrypt.", QuitFailure)
 
    padPath.process(message, outfile)
    if outfile != stdout: outfile.close()
 
  else:
    padPath.writePad(length, keySize)