⚠️ Warning: This is a draft ⚠️

This means it might contain formatting issues, incorrect code, conceptual problems, or other severe issues.

If you want to help to improve and eventually enable this page, please fork RosettaGit's repository and open a merge request on GitHub.

This is a console based app for One Time Pad file management. It uses the internet random.org as source combined with local random number generation as a backup source of random letters.

using Dates, HTTP

const configs = Dict("OTPdir" => ".")

stringtohash(s) = string(hash(s), base=16)

vencode(a::Char, b) = Char((Int(a) + Int(b) - 130) % 26 + 65)
vencode(atxt::String, btxt) = String(map((a, b) -> vencode(a, b), atxt, btxt))
vdecode(a::Char, b) = Char((Int(a) + 26 - Int(b)) % 26 + 65)
vdecode(atxt::String, btxt) = String(map((a, b) -> vdecode(a, b), atxt, btxt))

function getrandom()
    bytes, ret, source = UInt8[], Char[], "local"
    try
        hx = HTTP.get("https://www.random.org/cgi-bin/randbyte?nbytes=1024&format=hex").body
        if length(hx) < 100
            throw("not enough bytes gotten from internet")
        end
        bytes = map(s -> parse(UInt8, s, base=16), split(strip(String(hx)), r"\s+"))
        source = "random.org"
        bytes = map((a, b) -> xor(a, b), bytes, rand(UInt8, length(bytes)))
    catch y
        @warn("internet source failure: $y")
        bytes = rand(UInt8, 1568)
    end
    for b in bytes
        ch = "ABCDEFGHIJKLMNOPQRSTUVWXYZ***"[div(b, 9) + 1]
        if ch != '*'
            push!(ret, ch)
        end
    end
    source, ret
end

function newOTPfilename()
    dircontents = readdir(configs["OTPdir"])
    while true
        nam = "OTP" * replace(string(now()), ":" => "_") * ".1tp"
        if (i = findfirst(x -> x == nam, dircontents)) != nothing
            sleep(0.5)
        else
            return nam
        end
    end
end

function lines40by5(s)
    ret = ""
    for (i, ch) in enumerate(s)
        ret *= (i % 40 == 0) ? ch * "\n" :
            (i % 5 == 1) ? " " * ch : ch
    end
    ret
end

function createOTPfile(nletters, partnername="")
    newname, source = newOTPfilename(), "unknown"
    fp = open(newname, "w")
    needed = nletters + (nletters % 40 == 0 ? 0 : 40 - n % 40)
    data = Char[]
    while true
        source, randomdata = getrandom()
        println("Got ", length(randomdata), " letters from ", source)
        data = append!(data, randomdata)
        if length(data) >= needed
            data = data[1:needed]
            break
        end
    end
    s = lines40by5(data)
    write(fp, "# $source OTP ", stringtohash(partnername), "\n", s)
    close(fp)
    newname, needed, source
end

function readOTPfile(filename, n, skiplines=0, useused=false)
    fp = open(filename, "r+")
    seekpositions, ret = Vector{Int}(), ""
    while length(ret) < n
        if eof(fp)
            throw("Not enough letters in file")
        end
        if skiplines < 1
            push!(seekpositions, position(fp))
        end
        line = readline(fp)
        if skiplines > 0
            skiplines -= 1
            continue
        end
        if (useused || line[1] != '-') && line[1] != '#'
            ret *= replace(line, r"[^A-Z]" => "")
        else
            pop!(seekpositions)
        end
    end
    seekstart(fp)
    for pos in seekpositions
        seek(fp, pos)
        write(fp, '-')
    end
    close(fp)
    ret[1:n]
end

function getOTPfiles(dir=configs["OTPdir"])
    namedict = Dict{String, Pair{String,String}}()
    filenames = filter(nam -> occursin(r"\.1tp$", nam), readdir(dir))
    for (i, nam) in enumerate(filenames)
        fp = open(dir * "/" * nam, "r")
        lin = readline(fp)
        m = match(r"#\s+([\w\.]+)\s+OTP\s+([01-9a-fA-F]+)", lin)
        if m != nothing
            namedict[nam] = Pair(m.captures[1], m.captures[2])
        end
    end
    namedict
end

function listOTPfiles(partnername="")
    phash = stringtohash(partnername)
    files = Dict{Int, String}()
    println("Number            File name               partner code       source     count")
    for (i, p) in enumerate(getOTPfiles())
        if partnername == "" || phash == p[2][2]
            pathname = configs["OTPdir"] * "/" * p[1]
            files[i] = p[1]
            println(rpad(i, 8), rpad(p[1], 32), rpad(p[2][2], 20),
                rpad(p[2][1], 12), stat(pathname).size)
        end
    end
    files
end

function getconsoleinput(asInt=false, yn=false, default="")
    while true
        println("Enter choice ", asInt ? "number " : "", "or <enter> ", asInt ? "for 0:" : "to exit:")
        s = readline()
        if s == ""
            return asInt ? 0 : ""
        elseif !asInt
            if !yn || (s = string(uppercase(s)[1])) in ["Y", "N"]
                return s
            end
        else
            choice = tryparse(Int, s)
            if choice != nothing
                return choice
            end
        end
    end
end

function chooseOTPfiledialog()
    println("Enter partner name or <enter> for all.")
    pname = getconsoleinput()
    files = listOTPfiles(pname)
    println("Choose file number (0 to quit routine).")
    while true
        fnum = getconsoleinput(true)
        if fnum == 0
            return ""
        elseif haskey(files, fnum)
            return files[fnum]
        end
    end
end

function encryptdialog()
    if(fname = chooseOTPfiledialog()) == ""
        return
    end
    println("Skip lines marked as used?")
    useused = getconsoleinput(false, true) == "N"
    skiplines = 0
    println("Start file read at beginning?")
    if getconsoleinput(false, true) == "N"
        println("Enter number of lines to skip:")
        skiplines = getconsoleinput(true)
    end
    println("Enter lines of text to encrypt. Non-alphabet will be ignored. A blank line ends entry.")
    txt = ""
    while (lin = readline()) != ""
        txt *= lin
    end
    txt = replace(uppercase(txt), r"[^A-Z]" => "")
    otp = readOTPfile(fname, length(txt), skiplines, useused)
    println(lines40by5(vencode(txt, otp)))
end

function decryptdialog()
    if(fname = chooseOTPfiledialog()) == ""
        return
    end
    println("Skip lines marked as used?")
    useused = getconsoleinput(false, true) == "N"
    skiplines = 0
    println("Start file read at beginning?")
    if getconsoleinput(false, true) == "N"
        println("Enter number of lines to skip:")
        skiplines = getconsoleinput(true)
    end
    println("Enter lines of text to decrypt. A blank line ends entry.")
    txt = ""
    while (lin = readline()) != ""
        txt *= lin
    end
    txt = replace(txt, r"[^A-Z]" => "")
    otp = readOTPfile(fname, length(txt), skiplines, useused)
    println(vdecode(txt, otp))
end

function securedeletefile(filename, writes=100)
    ltrs = ["ABCDEFGHIJKLMNOPQRSTUVWXYZ"[i] for i in 1:26]
    pathname, len = configs["OTPdir"] * "/" * filename, stat(filename).size
    fp = open(pathname, "w")
    for _ in 1:writes
        write(fp, rand(ltrs, len))
        seekstart(fp)
    end
    close(fp)
    try rm(pathname); catch y; @warn(y) end
end

function createfiledialog()
    println("Enter partner name or press <enter> for none:")
    nam = getconsoleinput()
    println("Enter number of letters or <enter> for 2000:")
    n = getconsoleinput(true)
    newname, nletters, source = createOTPfile(n, nam)
    println("Created file $newname in directory $(configs["OTPdir"])",
        "\nwith $nletters letters. The source was $source.\n")
end

function deletefiledialog()
    fnam = chooseOTPfiledialog()
    if fnam != ""
        println("Confim by entering file name time string as (hh_mm_ss):")
        if occursin(getconsoleinput(), fnam)
            securedeletefile(fnam)
            println("File $fnam has been overwritten and deleted.")
        else
            println("No files deleted.")
        end
    end
end

function configure()
    println("The current subdirectory is: ", configs["OTPdir"])
    println("Enter new dirpath or <enter> to leave unchanged:")
    dirname = getconsoleinput()
    if dirname != ""
        try
            readdir(dirname)
            configs["OTPdir"] = dirname
        catch
            @warn("Invalid directory pathname: $dirname")
        end
    end
end

const mainmenu = """
Welcome to One Time Pad manager.

Select menu item:

A - Add new OTP file
D - Decrypt text
E - Encrypt text
R - Remove file (secure deletion)
C - Configure subdirectory
X - eXit
"""

function onetimepadapp()
    while true
        println(mainmenu)
        inchar = getconsoleinput()
        ltr = (inchar == "") ? "X" : string(uppercase(inchar)[1])
        if ltr == "A"
            createfiledialog()
        elseif ltr == "D"
            decryptdialog()
        elseif ltr == "E"
            encryptdialog()
        elseif ltr == "R"
            deletefiledialog()
        elseif ltr == "C"
            configure()
        elseif ltr == "X"
            break
        else
            println("Unknown choice $ltr, type $(typeof(ltr))")
        end
    end
    println("Close the console to keep past session text from being seen.")
end

onetimepadapp()