One-time pad

From Rosetta Code
Task
One-time pad
You are encouraged to solve this task according to the task description, using any language you may know.

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



Go

Translation of: Kotlin
package main

import (
    "bufio"
    "crypto/rand"
    "fmt"
    "io/ioutil"
    "log"
    "math/big"
    "os"
    "strconv"
    "strings"
    "unicode"
)

const (
    charsPerLine = 48
    chunkSize    = 6
    cols         = 8
    demo         = true // would normally be set to false
)

type fileType int

const (
    otp fileType = iota
    enc
    dec
)

var scnr = bufio.NewScanner(os.Stdin)

func check(err error) {
    if err != nil {
        log.Fatal(err)
    }
}

func toAlpha(s string) string {
    var filtered []rune
    for _, r := range s {
        if unicode.IsUpper(r) {
            filtered = append(filtered, r)
        }
    }
    return string(filtered)
}

func isOtpRelated(s string) bool {
    return strings.HasSuffix(s, ".1tp") || strings.HasSuffix(s, "1tp_cpy") ||
        strings.HasSuffix(s, ".1tp_enc") || strings.HasSuffix(s, "1tp_dec")
}

func makePad(nLines int) string {
    nChars := nLines * charsPerLine
    bytes := make([]byte, nChars)
    /* generate random upper case letters */
    max := big.NewInt(26)
    for i := 0; i < nChars; i++ {
        n, err := rand.Int(rand.Reader, max)
        check(err)
        bytes[i] = byte(65 + n.Uint64())
    }
    return inChunks(string(bytes), nLines, otp)
}

func vigenere(text, key string, encrypt bool) string {
    bytes := make([]byte, len(text))
    var ci byte
    for i, c := range text {
        if encrypt {
            ci = (byte(c) + key[i] - 130) % 26
        } else {
            ci = (byte(c) + 26 - key[i]) % 26
        }
        bytes[i] = ci + 65
    }
    temp := len(bytes) % charsPerLine
    if temp > 0 { // pad with random characters so each line is a full one
        max := big.NewInt(26)
        for i := temp; i < charsPerLine; i++ {
            n, err := rand.Int(rand.Reader, max)
            check(err)
            bytes = append(bytes, byte(65+n.Uint64()))
        }
    }
    ft := enc
    if !encrypt {
        ft = dec
    }
    return inChunks(string(bytes), len(bytes)/charsPerLine, ft)
}

func inChunks(s string, nLines int, ft fileType) string {
    nChunks := len(s) / chunkSize
    remainder := len(s) % chunkSize
    chunks := make([]string, nChunks)
    for i := 0; i < nChunks; i++ {
        chunks[i] = s[i*chunkSize : (i+1)*chunkSize]
    }
    if remainder > 0 {
        chunks = append(chunks, s[nChunks*chunkSize:])
    }
    var sb strings.Builder
    for i := 0; i < nLines; i++ {
        j := i * cols
        sb.WriteString(" " + strings.Join(chunks[j:j+cols], " ") + "\n")
    }
    ss := " file\n" + sb.String()
    switch ft {
    case otp:
        return "# OTP" + ss
    case enc:
        return "# Encrypted" + ss
    default: // case dec:
        return "# Decrypted" + ss
    }
}

func menu() int {
    fmt.Println(`
1. Create one time pad file.

2. Delete one time pad file.

3. List one time pad files.

4. Encrypt plain text.

5. Decrypt cipher text.

6. Quit program.
`)
    choice := 0
    for choice < 1 || choice > 6 {
        fmt.Print("Your choice (1 to 6) : ")
        scnr.Scan()
        choice, _ = strconv.Atoi(scnr.Text())
        check(scnr.Err())
    }
    return choice
}

func main() {
    for {
        choice := menu()
        fmt.Println()
        switch choice {
        case 1: // Create OTP
            fmt.Println("Note that encrypted lines always contain 48 characters.\n")
            fmt.Print("OTP file name to create (without extension) : ")
            scnr.Scan()
            fileName := scnr.Text() + ".1tp"
            nLines := 0
            for nLines < 1 || nLines > 1000 {
                fmt.Print("Number of lines in OTP (max 1000) : ")
                scnr.Scan()
                nLines, _ = strconv.Atoi(scnr.Text())
            }
            check(scnr.Err())
            key := makePad(nLines)
            file, err := os.Create(fileName)
            check(err)
            _, err = file.WriteString(key)
            check(err)
            file.Close()
            fmt.Printf("\n'%s' has been created in the current directory.\n", fileName)
            if demo {
                // a copy of the OTP file would normally be on a different machine
                fileName2 := fileName + "_cpy" // copy for decryption
                file, err := os.Create(fileName2)
                check(err)
                _, err = file.WriteString(key)
                check(err)
                file.Close()
                fmt.Printf("'%s' has been created in the current directory.\n", fileName2)
                fmt.Println("\nThe contents of these files are :\n")
                fmt.Println(key)
            }
        case 2: // Delete OTP
            fmt.Println("Note that this will also delete ALL associated files.\n")
            fmt.Print("OTP file name to delete (without extension) : ")
            scnr.Scan()
            toDelete1 := scnr.Text() + ".1tp"
            check(scnr.Err())
            toDelete2 := toDelete1 + "_cpy"
            toDelete3 := toDelete1 + "_enc"
            toDelete4 := toDelete1 + "_dec"
            allToDelete := []string{toDelete1, toDelete2, toDelete3, toDelete4}
            deleted := 0
            fmt.Println()
            for _, name := range allToDelete {
                if _, err := os.Stat(name); !os.IsNotExist(err) {
                    err = os.Remove(name)
                    check(err)
                    deleted++
                    fmt.Printf("'%s' has been deleted from the current directory.\n", name)
                }
            }
            if deleted == 0 {
                fmt.Println("There are no files to delete.")
            }
        case 3: // List OTPs
            fmt.Println("The OTP (and related) files in the current directory are:\n")
            files, err := ioutil.ReadDir(".") // already sorted by file name
            check(err)
            for _, fi := range files {
                name := fi.Name()
                if !fi.IsDir() && isOtpRelated(name) {
                    fmt.Println(name)
                }
            }
        case 4: // Encrypt
            fmt.Print("OTP file name to use (without extension) : ")
            scnr.Scan()
            keyFile := scnr.Text() + ".1tp"
            if _, err := os.Stat(keyFile); !os.IsNotExist(err) {
                file, err := os.Open(keyFile)
                check(err)
                bytes, err := ioutil.ReadAll(file)
                check(err)
                file.Close()
                lines := strings.Split(string(bytes), "\n")
                le := len(lines)
                first := le
                for i := 0; i < le; i++ {
                    if strings.HasPrefix(lines[i], " ") {
                        first = i
                        break
                    }
                }
                if first == le {
                    fmt.Println("\nThat file has no unused lines.")
                    continue
                }
                lines2 := lines[first:] // get rid of comments and used lines

                fmt.Println("Text to encrypt :-\n")
                scnr.Scan()
                text := toAlpha(strings.ToUpper(scnr.Text()))
                check(scnr.Err())
                tl := len(text)
                nLines := tl / charsPerLine
                if tl%charsPerLine > 0 {
                    nLines++
                }
                if len(lines2) >= nLines {
                    key := toAlpha(strings.Join(lines2[0:nLines], ""))
                    encrypted := vigenere(text, key, true)
                    encFile := keyFile + "_enc"
                    file2, err := os.Create(encFile)
                    check(err)
                    _, err = file2.WriteString(encrypted)
                    check(err)
                    file2.Close()
                    fmt.Printf("\n'%s' has been created in the current directory.\n", encFile)
                    for i := first; i < first+nLines; i++ {
                        lines[i] = "-" + lines[i][1:]
                    }
                    file3, err := os.Create(keyFile)
                    check(err)
                    _, err = file3.WriteString(strings.Join(lines, "\n"))
                    check(err)
                    file3.Close()
                    if demo {
                        fmt.Println("\nThe contents of the encrypted file are :\n")
                        fmt.Println(encrypted)
                    }
                } else {
                    fmt.Println("Not enough lines left in that file to do encryption.")
                }
            } else {
                fmt.Println("\nThat file does not exist.")
            }
        case 5: // Decrypt
            fmt.Print("OTP file name to use (without extension) : ")
            scnr.Scan()
            keyFile := scnr.Text() + ".1tp_cpy"
            check(scnr.Err())
            if _, err := os.Stat(keyFile); !os.IsNotExist(err) {
                file, err := os.Open(keyFile)
                check(err)
                bytes, err := ioutil.ReadAll(file)
                check(err)
                file.Close()
                keyLines := strings.Split(string(bytes), "\n")
                le := len(keyLines)
                first := le
                for i := 0; i < le; i++ {
                    if strings.HasPrefix(keyLines[i], " ") {
                        first = i
                        break
                    }
                }
                if first == le {
                    fmt.Println("\nThat file has no unused lines.")
                    continue
                }
                keyLines2 := keyLines[first:] // get rid of comments and used lines

                encFile := keyFile[0:len(keyFile)-3] + "enc"
                if _, err := os.Stat(encFile); !os.IsNotExist(err) {
                    file2, err := os.Open(encFile)
                    check(err)
                    bytes, err := ioutil.ReadAll(file2)
                    check(err)
                    file2.Close()
                    encLines := strings.Split(string(bytes), "\n")[1:] // exclude comment line
                    nLines := len(encLines)
                    if len(keyLines2) >= nLines {
                        encrypted := toAlpha(strings.Join(encLines, ""))
                        key := toAlpha(strings.Join(keyLines2[0:nLines], ""))
                        decrypted := vigenere(encrypted, key, false)
                        decFile := keyFile[0:len(keyFile)-3] + "dec"
                        file3, err := os.Create(decFile)
                        check(err)
                        _, err = file3.WriteString(decrypted)
                        check(err)
                        file3.Close()
                        fmt.Printf("\n'%s' has been created in the current directory.\n", decFile)
                        for i := first; i < first+nLines; i++ {
                            keyLines[i] = "-" + keyLines[i][1:]
                        }
                        file4, err := os.Create(keyFile)
                        check(err)
                        _, err = file4.WriteString(strings.Join(keyLines, "\n"))
                        check(err)
                        file4.Close()
                        if demo {
                            fmt.Println("\nThe contents of the decrypted file are :\n")
                            fmt.Println(decrypted)
                        }
                    }
                } else {
                    fmt.Println("Not enough lines left in that file to do decryption.")
                }
            } else {
                fmt.Println("\nThat file does not exist.")
            }
        case 6: // Quit program
            return
        }
    }
}
Output:
Similar (not exactly the same, of course) as the Kotlin sample session.

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) .) . (-))
Output:

Help message:

$ ./OneTimePad --help
usage: OneTimePad [-h] [-V] [--lines LINES] [-i FILE] [-o FILE] [-p FILE] [--encrypt | --decrypt | --generate]
OneTimePad
  -e        --encrypt      Encrypt file
  -d        --decrypt      Decrypt file (default)
  -g        --generate     Generate a one-time pad
  -i FILE   --input=FILE   Input file (for decryption and encryption)
  -o FILE   --output=FILE  Output file (for generation, decryption, and encryption)
  -p FILE   --pad=FILE     One-time pad to use (for decryption and encryption)
  -l LINES  --lines=LINES  New one-time pad's length (in lines of 48 characters) (for generation)
  -V        --version      Print version
  -h        --help         Show this help message and exit

Generate a pad, outputting into test-pad.1tp file:

$ ./OneTimePad -g -o test-pad.1tp -l 2

Encrypt a message using test-pad.1tp and jabberwock.txt (contents of jabberwock.txt: "Beware the Jabberwock, my son! The jaws that bite, the claws that catch!"), outputting the result into encrypted.txt:

$ ./OneTimePad -e -i jabberwock.txt -o encrypted.txt -p test-pad.1tp 

Decrypt the message, outputting the result into decrypted.txt:

$ ./OneTimePad -d -i encrypted.txt -o decrypted.txt -p test-pad.1tp 

Contents of test-pad.1tp:

# OTP pad, generated by https://github.com/kssytsrk/one-time-pad on 2021-08-08 06:53:52.91928977 UTC
- XAKUVI GLSWZB TEQCIR GNPHNR FBAPXQ IKXBCX BAAQIK OTTCQR
- YCLYOE YTPFWC SBVHSL MTEKHN QYOVMN GXWTUV VHQWUE DSWVJJ

Contents of encrypted.txt:

YEGUMM ZSWFZC UIHYWT QZNZBE YIEYXM ADEBVY JTEJPO QETYIK
FCEAOX AA

Contents of decrypted.txt:

BEWARETHEJABBERWOCKMYSONTHEJAWSTHATBITETHECLAWSTHATCATCH

J

Implementation (assumes linux and also uses ascii85 encoding to avoid character set problems):

require'convert/misc/vig convert/misc/ascii85'
randseq=: {{ 2!:0'dd 2>/dev/null if=/dev/urandom count=1 bs=',":y }}
genpad=: {{ EMPTY [ (randseq y) fwrite x }}
encrypt=: {{ toascii85 (fread x) 0 vig a. y }}
decrypt=: {{ (fread x) 1 vig a. fromascii85 y }}

Example:

   'example' genpad 1000
   'example' encrypt 'this is a test'
hc>*>I2nEB,6b#MdE;~>

   'example' decrypt 'hc>*>I2nEB,6b#MdE;~>'
this is a test

One time pads are files, and can managed by linux file utilities (ls, mv, rm, ...).

Java

Implementation supports multiple pads using the control name.

Implementation also includes the start and end ASCII characters. One usage shows support for uppercase only, a second usage shows allowing spaces, upper case, and lower case in the input text.

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.Random;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class OneTimePad {

    public static void main(String[] args) {
        String controlName = "AtomicBlonde";
        generatePad(controlName, 5, 60, 65, 90);
        String text = "IT WAS THE BEST OF TIMES IT WAS THE WORST OF TIMES";
        String encrypted = parse(true, controlName, text.replaceAll(" ", ""));
        String decrypted = parse(false, controlName, encrypted);
        System.out.println("Input  text    = " + text);
        System.out.println("Encrypted text = " + encrypted);
        System.out.println("Decrypted text = " + decrypted);

        controlName = "AtomicBlondeCaseSensitive";
        generatePad(controlName, 5, 60, 32, 126);
        text = "It was the best of times, it was the worst of times.";
        encrypted = parse(true, controlName, text);
        decrypted = parse(false, controlName, encrypted);
        System.out.println();
        System.out.println("Input text     = " + text);
        System.out.println("Encrypted text = " + encrypted);
        System.out.println("Decrypted text = " + decrypted);
    }
    
    private static String parse(boolean encryptText, String controlName, String text) {
        StringBuilder sb = new StringBuilder();
        int minCh = 0;
        int maxCh = 0;
        Pattern minChPattern = Pattern.compile("^#  MIN_CH = ([\\d]+)$");
        Pattern maxChPattern = Pattern.compile("^#  MAX_CH = ([\\d]+)$");
        boolean validated = false;
        try (BufferedReader in = new BufferedReader(new FileReader(getFileName(controlName))); ) {
            String inLine = null;
            while ( (inLine = in.readLine()) != null ) {
                Matcher minMatcher = minChPattern.matcher(inLine);
                if ( minMatcher.matches() ) {
                    minCh = Integer.parseInt(minMatcher.group(1));
                    continue;
                }
                Matcher maxMatcher = maxChPattern.matcher(inLine);
                if ( maxMatcher.matches() ) {
                    maxCh = Integer.parseInt(maxMatcher.group(1));
                    continue;
                }
                if ( ! validated && minCh > 0 && maxCh > 0 ) {
                    validateText(text, minCh, maxCh);
                    validated = true;
                }
                //  # is comment.  - is used key. 
                if ( inLine.startsWith("#") || inLine.startsWith("-") ) {
                    continue;
                }
                //  Have encryption key.
                String key = inLine;
                if ( encryptText ) {
                    for ( int i = 0 ; i < text.length(); i++) {
                        sb.append((char) (((text.charAt(i) - minCh + key.charAt(i) - minCh) % (maxCh - minCh + 1)) + minCh));
                    }
                }
                else {
                    for ( int i = 0 ; i < text.length(); i++) {
                        int decrypt = text.charAt(i) - key.charAt(i);
                        if ( decrypt < 0 ) {
                            decrypt += maxCh - minCh + 1;
                        }
                        decrypt += minCh;
                        sb.append((char) decrypt);
                    }
                }
                break;
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return sb.toString();
    }

    private static void validateText(String text, int minCh, int maxCh) {
        //  Validate text is in range
        for ( char ch : text.toCharArray() ) {
            if ( ch != ' ' && (ch < minCh || ch > maxCh) ) {
                throw new IllegalArgumentException("ERROR 103:  Invalid text.");
            }
        }
        
    }
    
    private static String getFileName(String controlName) {
        return controlName + ".1tp";
    }
    
    private static void generatePad(String controlName, int keys, int keyLength, int minCh, int maxCh) {
        Random random = new Random();
        try ( BufferedWriter writer = new BufferedWriter(new FileWriter(getFileName(controlName), false)); ) {
            writer.write("#  Lines starting with '#' are ignored.");
            writer.newLine();
            writer.write("#  Lines starting with '-' are previously used.");
            writer.newLine();
            writer.write("#  MIN_CH = " + minCh);
            writer.newLine();
            writer.write("#  MAX_CH = " + maxCh);
            writer.newLine();
            for ( int line = 0 ; line < keys ; line++ ) {
                StringBuilder sb = new StringBuilder();
                for ( int ch = 0 ; ch < keyLength ; ch++ ) {
                    sb.append((char) (random.nextInt(maxCh - minCh + 1) + minCh));
                }
                writer.write(sb.toString());
                writer.newLine();
            }
            writer.write("#  EOF");
            writer.newLine();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

}
Output:

Demonstration of job:

Input  text    = IT WAS THE BEST OF TIMES IT WAS THE WORST OF TIMES
Encrypted text = ANFNPULTOZRSUNBACQXBMUVIGNWSRJFFNCBVCNF
Decrypted text = ITWASTHEBESTOFTIMESITWASTHEWORSTOFTIMES

Input text     = It was the best of times, it was the worst of times.
Encrypted text = jF,2abB`:3f9}@85"Y.k[JyWRNO\(q~0Z-5_P:E_H3Q0U(2>'eWy
Decrypted text = It was the best of times, it was the worst of times.

Generated one time pad supporting uppercase only.

#  Lines starting with '#' are ignored.
#  Lines starting with '-' are previously used.
#  MIN_CH = 65
#  MAX_CH = 90
SUJNXBEPNVZZGIISQMFTTYVQNGSWDSNMZXINQJNEQHFBLCSZFCHJGKDLPQPN
RQMZKUCZQZXQDTKXKSVRCFNZYTGZWCDPPMCHDHORVFWBLXJLWTJRBRGUOYWM
PKEVUUBDOMBSFXVJFWGORCZPBSQOWZEOFFHZARVJWTEUQUAKGTMHFAVZSVOO
DKNEKKCTQITFVSOWKMXAOQKWTUYOUXICSEWYCPGOPWFKHYZXQXOIVQUZWWZB
NQUICNAJLTKDFSPXVBIVUUYUTZNNRNBQQXAGPERXZQNQLHEUXSRNIADKXZCL
#  EOF

Generated one time pad supporting lowercase, uppercase, symbols, and spaces.

#  Lines starting with '#' are ignored.
#  Lines starting with '-' are previously used.
#  MIN_CH = 32
#  MAX_CH = 126
AQ,: nBkQMfV8LC52r.vq\4cFNeg(y=<Z8LyPBUlT>Q@n(=T9 ckJJt6c59[
A{qjk{}x?vlu5(/I4IrkC0NJS2}?_a xF__;5V& -Nz7qh@7OD}l<u9~xGMZ
6JOW|8cZ<,O:vp*LMwU~5u@AJ5nVG7 x?_zfyY(>QMA|U37Z39B}M-lnI-GB
|Qe1*V^PHo"\M:YQo1Z$}2XUk[,L(<P+RDso>'LE.>#K)I6IvrJ&l}O<~=C^
rx0ubl%t(-tOnCQz_y5k%xlHHi$cvaRCIYD<5nUFWD5Hmd@K2.{/N^>PP.<I
#  EOF

Julia

See One-time pad/Julia

Kotlin

This uses the JDK's SecureRandom class for generating cryptographically strong random numbers. For convenience all three sub-tasks are catered for by a single, menu-based, program.

// version 1.2.31

import java.io.File
import java.security.SecureRandom

const val CHARS_PER_LINE = 48
const val CHUNK_SIZE = 6
const val COLS = 8
const val DEMO = true  // would normally be set to false

enum class FileType { OTP, ENC, DEC }

fun Char.isAlpha() = this in 'A'..'Z'

fun String.toAlpha() = this.filter { it.isAlpha() }

fun String.isOtpRelated() = endsWith(".1tp") || endsWith(".1tp_cpy") ||
                            endsWith(".1tp_enc") || endsWith(".1tp_dec")

fun makePad(nLines: Int): String {
    val nChars = nLines * CHARS_PER_LINE
    val sr = SecureRandom()
    val sb = StringBuilder(nChars)
    /* generate random upper case letters */
    for (i in 0 until nChars) sb.append((sr.nextInt(26) + 65).toChar())
    return sb.toString().inChunks(nLines, FileType.OTP)
}

fun vigenere(text: String, key: String, encrypt: Boolean = true): String {
    val sb = StringBuilder(text.length)
    for ((i, c) in text.withIndex()) {
        val ci = if (encrypt)
            (c.toInt() + key[i].toInt() - 130) % 26
        else
            (c.toInt() - key[i].toInt() +  26) % 26
        sb.append((ci + 65).toChar())
    }
    val temp = sb.length % CHARS_PER_LINE
    if (temp > 0) {  // pad with random characters so each line is a full one
        val sr = SecureRandom()
        for (i in temp until CHARS_PER_LINE) sb.append((sr.nextInt(26) + 65).toChar())
    }
    val ft = if (encrypt) FileType.ENC else FileType.DEC
    return sb.toString().inChunks(sb.length / CHARS_PER_LINE, ft)
}

fun String.inChunks(nLines: Int, ft: FileType): String {
    val chunks = this.chunked(CHUNK_SIZE)
    val sb = StringBuilder(this.length + nLines * (COLS + 1))
    for (i in 0 until nLines) {
        val j = i * COLS
        sb.append(" ${chunks.subList(j, j + COLS).joinToString(" ")}\n")
    }
    val s = " file\n" + sb.toString()
    return when (ft) {
        FileType.OTP -> "# OTP" + s
        FileType.ENC -> "# Encrypted" + s
        FileType.DEC -> "# Decrypted" + s
    }
}

fun menu(): Int {
    println("""
        |
        |1. Create one time pad file.
        |
        |2. Delete one time pad file.
        |
        |3. List one time pad files.
        |
        |4. Encrypt plain text.
        |
        |5. Decrypt cipher text.
        |
        |6. Quit program.
        |
        """.trimMargin())
    var choice: Int?
    do {
        print("Your choice (1 to 6) : ")
        choice = readLine()!!.toIntOrNull()
    }
    while (choice == null || choice !in 1..6)
    return choice
}

fun main(args: Array<String>) {
    mainLoop@ while (true) {
        val choice = menu()
        println()
        when (choice) {
            1 -> {  // Create OTP
                println("Note that encrypted lines always contain 48 characters.\n")
                print("OTP file name to create (without extension) : ")
                val fileName = readLine()!! + ".1tp"  
                var nLines: Int?

                do {
                    print("Number of lines in OTP (max 1000) : ")
                    nLines = readLine()!!.toIntOrNull()
                }
                while (nLines == null || nLines !in 1..1000)

                val key = makePad(nLines)
                File(fileName).writeText(key)
                println("\n'$fileName' has been created in the current directory.")
                if (DEMO) {
                    // a copy of the OTP file would normally be on a different machine
                    val fileName2 = fileName + "_cpy"  // copy for decryption
                    File(fileName2).writeText(key)
                    println("'$fileName2' has been created in the current directory.")
                    println("\nThe contents of these files are :\n")
                    println(key)
                }
            }

            2 -> {  // Delete OTP
                println("Note that this will also delete ALL associated files.\n")
                print("OTP file name to delete (without extension) : ")
                val toDelete1 = readLine()!! + ".1tp"
                val toDelete2 = toDelete1 + "_cpy"
                val toDelete3 = toDelete1 + "_enc"
                val toDelete4 = toDelete1 + "_dec"
                val allToDelete = listOf(toDelete1, toDelete2, toDelete3, toDelete4)
                var deleted = 0
                println()
                for (name in allToDelete) {
                    val f = File(name)
                    if (f.exists()) {
                        f.delete()
                        deleted++
                        println("'$name' has been deleted from the current directory.")
                    }
                }
                if (deleted == 0) println("There are no files to delete.")
            }

            3 -> {  // List OTPs
                println("The OTP (and related) files in the current directory are:\n")
                val otpFiles = File(".").listFiles().filter {
                    it.isFile() && it.name.isOtpRelated()
                }.map { it.name }.toMutableList()
                otpFiles.sort()
                println(otpFiles.joinToString("\n"))
            }

            4 -> {  // Encrypt
                print("OTP file name to use (without extension) : ")
                val keyFile = readLine()!! + ".1tp"
                val kf = File(keyFile)
                if (kf.exists()) {
                    val lines = File(keyFile).readLines().toMutableList()
                    var first = lines.size
                    for (i in 0 until lines.size) {
                        if (lines[i].startsWith(" ")) {
                            first = i
                            break
                        }
                    }
                    if (first == lines.size) {
                        println("\nThat file has no unused lines.")
                        continue@mainLoop
                    }
                    val lines2 = lines.drop(first)  // get rid of comments and used lines

                    println("Text to encrypt :-\n")
                    val text = readLine()!!.toUpperCase().toAlpha()
                    val len = text.length
                    var nLines = len / CHARS_PER_LINE
                    if (len % CHARS_PER_LINE > 0) nLines++

                    if (lines2.size >= nLines) {
                        val key = lines2.take(nLines).joinToString("").toAlpha()
                        val encrypted = vigenere(text, key)
                        val encFile = keyFile + "_enc"
                        File(encFile).writeText(encrypted)
                        println("\n'$encFile' has been created in the current directory.")
                        for (i in first until first + nLines) {
                            lines[i] = "-" + lines[i].drop(1)
                        }
                        File(keyFile).writeText(lines.joinToString("\n"))
                        if (DEMO) {
                            println("\nThe contents of the encrypted file are :\n")
                            println(encrypted)
                        }
                    }
                    else println("Not enough lines left in that file to do encryption")
                }
                else println("\nThat file does not exist.")
            }

            5 -> {  // Decrypt
                print("OTP file name to use (without extension) : ")
                val keyFile = readLine()!! + ".1tp_cpy"
                val kf = File(keyFile)
                if (kf.exists()) {
                    val keyLines = File(keyFile).readLines().toMutableList()
                    var first = keyLines.size
                    for (i in 0 until keyLines.size) {
                        if (keyLines[i].startsWith(" ")) {
                            first = i
                            break
                        }
                    }
                    if (first == keyLines.size) {
                        println("\nThat file has no unused lines.")
                        continue@mainLoop
                    }
                    val keyLines2 = keyLines.drop(first)  // get rid of comments and used lines

                    val encFile = keyFile.dropLast(3) + "enc"
                    val ef = File(encFile)
                    if (ef.exists()) {
                        val encLines = File(encFile).readLines().drop(1)  // exclude comment line
                        val nLines = encLines.size
                        if (keyLines2.size >= nLines) {
                            val encrypted = encLines.joinToString("").toAlpha()
                            val key = keyLines2.take(nLines).joinToString("").toAlpha()
                            val decrypted = vigenere(encrypted, key, false)
                            val decFile = keyFile.dropLast(3) + "dec"
                            File(decFile).writeText(decrypted)
                            println("\n'$decFile' has been created in the current directory.")
                            for (i in first until first + nLines) {
                                keyLines[i] = "-" + keyLines[i].drop(1)
                            }
                            File(keyFile).writeText(keyLines.joinToString("\n"))
                            if (DEMO) {
                                println("\nThe contents of the decrypted file are :\n")
                                println(decrypted)
                            }
                        }
                        else println("Not enough lines left in that file to do decryption")
                    }
                    else println("\n'$encFile' is missing.")
                }
                else println("\nThat file does not exist.")
            }

            else -> return  // Quit
        }
    }
}
Output:

Input/output for a sample session. In the interests of brevity, --<menu>-- indicates the re-display of the menu after the previous choice has been processed:

1. Create one time pad file.

2. Delete one time pad file.

3. List one time pad files.

4. Encrypt plain text.

5. Decrypt cipher text.

6. Quit program.

Your choice (1 to 6) : 1

Note that encrypted lines always contain 48 characters.

OTP file name to create (without extension) : user1
Number of lines in OTP (max 1000) : 4

'user1.1tp' has been created in the current directory.
'user1.1tp_cpy' has been created in the current directory.

The contents of these files are :

# OTP file
 MZSAVH VHJLRS YLFXZB JWAHSN AHPCSE RUJMFX ZZPMFB QTTNLM
 LDANXY LGJEBV IRKDTR NHKOPG ZDUBDC KOOWZC HEBKWC YAHOFY
 DYPCXG TGCBXC VFETFM VOTHNI ZCGKNL QONXYE IDCROZ LHLWMN
 YWSGDO YYSNNV QMOJFM AHVOGP CDFAKM HZCWEX CEAXXV INKAEO


--<menu>--

Your choice (1 to 6) : 3

The OTP (and related) files in the current directory are:

user1.1tp
user1.1tp_cpy

--<menu>--

Your choice (1 to 6) : 4

OTP file name to use (without extension) : user1
Text to encrypt :-

Beware the Jabberwock, my son! The jaws that bite, the claws that catch!

'user1.1tp_enc' has been created in the current directory.

The contents of the encrypted file are :

# Encrypted file
 NDOAML OONURT ZPWTND TIYZGA TOTLSA JNQMYY HSTFMF SETJDF
 SDTPXR NNHVAO YSCVZU UTLAFM PEWGRH OJRCTF IOVEHK ZKETBQ


--<menu>--

Your choice (1 to 6) : 5

OTP file name to use (without extension) : user1

'user1.1tp_dec' has been created in the current directory.

The contents of the decrypted file are :

# Decrypted file
 BEWARE THEJAB BERWOC KMYSON THEJAW STHATB ITETHE CLAWST
 HATCAT CHYRZT QBSSGD HMBMQG QBCFOF EVDGUD BKUULI BKXFWS


--<menu>-

Your choice (1 to 6) : 2

Note that this will also delete ALL associated files.

OTP file name to delete (without extension) : user1

'user1.1tp' has been deleted from the current directory.
'user1.1tp_cpy' has been deleted from the current directory.
'user1.1tp_enc' has been deleted from the current directory.
'user1.1tp_dec' has been deleted from the current directory.

--<menu>--

Your choice (1 to 6) : 6

Nim

Translation of: Python
Library: nimcrypto
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)
Output:

Show the help message and exit:

$ ./one_time_pad --help
Usage: one_time_pad [-h] [--length LENGTH] [--key-size KEY_SIZE] [-o OUTFILE]
                    [--encrypt FILE | --decrypt FILE] pad

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.

Create a new one-time pad in the current working directory:

$ ./one_time_pad --length 5 --key-size 128 mypad
New one-time pad written to mypad.1tp
$ head mypad.1tp
#one-time pad
#name=mypad.1tp
#size=5
73BA79D1E6E153...

Distribute our new one-time pad and use it to encrypt a message:

$ cp mypad.1tp shared.1tp
$ echo "like all shared secrets, the pad has to be at least as long as the message" > message.txt
$ ./one_time_pad --encrypt message.txt --outfile secret.dat mypad

Decrypt the message using the shared copy of the one-time pad:

$ ./one_time_pad --decrypt secret.dat --outfile plaintext.txt shared.1tp
$ cat plaintext.txt
like all shared secrets, the pad has to be at least as long as the message

Perl

# 20200814 added Perl programming solution

use strict;
use warnings;

use Crypt::OTP;
use Bytes::Random::Secure qw( random_bytes );

print "Message     : ", my $message = "show me the monKey", "\n";

my $otp = random_bytes(length $message);
print "Ord(OTP)    : ", ( map { ord($_).' ' } (split //, $otp)   ) , "\n";

my $cipher = OTP( $otp, $message, 1 );
print "Ord(Cipher) : ", ( map { ord($_).' ' } (split //, $cipher) ) , "\n";

print "Decoded     : ",  OTP( $otp, $cipher, 1 ), "\n";
Output:
Message     : show me the monKey
Ord(OTP)    : 133 61 239 52 182 60 169 161 33 153 142 106 128 43 55 44 224 7
Ord(Cipher) : 246 85 128 67 150 81 204 129 85 241 235 74 237 68 89 103 133 126
Decoded     : show me the monKey

Phix

See One-time pad/Phix

Python

"""One-time pad using an XOR cipher. Requires Python >=3.6."""

import argparse
import itertools
import pathlib
import re
import secrets
import sys

# One-time pad file signature.
MAGIC = "#one-time pad"


def make_keys(n, size):
    """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.
    return (secrets.token_hex(size) for _ in range(n))


def make_pad(name, pad_size, key_size):
    """Create a new one-time pad identified by the given name.

    Args:
        name (str): Unique one-time pad identifier.
        pad_size (int): The number of keys (or pages) in the pad.
        key_size (int): The number of bytes per key.
    Returns:
        The new one-time pad as a string.
    """
    pad = [
        MAGIC,
        f"#name={name}",
        f"#size={pad_size}",
        *make_keys(pad_size, key_size),
    ]

    return "\n".join(pad)


def xor(message, key):
    """Return ``message`` XOR-ed with ``key``.

    Args:
        message (bytes): Plaintext or cyphertext to be encrypted or decrypted.
        key (bytes): Encryption and decryption key.
    Returns:
        Plaintext or cyphertext as a byte string.
    """
    return bytes(mc ^ kc for mc, kc in zip(message, itertools.cycle(key)))


def use_key(pad):
    """Use the next available key from the given one-time pad.

    Args:
        pad (str): A one-time pad.
    Returns:
        (str, str) A two-tuple of updated pad and key.
    """
    match = re.search(r"^[a-f0-9]+$", pad, re.MULTILINE)
    if not match:
        error("pad is all used up")

    key = match.group()
    pos = match.start()

    return (f"{pad[:pos]}-{pad[pos:]}", key)


def log(msg):
    """Log a message."""
    sys.stderr.write(msg)
    sys.stderr.write("\n")


def error(msg):
    """Exit with an error message."""
    sys.stderr.write(msg)
    sys.stderr.write("\n")
    sys.exit(1)


def write_pad(path, pad_size, key_size):
    """Write a new one-time pad to the given path.

    Args:
        path (pathlib.Path): Path to write one-time pad to.
        pad_size (int): The number of keys (or pages) in the pad.
        key_size (int): The number of bytes per key.
    """
    if path.exists():
        error(f"pad '{path}' already exists")

    with path.open("w") as fd:
        fd.write(make_pad(path.name, pad_size, key_size))

    log(f"New one-time pad written to {path}")


def main(pad, message, outfile):
    """Encrypt or decrypt ``message`` using the given pad.

    Args:
        pad (pathlib.Path): Path to one-time pad.
        message (bytes): Plaintext or ciphertext message to encrypt or decrypt.
        outfile: File-like object to write to.
    """
    if not pad.exists():
        error(f"no such pad '{pad}'")

    with pad.open("r") as fd:
        if fd.readline().strip() != MAGIC:
            error(f"file '{pad}' does not look like a one-time pad")

    # Rewrites the entire one-time pad every time
    with pad.open("r+") as fd:
        updated, key = use_key(fd.read())

        fd.seek(0)
        fd.write(updated)

    outfile.write(xor(message, bytes.fromhex(key)))


if __name__ == "__main__":
    # Command line interface
    parser = argparse.ArgumentParser(description="One-time pad.")

    parser.add_argument(
        "pad",
        help=(
            "Path to one-time pad. If neither --encrypt or --decrypt "
            "are given, will create a new pad."
        ),
    )

    parser.add_argument(
        "--length",
        type=int,
        default=10,
        help="Pad size. Ignored if --encrypt or --decrypt are given. Defaults to 10.",
    )

    parser.add_argument(
        "--key-size",
        type=int,
        default=64,
        help="Key size in bytes. Ignored if --encrypt or --decrypt are given. Defaults to 64.",
    )

    parser.add_argument(
        "-o",
        "--outfile",
        type=argparse.FileType("wb"),
        default=sys.stdout.buffer,
        help=(
            "Write encoded/decoded message to a file. Ignored if --encrypt or "
            "--decrypt is not given. Defaults to stdout."
        ),
    )

    group = parser.add_mutually_exclusive_group()

    group.add_argument(
        "--encrypt",
        metavar="FILE",
        type=argparse.FileType("rb"),
        help="Encrypt FILE using the next available key from pad.",
    )
    group.add_argument(
        "--decrypt",
        metavar="FILE",
        type=argparse.FileType("rb"),
        help="Decrypt FILE using the next available key from pad.",
    )

    args = parser.parse_args()

    if args.encrypt:
        message = args.encrypt.read()
    elif args.decrypt:
        message = args.decrypt.read()
    else:
        message = None

    # Sometimes necessary if message came from stdin
    if isinstance(message, str):
        message = message.encode()

    pad = pathlib.Path(args.pad).with_suffix(".1tp")

    if message:
        main(pad, message, args.outfile)
    else:
        write_pad(pad, args.length, args.key_size)
Output:

Command line interface. Show the help message and exit

$ python otp.py --help
usage: otp.py [-h] [--length LENGTH] [--key-size KEY_SIZE] [-o OUTFILE]
              [--encrypt FILE | --decrypt FILE]
              pad

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.
  -o OUTFILE, --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.


Create a new one-time pad in the current working directory.

$ python otp.py mypad --length 5 --key-size 128
$
$ head mypad.1tp
#one-time pad
#name=mypad.1tp
#size=5
8522ecc603a492693c04a19e55a3340601e3 ...


Distribute our new one-time pad and use it to encrypt a message.

$ cp mypad.1tp shared.1tp
$
$ echo "like all shared secrets, the pad has to be at least as long as the message" > message.txt
$ python otp.py mypad.1tp --encrypt message.txt -o secret.dat


And decrypt the message using the shared copy of the one-time pad.

$ python otp.py shared.1tp --decrypt secret.dat -o plaintext.txt

Racket

See One-time pad/Racket

Raku

(formerly Perl 6)

Works with: Rakudo version 2017.10

The task is somewhat under-specified, especially the third (optional) section so I'm skipping that for now. Each sub-task has it's own code.

Sub-task one: generate one-time-pad files.

This is a fairly basic otp file generator. Uses Crypt::Random for decently high quality random numbers. (Random bytes are drawn from /dev/urandom on Unix-like systems, and CryptGenRandom() on Windows.) It will ask for a file name to save to, and the number of lines you want. Each line can be used to encode up to 48 characters of data. Default is 1000 lines, Only generating 4 lines here for demonstration purposes. Saving the file to 'rosettacode.1tp'.

sub MAIN {
    put "Generate data for one time pad encryption.\n" ~
        "File will have .1tp extension.";
    my $fn;
    loop {
        $fn = prompt 'Filename for one time pad data: ';
        if $fn !~~ /'.1tp' $/ { $fn ~= '.1tp' }
        if $fn.IO.e {
            my $ow = prompt "$fn aready exists, over-write? y/[n] ";
            last if $ow ~~ m:i/'y'/;
            redo;
        }
        last;
    }

    put 'Each line will contain 48 characters of encyption data.';
    my $lines = prompt 'How many lines of data to generate? [1000] ';
    $lines ||= 1000;
    generate($fn, $lines);
    say "One-time-pad data saved to: ", $fn.IO.absolute;

    sub generate ( $fn, $lines) {
        use Crypt::Random;
        $fn.IO.spurt: "# one-time-pad encryption data\n" ~
          ((sprintf(" %s %s %s %s %s %s %s %s\n",
          ((('A'..'Z')[crypt_random_uniform(26)] xx 6).join) xx 8))
          xx $lines).join;
    }
}
Output:
Generate data for one time pad encryption.
File wile have .1tp extension.
Filename for one time pad data: rosettacode
Each line will contain 48 characters of encyption data.
How many lines of data to generate? [1000] 4
One-time-pad data saved to: /home/myhome/mydirectory/rosettacode.1tp

Sample file generated by above code:

# one-time-pad encryption data
 DSUJOU UUWDZD VHFWRR AJDMDC ERZDGD WWKLHJ YITCML FORXCV
 BXGFCL ANGCGY VTAEUG UYAIPK FXWMNI INDLOR JIDZQL BOFFQD
 JISNOS CMLRPW TFGELQ HPTMRN SHBBDP AIVDAC CEWIFH TRLQVK
 FRBUUC GDCQHQ CEEURS RGVWVT JZIQLP NQCABF BWUPUI UDTZAF

Sub-task two: encrypt/decrypt text using the otp files generated by part one.

One-time-pad encryption gets it's security from the fact that the pads are used one time. As a line is used in the otp file, it needs to be marked as used, or removed so it doesn't get reused. Theoretically, you would make a copy of rosettacode.1tp and send it by secure means to the receiver of your encrypted text so that they can use it to decrypt. Since we are encrypting and decrypting on the same computer, we'll make a copy of the otp file named rosettacopy.1tp and use that for decryption so the encrypt and decrypt functions don't conflict.

sub s2v ($s) { $s.uc.comb(/ <[ A..Z ]> /)».ord »-» 65 }
sub v2s (@v) { (@v »%» 26 »+» 65)».chr.join }

sub hide   ($secret, $otp) { v2s(s2v($secret) »+» s2v($otp)) }
sub reveal ($hidden, $otp) { v2s(s2v($hidden) »-» s2v($otp)) }

sub otp-data ($fn, $lines) {
    my $fh = $fn.IO.open :rw;
    my $data;
    my $count = 0;
    repeat {
        my $pos = $fh.tell;
        my $line = $fh.get;
        if $line.substr(0,1) ne '-'|'#' {
            $data ~= $line;
            $fh.seek($pos);
            $fh.put: '-' ~ $line.substr(1);
            $count++;
        }
    } until $count == $lines or $fh.eof;
    note "Insufficient lines of data remaining in $fn" if $count != $lines;
    $data;
}

sub otp-size (Str $string) { ceiling $string.uc.comb(/ <[ A..Z ]> /) / 48 }

sub otp-encrypt ( $secret, $fn ) {
    my $otp-size = otp-size $secret;
    my $otp-data = otp-data($fn, $otp-size);
    my $encrypted = hide $secret, $otp-data;
    # pad encryted text out to a full line with random text
    $encrypted ~= ('A'..'Z').roll while $encrypted.chars % 48;
    join "\n", $encrypted.comb(6).rotor(8, :partial).map:
      { sprintf "{ join ' ', "%s" xx $_ }", $_ };
}

sub otp-decrypt ( $secret, $fn ) {
    my $otp-size = otp-size $secret;
    my $otp-data = otp-data($fn, $otp-size);
    my $plain-text = reveal $secret, $otp-data;
    join "\n", $plain-text.comb(6).rotor(8, :partial).map:
      { sprintf "{ join ' ', "%s" xx $_ }", $_ };
}

my $otp-encrypt-fn = 'rosettacode.1tp';
my $otp-decrypt-fn = 'rosettacopy.1tp';

my $secret = "Beware the Jabberwock, my son! The jaws that bite, the claws that catch!";

say "Secret:\n$secret\n\nEncrypted:";
say my $encrypted =   otp-encrypt $secret,    $otp-encrypt-fn;
say "\nDecrypted:\n", otp-decrypt $encrypted, $otp-decrypt-fn;
Output:
Secret:
Beware the Jabberwock, my son! The jaws that bite, the claws that catch!

Encrypted:
EWQJFY NBAMZE WLWSFT KVBERP XYDMGZ OPRLAK GBXVTP HZRTUO
IXZHCE CUUORL THOFDI DCXRLG JQICPI ZEREHP RLOEAE PRMVJH

Decrypted:
BEWARE THEJAB BERWOC KMYSON THEJAW STHATB ITETHE CLAWST
HATCAT CHOMLN YOOBJC JEXJWW ETMQCA RROTTY IDLFKT ODHQTE

Contents of rosettacode.1tp after encryption / rosettacopy.1tp after decryption:

# one-time-pad encryption data
-DSUJOU UUWDZD VHFWRR AJDMDC ERZDGD WWKLHJ YITCML FORXCV
-BXGFCL ANGCGY VTAEUG UYAIPK FXWMNI INDLOR JIDZQL BOFFQD
 JISNOS CMLRPW TFGELQ HPTMRN SHBBDP AIVDAC CEWIFH TRLQVK
 FRBUUC GDCQHQ CEEURS RGVWVT JZIQLP NQCABF BWUPUI UDTZAF

Tcl

Part 1: random strings

Get true random numbers, and turn them into strings.

With "randInt" from Tcl'ers wiki Cryptographically secure random numbers using /dev/urandom

puts "# True random chars for one-time pad" 

proc randInt { min max } {
    set randDev [open /dev/urandom rb]
    set random [read $randDev 8]
    binary scan $random H16 random
    set random [expr {([scan $random %x] % (($max-$min) + 1) + $min)}]
    close $randDev
    return $random
}

proc randStr { sLen grp alfa } {
  set aLen [string length $alfa]; incr aLen -1
  set rs ""
  for {set i 0} {$i < $sLen} {incr i} {
    if { [expr {$i % $grp} ] == 0} { append rs " " }
    set r [randInt 0 $aLen]
    set char [string index $alfa $r]
    append rs $char
  ##puts "$i: $r $char"
  }
  return $rs
}

set alfa "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
set len 48
set lines 4
set fn "test.1tp"

# Write file:
set fh [open $fn w]
puts $fh "# OTP"
for {set ln 0} {$ln < $lines} {incr ln} {
    set line [randStr $len 6 $alfa]
  ##puts "$ln :$line."
    puts $fh $line
}
close $fh

# Read file:
puts "# File $fn:"
set fh [open $fn]
puts [read $fh [file size $fn]]
close $fh

puts "# Done."
Output:
# True random chars for one-time pad
# File test.1tp:
# OTP
 OWCTEL SGDQEA UKEWCU PUTDEA XICBOL VVMJHD OHAXSE ZFAGDE
 QHDHKQ CCJBYF CMRCMC IXXPVM IOHQDA XIDTPX FGRIJC NPDOAT
 MYYQUV ZVKGDF ZLYKSX MBPLON RMQKQT QDYJVO LNKUFV DNKIQP
 NQOZKU MQOWHS VOQFWL EQWBFA HZQAMG JWNHGZ QERNNV GBKQTM
# Done.


Part 2: Encrypt/Decrypt

See Tcl'ers wiki: vignere Vigenere ...


Part 3: Management

  • list padfiles in directory
  • list lines / blocks between "comment"-lines in padfile (i.e. remaining usable data)

...


Wren

Translation of: Go
Library: Wren-srandom
Library: Wren-ioutil
Library: Wren-dynamic
Library: Wren-str
import "io" for File, Directory
import "./srandom" for SRandom
import "./ioutil" for FileUtil, Input
import "./dynamic" for Enum
import "./str" for Char, Str

var CHARS_PER_LINE = 48
var CHUNK_SIZE = 6
var COLS = 8
var DEMO = true  // would normally be set to false

var FileType = Enum.create("FileType", ["OTP", "ENC", "DEC"])

var toAlpha = Fn.new { |s| s.where { |c| Char.isAsciiUpper(c) }.join() }

var isOtpRelated = Fn.new { |s|
    return s.endsWith(".1tp") || s.endsWith(".1tp_cpy") ||
           s.endsWith(".1tp_enc") || s.endsWith(".1tp_dec")
}

var inChunks = Fn.new { |s, nLines, ft|
    var chunks = Str.chunks(s, CHUNK_SIZE)
    var sb = ""
    for (i in 0...nLines) {
        var j = i * COLS
        var ch = chunks[j...j+COLS].join(" ")
        sb = sb + " " + ch + "\n"
    }
    sb = " file\n" + sb
    return (ft == FileType.OTP) ? "# OTP" + sb :
           (ft == FileType.ENC) ? "# Encrypted" + sb :
           (ft == FileType.DEC) ? "# Decrypted" + sb : ""
}

var makePad = Fn.new { |nLines|
    var nChars = nLines * CHARS_PER_LINE
    var sb = ""
    /* generate random upper case letters */
    for (i in 0...nChars) sb = sb + String.fromByte(SRandom.int(65, 91))
    return inChunks.call(sb, nLines, FileType.OTP)
}

var vigenere = Fn.new { |text, key, encrypt|
    var sb = ""
    var i = 0
    for (c in text) {
        var ci = encrypt ? (c.bytes[0] + key[i].bytes[0] - 130) % 26 :
                           (c.bytes[0] - key[i].bytes[0] +  26) % 26
        sb = sb + String.fromByte(ci + 65)
        i = i + 1
    }
    var temp = sb.count % CHARS_PER_LINE
    if (temp > 0) {  // pad with random characters so each line is a full one
        for (i in temp...CHARS_PER_LINE) sb = sb + String.fromByte(SRandom.int(65, 91))
    }
    var ft = encrypt ? FileType.ENC : FileType.DEC
    return inChunks.call(sb, (sb.count / CHARS_PER_LINE).floor, ft)
}

var menu = Fn.new {
    System.print("""

1. Create one time pad file.

2. Delete one time pad file.

3. List one time pad files.

4. Encrypt plain text.

5. Decrypt cipher text.

6. Quit program.

""")
    return Input.integer("Your choice (1 to 6) : ", 1, 6)
}

while (true) {
    var choice = menu.call()
    System.print()
    if (choice == 1) {  // Create OTP
        System.print("Note that encrypted lines always contain 48 characters.\n")
        var fileName = Input.text("OTP file name to create (without extension) : ", 1) + ".1tp"
        var nLines = Input.integer("Number of lines in OTP (max 1000) : ", 1, 1000)
        var key = makePad.call(nLines)
        File.create(fileName) { |f| f.writeBytes(key) }
        System.print("\n'%(fileName)' has been created in the current directory.")
        if (DEMO) {
            // a copy of the OTP file would normally be on a different machine
            var fileName2 = fileName + "_cpy"  // copy for decryption
            File.create(fileName2) { |f| f.writeBytes(key) }
            System.print("'%(fileName2)' has been created in the current directory.")
            System.print("\nThe contents of these files are :\n")
            System.print(key)
        }
    } else if (choice == 2) {  // Delete OTP
        System.print("Note that this will also delete ALL associated files.\n")
        var toDelete1 = Input.text("OTP file name to delete (without extension) : ", 1) + ".1tp"
        var toDelete2 = toDelete1 + "_cpy"
        var toDelete3 = toDelete1 + "_enc"
        var toDelete4 = toDelete1 + "_dec"
        var allToDelete = [toDelete1, toDelete2, toDelete3, toDelete4]
        var deleted = 0
        System.print()
        for (name in allToDelete) {
            if (File.exists(name)) {
                File.delete(name)
                deleted = deleted + 1
                System.print("'%(name)' has been deleted from the current directory.")
            }
        }
        if (deleted == 0) System.print("There are no files to delete.")
    } else if (choice == 3) {  // List OTPs
        System.print("The OTP (and related) files in the current directory are:\n")
        var otpFiles = Directory.list("./").where { |f| File.exists(f) && isOtpRelated.call(f) }.toList
        System.print(otpFiles.join("\n")) // already sorted
    } else if (choice == 4) {  // Encrypt
        var keyFile = Input.text("OTP file name to use (without extension) : ", 1) + ".1tp"
        if (File.exists(keyFile)) {
            var lines = FileUtil.readLines(keyFile)
            var first = lines.count
            for (i in 0...lines.count) {
                if (lines[i].startsWith(" ")) {
                    first = i
                    break
                }
            }
            if (first == lines.count) {
                System.print("\nThat file has no unused lines.")
                continue
            }
            var lines2 = lines.skip(first).toList  // get rid of comments and used lines
            var text = toAlpha.call(Str.upper(Input.text("Text to encrypt :-\n\n", 1)))
            var len = text.count
            var nLines = (len / CHARS_PER_LINE).floor
            if (len % CHARS_PER_LINE > 0) nLines = nLines + 1
            if (lines2.count >= nLines) {
                var key = toAlpha.call(lines2.take(nLines).join(""))
                var encrypted = vigenere.call(text, key, true)
                var encFile = keyFile + "_enc"
                File.create(encFile) { |f| f.writeBytes(encrypted) }
                System.print("\n'%(encFile)' has been created in the current directory.")
                for (i in first...first + nLines) {
                    lines[i] = "-" + lines[i][1..-1]
                }
                File.create(keyFile) { |f| f.writeBytes(lines.join("\n")) }
                if (DEMO) {
                    System.print("\nThe contents of the encrypted file are :\n")
                    System.print(encrypted)
                }
            } else System.print("Not enough lines left in that file to do encryption")
        } else System.print("\nhat file does not exist.")
    } else if (choice == 5) {  // Decrypt
        var keyFile = Input.text("OTP file name to use (without extension) : ", 1) + ".1tp_cpy"
        if (File.exists(keyFile)) {
            var keyLines = FileUtil.readLines(keyFile)
            var first = keyLines.count
            for (i in 0...keyLines.count) {
                if (keyLines[i].startsWith(" ")) {
                    first = i
                    break
                }
            }
            if (first == keyLines.count) {
                System.print("\nThat file has no unused lines.")
                continue
            }
            var keyLines2 = keyLines[first..-1]  // get rid of comments and used lines
            var encFile = keyFile[0..-4] + "enc"
            if (File.exists(encFile)) {
                var encLines = FileUtil.readLines(encFile)[1..-1]  // exclude comment line
                var nLines = encLines.count
                if (keyLines2.count >= nLines) {
                    var encrypted = toAlpha.call(encLines.join(""))
                    var key = toAlpha.call(keyLines2.take(nLines).join(""))
                    var decrypted = vigenere.call(encrypted, key, false)
                    var decFile = keyFile[0..-4] + "dec"
                    File.create(decFile) { |f| f.writeBytes(decrypted) }
                    System.print("\n'%(decFile)' has been created in the current directory.")
                    for (i in first...first + nLines) {
                        keyLines[i] = "-" + keyLines[i][1..-1]
                    }
                    File.create(keyFile) { |f| f.writeBytes(keyLines.join("\n")) }
                    if (DEMO) {
                        System.print("\nThe contents of the decrypted file are :\n")
                        System.print(decrypted)
                    }
                } else System.print("Not enough lines left in that file to do decryption")
            } else System.print("\n'%(encFile)' is missing.")
        } else System.print("\nThat file does not exist.")
    } else {
        return  // Quit
    }
}
Output:

Input/output for a sample session. In the interests of brevity, --<menu>-- indicates the re-display of the menu after the previous choice has been processed:

1. Create one time pad file.

2. Delete one time pad file.

3. List one time pad files.

4. Encrypt plain text.

5. Decrypt cipher text.

6. Quit program.

Your choice (1 to 6) : 1

Note that encrypted lines always contain 48 characters.

OTP file name to create (without extension) : user1
Number of lines in OTP (max 1000) : 4

'user1.1tp' has been created in the current directory.
'user1.1tp_cpy' has been created in the current directory.

The contents of these files are :

# OTP file
 JRTAGR LQKYMI NADNZW XAVCEX RYLONG UODMBF XZAAFE LWLCGT
 BGNPAT CJYIDS UDRWOD QMJMYI SYDRWV PQEXLI AXJWSI GMUTRR
 KIUSZH XFNRCF NKJLYU IDJXIQ GHSSAN MLSHVU AMKIFV MVXAZT
 QSOLCZ RXYLMS QOLAMJ JJGAHS IROKWS ZARISI TIKXHC GCPPSB

--<menu>--

Your choice (1 to 6) : 3

The OTP (and related) files in the current directory are:

user1.1tp
user1.1tp_cpy

--<menu>--

Your choice (1 to 6) : 4

OTP file name to use (without extension) : user1
Text to encrypt :-

Beware the Jabberwock, my son! The jaws that bite, the claws that catch!

'user1.1tp_enc' has been created in the current directory.

The contents of the encrypted file are :

# Encrypted file
 KVPAXV EXOHMJ OEUJNY HMTUSK KFPXNC MHKMUG FSETMI NHLYYM
 IGGRAM EQLTCZ VNUYHO CYRHKU GTRZAZ UUDTDL DEDXRL DMAVDA

--<menu>--

Your choice (1 to 6) : 5

OTP file name to use (without extension) : user1

'user1.1tp_dec' has been created in the current directory.

The contents of the decrypted file are :

# Decrypted file
 BEWARE THEJAB BERWOC KMYSON THEJAW STHATB ITETHE CLAWST
 HATCAT CHNLZH BKDCTL MMIVMM OVOIEE FEZWSD DHUBZD XAGCMJ

--<menu>--

Your choice (1 to 6) : 2

Note that this will also delete ALL associated files.

OTP file name to delete (without extension) : user1

'user1.1tp' has been deleted from the current directory.
'user1.1tp_cpy' has been deleted from the current directory.
'user1.1tp_enc' has been deleted from the current directory.
'user1.1tp_dec' has been deleted from the current directory.

--<menu>--

Your choice (1 to 6) : 6

TypeScript

#!/usr/bin/env node
import { writeFileSync, existsSync, readFileSync, unlinkSync } from 'node:fs';
//https://www.elitizon.com/2021/01-09-how-to-create-a-cli-command-with-typescript/#:~:text=%20Boost%20your%20productivity%20by%20creating%20your%20own,information.%20Required%3A%20string.%20%20...%20%20More%20

const a:string[] = process.argv;
const argv:string[] = a.splice(2)
/**
 *  Extension of the pad files
 */
const padExtension:string = ".1tp";
/**
 * Extension of the key generated
 */
const keyExtension:string = ".key";
/**
 * Array of commands usable
 */
const commands:string[] = ["--generate", "--encrypt","--decrypt"];
/**
 * The alphabet
 */
const stringLetter = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"


/**
 * Main function
 */
const main = ():void => {
    // if no args have been give display help
    if(argv.length == 0){
        help();
        return;
    }

    // Check if there is a command in the file
    if(commands.includes(argv[0])){
        let choise = argv[0].trim().toLowerCase();
        if(choise === "--generate"){
                console.log("Generating action chosen");

                // Check if the arguments have been given to generate a new key
                if(!argv[1]){
                    ("Pad name not specified")
                    help();
                    return;
                }

                // Check if the length of the filename is at least 4 characters
                if(argv[1].length <= 3){
                    console.log("The name of the pad should be at least 4 letters long");
                    help();
                    return;
                }

                // Check if the filesize is a number(should be the second argv)
                let fileSize = +argv[2];
                if(isNaN(fileSize)){
                    console.log("File size should be a number");
                    help();
                    return
                }

                createFile(argv[1], fileSize)
        }

        // If the selected action is to encrypt
        if( choise === "--encrypt"){
                console.log("Encrypting action chosen");
                let padDataName = argv[2] ? `${argv[2].trim()}${padExtension}` : `${argv[1].trim()}${padExtension}`

                // Check if the files exist
                if(!argv[1] && !argv[2]){
                    console.log("Parameters not given");
                    help();
                    return;
                }

                // Check if the text file exists and the pad file
                if(!existsSync(`${argv[1].trim().toLowerCase()}.txt`)){
                    console.log("Text file doesn't exit");
                    help();
                    return;
                }

                console.log("Reading file")
                // Get the data from the file as well as the data from the padfile
                let fileDataEnc = readFileSync(`${argv[1].trim().toLowerCase()}.txt`, 'utf-8'); 
                
                if(!existsSync(`${argv[2]}${padExtension}`)){
                    console.log("1tp file doesn't exist, generating new 1pt file");
                    createFile(argv[1], fileDataEnc.length);
                }

                console.log("Reading 1pt file");
                // Get the pad data
                let padDataEnc = RetrievePadData(padDataName);
                
                // if the file 1pt file has been specified but it's not big enough
                if(fileDataEnc.length > padDataEnc.length){
                    console.log("The file is bigger than the 1tp file. Generating a new padfile");
                    createFile(argv[1], fileDataEnc.length);
                    padDataEnc = RetrievePadData(`${argv[1]}${padExtension}`)
                }

                console.log("Converting the file");
                let keyEnc = padFunction(fileDataEnc,padDataEnc);
                createFile(`${argv[1]}.key`, keyEnc.length, false, keyEnc);

        }


        if(choise === "--decrypt"){
                console.log("Decrypting action chosen");

                // Check if the necessary args were given
                if(!argv[1]){
                    console.log("No file specified For decryption specified")
                    return;
                }
                if(!argv[2]){
                    console.log("No 1pt file was given, Please specify the file to use");
                    return;
                }
                if(!argv[3] || argv[3].trim() != '-o') {
                   console.log("No file for output specified");
                }


                let fileNameDec = `${argv[1].trim()}${keyExtension}`; 
                let padNameDec = `${argv[2].trim()}${padExtension}`;

                // Check if the files exists or not
                if(!existsSync(fileNameDec)){
                    console.log("File specified does not exist");
                    return;
                }
                if(!existsSync(padNameDec)){
                    console.log("1pt file specified doesn't exist");
                    return;
                }

                console.log("Read the encrypted file");
                let fileDataDec = readFileSync(fileNameDec, 'utf-8');
                console.log("Reading the 1pt file");
                let padDataDec = RetrievePadData(padNameDec);

                // decrypt the file
                console.log("Generating the file");
                let keyDec = padFunction(fileDataDec, padDataDec)
                

                if(argv[3] && argv[3].trim()==='-o'){
                    let textFileDec = `${argv[1].trim()}.txt`; 
                    if(!argv[4]){
                        console.log("No output file specified, creating a new file")
                    }
                    if(argv[4] && argv[4].trim().length < 3){
                        console.log("The name of the output file should be greater than 4 characters");
                        console.log(`Creating new file`)
                    }else {
                        argv[4] ? textFileDec = `${argv[4].trim()}.txt` : textFileDec = `${argv[1].trim()}.txt`
                    }

                    console.log("Generating text file");
                    createFile(textFileDec, 0, false, keyDec );
                    // https://sebhastian.com/javascript-delete-file/
                    console.log("Deleting encrypted file");
                    unlinkSync(fileNameDec);
                    console.log("Deleting 1pt file");
                    unlinkSync(padNameDec);
                }else {
                    console.log("The decrypted file is: \n\n")
                    console.log(keyDec);
                }
        }

    }else {
        console.log("Invalid action");
        help();
        return;
    }

}

/**
 * Display help messages
 */
const help = ():void => {
    console.log("\n\n\n1tp is a tool used to encrypt and decrypt text files");
    console.log("Look at the commands below to learn the use of it");
    console.log("The input file for encryption should be a txt file");
    console.log("The output will be a .kye file and a .1pt file");
    console.log("\n\n**Note: Don't add file extensios while running the command**");
    console.log("**Note: if during creation, the application encouters a file already existing, it will overwrite the file**");
    console.log("Options: ");
    console.log("\t -h | --help \t\t\t\t\t\t\t View help");
    console.log("\t --generate <pad name> <size of file> \t\t\t\t Create a pad file given a size");
    console.log("\t --encrypt <txt file> <pad name> \t\t\t\t Encrypt a file give a pad file");
    console.log("\t\t\t\t All parameters for encryption are required");
    console.log("\t --decrypt <key file> <pad name> -o <txt file>  \t Decrypt a file give a key and 1tp files");
    console.log("\t\t\t\t If output file is not specified it will display the text inside the console");
    console.log("\t\t\t\t If only the `-o` option is defined without a txt file, it will create a new file");
    console.log("\n** Note: Decrypting a file will delete the key and the 1pt files**");
}

/**
 * Create a file with a random key
 * @param file Name of the file being created
 * @param fileSize Size of the file being created
 */
const createFile = (file:string, fileSize:number = 1024, key:boolean = true, data:string = ""):void => {
    let fileName:string;
    if(key){
        fileName = `${file.trim().toLowerCase()}${padExtension}`;
    
        // https://flaviocopes.com/how-to-check-if-file-exists-node/#:~:text=The%20way%20to%20check%20if%20a%20file%20exists,the%20existence%20of%20a%20file%20without%20opening%20it%3A
        
        console.log("Generating the One time pad");
        const pad = generateOneTimePad(fileSize);
        //https://code-boxx.com/create-save-files-javascript/#:~:text=%20The%20possible%20ways%20to%20create%20and%20save,the%20server.%0Avar%20data%20%3D%20new%20FormData...%20More%20
        console.log("Writing to file");
        writeFileSync(fileName, pad);
        return; 
        
    }

    console.log("Writing to file");
    writeFileSync(file, data);
};

/**
 * Generate a pad
 * @param fileSize Size of the new pad being created
 * @returns the new pad
 */
const generateOneTimePad = (fileSize:number):string => {
    /**
     * String in which we will put the one time pad
     */
    let otp:string = "";
    /**
     * used to organize the key
     */
    let splitCounter = 0;
    let columnCounter = 0;

    for (let i = 0; i <= fileSize;) {
        if(splitCounter < 6){
            // Generate a random letter from the alphabet given
            otp += (stringLetter.charAt(Math.random()*1000%stringLetter.length))
            splitCounter++;
            i++;
            continue;
        }
        // Used to organize the key so it looks estetic
        if(columnCounter < 7){
            splitCounter = 0;
            columnCounter++;
            otp += ("\t");
            continue;
        }else{
            splitCounter = 0;
            columnCounter = 0;
            otp += ("\n");
            continue;
        }
        
    }
    return otp
}

/**
 * Get the pad data from the file
 * @returns Text pad data
 */
const RetrievePadData = (padFile:string):string => {
    // Retrieve the pad data
    let padData = "" 
    // read the file
    const data = readFileSync(`${padFile}`, 'utf-8') 

    // Take out all the tabs and new lines we added while generating the key
    const stringArray = data.toString().split("\n");
    for (let index = 0; index < stringArray.length; index++) {
        const element = stringArray[index].split("\t");
        for (let i = 0; i < element.length; i++) {
            padData += element[i];
        }
    }

    return padData;
}

/**
 * One time pad function
 * @param fileData Data of the file to encrypt or decrypt
 * @param padData Data of the pad file
 * @returns The generated encryption or decryption
 */
const padFunction = (fileData:string, padData:string ):string => {
    let key = "";
    for (let i = 0; i < fileData.length; i++) {
        key += String.fromCharCode(-fileData[i].charCodeAt(0)+padData[i].toLowerCase().charCodeAt(0)+97);
        // console.log(`File char: ${fileData[i]} ${fileData[i].charCodeAt(0)}   Pad Data: ${padData[i]} ${padData[i].toLocaleLowerCase().charCodeAt(0)}   Key: ${key[i]} ${key[i].charCodeAt(0)}`)
    }
    return key;
}

main();
Output:

Help Message

 $ node .\bin\
1tp is a tool used to encrypt and decrypt text files
Look at the commands below to learn the use of it
The input file for encryption should be a txt file
The output will be a .kye file and a .1pt file

**Note: Don't add file extensios while running the command**
**Note: if during creation, the application encouters a file already existing, it will overwrite the file**
Options: 
	 -h | --help 							 View help
	 --generate <pad name> <size of file> 				 Create a pad file given a size
	 --encrypt <txt file> <pad name> 				 Encrypt a file give a pad file
				 All parameters for encryption are required
	 --decrypt <key file> <pad name> -o <txt file>  	 Decrypt a file give a key and 1tp files
				 If output file is not specified it will display the text inside the console
				 If only the `-o` option is defined without a txt file, it will create a new file

** Note: Decrypting a file will delete the key and the 1pt files**

Encrypting a file

 $ node .\bin\ --encrypt fileName
Encrypting action chosen
Reading file
1tp file doesn't exist, generating new 1pt file
Generating the One time pad
Writing to file
Reading 1pt file
Converting the file
Writing to file
_________________________________________________
fileName.1tp
_________________________________________________
SKGLQD	OJANNV	WTRAUE	HOTWJK	OCUEK

_________________________________________________
fileName.key
_________________________________________________
�d_Z²\]«UV¯rjraIfRdlµkfY]coa
_________________________________________________

Decrypting a file

 $ node .\bin\ --decrypt fileName fileName
Decrypting action chosen
No file for output specified
Read the encrypted file
Reading the 1pt file
Generating the file
The decrypted file is: 

This is my encrypted message