One-time pad/Julia

From Rosetta Code

See One-time_pad

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()