One-time pad/Phix

From Rosetta Code

Phix
A simple xor-based one time pad that allows any file to be encrypted and decrypted, since it seems silly to limit this to plaintext. The one time pad files are therefore binary rather than plain text, with comments and used counts in separate .1te/.1td files, rather than #/- in the .1tp itself. Uses a trivial xor rather than a Vigenere cipher, since the latter adds nothing. Ignores the "true random numbers, eg from /dev/random" part, especially since the wikipedia page cites /dev/random as potentially unsuitable for cryptographic use. Instead this just uses the builtin rand(): for better security I might suggest you replace that, possibly with several different (pseudo) random number generators.

The dump feature is only for testing, obviously, as is decrypt defaulting to the last encrypted message/file/pad. Note however that .1te/.1td handling is not: if you try to make a one time pad "bi-directional", then not only would there be some confusion and re-alignment issues should both parties send a message at the same time, but two messages encrypted with the same part of the one time pad severely weakens the security of those two messages. To prevent that, delete/do not share the .1te or .1td file, and it will not allow encryption/decryption respectively.

Note that options 4 (Encrypt plain text) and 5 (Decrypt cipher text) are also only suitable for testing: you cannot actually enter anything for option 5, it just decodes whatever was last encoded by option 4, however it would be trivial to store the output of option 4 in a file and send that, and the recipient would then decode it using option 7 (Decrypt file).

TIP: if a message gets lost, or you quit without decrypting something, then edit the recipient's .1td file manually, so that Used matches the sender's .1te file.

string default_enc = "",
       default_dec = ""
 
function size_used(string name, integer now_used=0)
    object txt = read_lines(name)
    if sequence(txt) then
        integer size, used
        for i=1 to length(txt) do
            if match("Size",txt[i])=1 then
                {{size}} = scanf(txt[i],"Size: %d")
            elsif match("Used",txt[i])=1 then
                if now_used=0 then
                    {{used}} = scanf(txt[i],"Used: %d")
                    return {size,used}
                else
                    txt[i] = sprintf("Used: %d",now_used)
                    if not write_lines(name,txt) then
                        printf(1,"error updating %s\n",{name})
                    end if
                    return 1
                end if
            end if
        end for
    end if
    return {-1,-1}
end function
 
procedure list_one_time_pads(bool set_default=false)
    puts(1,"\n")
    sequence d = dir("*.1t?")
    if d={} then
        printf(1,"No one time pad files found.\n")
        return
    elsif set_default then
        sequence bases = {},
                 types = {{},{},{}}     -- p/e/d
        for i=1 to length(d) do
            string name = d[i][D_NAME]
            integer dot = find('.',name)
            if dot!=0 then
                string base = name[1..dot-1],
                       ext = name[dot+1..$]
                integer e = find(ext,{"1tp","1te","1td"})
                if e!=0 then
                    integer k = find(base,bases)
                    if k=0 then
                        bases = append(bases,base)
                        k = length(bases)
                    end if
                    types[e] &= k
                end if
            end if
        end for
        default_enc = iff(length(types[2])=1?bases[types[2][1]]&".1te":"")
        default_dec = iff(length(types[3])=1?bases[types[3][1]]&".1td":"")
    else
        for i=1 to length(d) do
            sequence di = d[i]
            string name = di[D_NAME],
                   info = iff(name[$]='p'?sprintf("Size: %d",di[D_SIZE])
                         :sprintf("Size: %d, Used: %d",size_used(name)))
            printf(1,"%s  Created: %02d/%02d/%04d, %s\n",
                     {name,di[D_DAY],di[D_MONTH],di[D_YEAR],info})
        end for
    end if
end procedure
 
procedure dump_hex(sequence s)
    if length(s)=0 then
        printf(1,"{}\n")
    else
        for i=1 to length(s) do
            printf(1,"%02x%c",{s[i],iff(mod(i,16)=0?'\n':' ')})
        end for
    end if
end procedure
 
procedure dump_file(string file, bool ashex=false)
    printf(1,"The contents of %s:\n\n",{file})
    if match(".1tp",file) or ashex then
        integer fn = open(file,"rb")
        dump_hex(get_bytes(fn, 64))
        close(fn)
        puts(1,iff(get_file_size(file)>64?"...\n\n","\n"))
    else
        puts(1,join(read_lines(file),"\n")&"\n")
    end if
end procedure
 
procedure create_one_time_pad()
    string filename = prompt_string("OTP file name to create (without extension):")
    if find('.',filename) then
        puts(1,"extension not allowed\n")
    elsif filename!="" then
        string pfile = filename&".1tp",
               efile = filename&".1te",
               dfile = filename&".1td"
        if file_exists(pfile) then
            string del = lower(prompt_string("File already exists - delete?:"))
            if not find(del,{"y","yes"}) then return end if
            if not delete_file(pfile) then
                printf(1,"error deleting %s\n",{pfile})
                return
            end if
        end if
        string comment = prompt_string("comment (optional):")
        integer filesize
        while 1 do
            string sizestr = prompt_string("file size (1MB):")
            sequence res = iff(sizestr=""?{{1,"MB"}}:scanf(sizestr,"%d%s"))
            if length(res)=1 then
                integer k = find(upper(res[1][2]),{"","K","KB","M","MB","G","GB"})
                if k!=0 then
                    k = {1,1024,1024*1024,1024*1204}[floor(k/2)+1]
                    filesize = res[1][1]*k
                    exit
                end if
                printf(1,"unknown suffix:%s - use K|M|G or KB|MB|GB\n",{res[1][2]})
            end if
        end while
        printf(1,"Creating %s (%d bytes)\n",{pfile,filesize})
        integer fn = open(pfile,"wb")
        if fn=-1 then
            printf(1,"error opening %s\n",{pfile})
            return
        end if
        for i=1 to filesize do
            puts(fn,rand(256)-1)
        end for
        close(fn)
        fn = open(efile,"w")
        if fn=-1 then
            printf(1,"error opening %s\n",{efile})
            return
        end if
        printf(fn,"# OTP file\n# %s\nSize: %d\nUsed: 0\n",{comment,filesize})
        close(fn)
        if not copy_file(efile,dfile,true) then
            printf(1,"error copying %s to %s\n",{efile,dfile})
            return
        end if
        printf(1,"\n%s and %s and %s have been created in the current directory.\n\n",
                 {pfile,efile,dfile})
        dump_file(pfile)
        dump_file(efile)
        default_enc = efile
        default_dec = dfile
    end if
end procedure
 
procedure delete_one_time_pad()
    string filename = prompt_string("OTP file name to delete (without extension):")
    if find('.',filename) then
        puts(1,"extension not allowed\n")
    elsif filename!="" then
        string efile = filename&".1te",
               dfile = filename&".1td"
        filename &= ".1tp"
        if not file_exists(filename) then
            printf(1,"file %s found\n",{filename})
            return
        end if
        string del = lower(prompt_string("Delete file - are you sure?:"))
        if not find(del,{"y","yes"}) then
            printf(1,"file not deleted\n")
            return
        end if
        if not delete_file(filename) then
            printf(1,"error deleting %s\n",{filename})
            return
        end if
        integer c = 1
        c += delete_file(efile)
        c += delete_file(dfile)
        printf(1,"%d file%s successfully deleted.\n",{c,iff(c=1?"":"s")})
        if default_enc=efile then default_enc = "" end if
        if default_dec=dfile then default_dec = "" end if
    end if
end procedure
 
sequence msg = {}
 
procedure crypt_text(integer de)
    sequence text = iff(de='d'?msg:prompt_string("message:"))
    string dflt = iff(de='d'?default_dec:default_enc),
           otp = iff(length(dflt)?"OTP ("&dflt&"):":"OTP:"),
           efile = prompt_string(otp)
    if efile="" then efile=dflt end if
    if length(efile) then
        if not find('.',efile) then efile &= ".1t"&de end if
        integer {size,used} = size_used(efile)
        if size=-1 or used=-1 then
            printf(1,"File %s cannot be opened\n",{efile})
        elsif used+length(text)>size then
            printf(1,"OTP exhausted (%d>%d)\n",{used+length(text),size})
        else
            efile[$] = 'p'
            integer fn = open(efile,"rb")
            if seek(fn,used)!=SEEK_OK then
                printf(1,"Error in seek(%s,%d)\n",{efile,fn})
                text = ""
            else    
                msg = get_bytes(fn,length(text))
                msg = sq_xor_bits(msg,text)
                if de='e' then
                    printf(1,"Encrypted:\n")
                    dump_hex(msg)
                    printf(1,"\n\n")
                else
                    printf(1,"Decrypted: %s\n\n",{msg})
                end if
            end if
            close(fn)
            efile[$] = de
            if length(text)!=0 then
                {} = size_used(efile, used+length(text))
            end if
            dump_file(efile)
        end if
    end if
end procedure
 
string last_file = ""
 
procedure crypt_file(integer de)
    string file = iff(de='d' and last_file!=""?"file ("&last_file&"):":"file:"),
           filename = prompt_string(file)
    if de='d' and filename="" then filename=last_file end if
    if filename="" then return end if
    if not file_exists(filename) then
        printf(1,"The file %s does not exist\n",{filename})
        return
    end if
    atom filesize = get_file_size(filename)
    integer fn = open(filename,"rb")
    if fn=-1 then
        printf(1,"error opening %s\n",{filename})
        return
    end if
    sequence bytes = get_bytes(fn,filesize)
    close(fn)
    if length(bytes)!=filesize then ?9/0 end if -- uh?
    if de='e' then
        dump_file(filename, true)
    end if
    string outfile = prompt_string("output:")
    if length(outfile)=0 then return end if
    integer outfn = open(outfile,"wb")
    if outfn=-1 then
        printf(1,"error opening output file %s\n",{outfile})
        return
    end if
    string dflt = iff(de='d'?default_dec:default_enc),
           otp = iff(length(dflt)?"OTP ("&dflt&"):":"OTP:"),
           efile = prompt_string(otp)
    if efile="" then efile=dflt end if
    if length(efile) then
        if not find('.',efile) then efile &= ".1t"&de end if
        integer {size,used} = size_used(efile)
        if size=-1 or used=-1 then
            printf(1,"File %s cannot be opened\n",{efile})
        elsif used+filesize>size then
            printf(1,"OTP exhausted (%d>%d)\n",{used+filesize,size})
        else
            efile[$] = 'p'
            fn = open(efile,"rb")
            if seek(fn,used)!=SEEK_OK then
                printf(1,"Error in seek(%s,%d)\n",{efile,fn})
                filesize = 0
            else    
                msg = get_bytes(fn,filesize)
                msg = sq_xor_bits(msg,bytes)
                puts(outfn,msg)
            end if
            close(fn)
            close(outfn)
            efile[$] = de
            if filesize!=0 then
                {} = size_used(efile, used+filesize)
            end if
            if de='d' then
                dump_file(outfile, true)
            else
                last_file = outfile
            end if
            dump_file(efile)
        end if
    end if
end procedure
 
list_one_time_pads(set_default:=true)
 
-- note: the trailing space after the opening """ is deliberate:
constant menu_txt = """ 
1. List one time pad files.
2. Create one time pad file.
3. Delete one time pad file.
4. Encrypt plain text.
5. Decrypt cipher text.
6. Encrypt file.
7. Decrypt file.
8. Quit program.
Your choice (1 to 8) : """
 
while 1 do
    integer choice = prompt_number(menu_txt,{1,8})
    switch choice do
        case 1: list_one_time_pads()
        case 2: create_one_time_pad()
        case 3: delete_one_time_pad()
        case 4: crypt_text('e')
        case 5: crypt_text('d')
        case 6: crypt_file('e')
        case 7: crypt_file('d')
        case 8: exit
        default :puts(1,"not implemented...\n")
    end switch
end while
Output:

Sample session. Similar to Kotlin, menu display/selection has been shorted to <menu N. Xxxx.>

No one time pad files found.

1. List one time pad files.
2. Create one time pad file.
3. Delete one time pad file.
4. Encrypt plain text.
5. Decrypt cipher text.
6. Encrypt file.
7. Decrypt file.
8. Quit program.
Your choice (1 to 8) : 2
OTP file name to create (without extension):007
comment (optional):Pussy Galore
file size (1MB):
Creating 007.1tp (1048576 bytes)

007.1tp and 007.1te and 007.1td have been created in the current directory.

The contents of 007.1tp:

C3 A1 90 B2 70 E6 A2 DD 27 F4 3C 21 48 D1 54 1C
D7 7E AA 09 40 CC 49 83 F4 E3 96 86 1D 48 15 F6
A3 C2 5F 47 00 BE D1 6B 4E 4A EA 50 DB F8 62 2F
D5 66 D5 0F A9 63 E6 4D 65 BB 67 70 6F 51 34 73
...

The contents of 007.1te:

# OTP file
# Pussy Galore
Size: 1048576
Used: 0

<menu 1. List one time pad files.>
007.1td  Created: 10/11/2018, Size: 1048576, Used: 0
007.1te  Created: 10/11/2018, Size: 1048576, Used: 0
007.1tp  Created: 10/11/2018, Size: 1048576

<menu 4. Encrypt plain text.>
message:This is message 1234
OTP (007.1te):
Encrypted:
97 C9 F9 C1 50 8F D1 FD 4A 91 4F 52 29 B6 31 3C
E6 4C 99 3D

The contents of 007.1te:

# OTP file
# Pussy Galore
Size: 1048576
Used: 20

<menu 5. Decrypt cipher text.>
OTP (007.1td):
Decrypted: This is message 1234

The contents of 007.1td:

# OTP file
# Pussy Galore
Size: 1048576
Used: 20

<menu 8. Quit program.>