#    _____ ___  ____     ____  _____ ____ ____  _____ _____ 
#   |_   _/ _ \|  _ \   / ___|| ____/ ___|  _ \| ____|_   _|
#     | || | | | |_) |  \___ \|  _|| |   | |_) |  _|   | |
#     | || |_| |  __/    ___) | |__| |___|  _ <| |___  | |
#     |_| \___/|_|      |____/|_____\____|_| \_\_____| |_|



# OFFENSIVEencfile.nim


#   Modified version of the original "encfile.nim".
#   This fork will be only supports encryption and not decryption.
#   However, it encodes all information necessary to decrypt using "encfile.nim"
#   It has no error handling. Good luck. In case of problem, check your password length.
#                               - Eline.
# Decryption HAS been tested as of 28 November 2023 (initial release).

# TODO: use sysargs to encrypt files :p
# i.e ./offensiveencfile input output password
# or maybe only get password interactively. hmm...

# WARNING:
#    This program purges command history on Windows. 

import nimcrypto
import std/sysrand
import std/streams
const
    # default encryption/decryption buffer size - 64KB
    bufferSizeDef = 64 * 1024
    # maximum password length (number of chars)
    # AES block size in bytes
    AESBlockSize = 16

# password stretching function
proc stretch(passw: string, iv1: array[16, byte]): array[32, byte] =
    
    var digest: array[32, byte]
    copyMem(addr digest[0], unsafeAddr iv1[0], len(iv1))

    var passwBytes: array[1024, byte]
    copyMem(addr passwBytes[0], unsafeAddr passw[0], len(passw))
    
    for i in 1 .. 8192:
      var sha: sha512_256
      sha.init()
      sha.update(digest)  
      sha.update(passwBytes[0..len(passw)-1])
      digest = sha.finish().data

    return digest


# encrypt binary stream function
# arguments:
# fIn: input binary stream
# fOut: output binary stream
# passw: encryption password
# bufferSize: encryption buffer size, must be a multiple of
#             AES block size (16)
#             using a larger buffer speeds up things when dealing
#             with long streams
proc encryptStream*(fIn: Stream, fOut: Stream, passw: string, bufferSize: int) =

    # validate bufferSize
    if bufferSize mod AESBlockSize != 0:
        raise newException(OSError, "Buffer size must be a multiple of AES block size.")
    

    # generate external iv (used to encrypt the main iv and the
    # encryption key)
    # let iv1 = urandom(AESBlockSize)
    let initIv1 = urandom(AESBlockSize)
    var iv1: array[16, byte]
    for i in [0..15]:
        iv1[i]=initIv1[i]
    
    # stretch password and iv
    let key = stretch(passw, iv1)
    
    # generate random main iv
    var iv0 = urandom(AESBlockSize)
    
    # generate random internal key
    var intKey = urandom(32)
    
    # instantiate AES cipher
    var encryptor0: CBC[aes256]
    encryptor0.init(intKey, iv0)
    
    # instantiate HMAC-SHA256 for the ciphertext
    var hmac0: HMAC[sha512_256]
    hmac0.init(intKey)

    # instantiate another AES cipher
    var encryptor1: CBC[aes256]
    encryptor1.init(key, iv1)
    
    # encrypt main iv and key
    var plainText = newString(len(iv0)+len(intKey))
    var c_iv_key = newString(len(iv0)+len(intKey))
    copyMem(addr plainText[0], unsafeAddr iv0[0], len(iv0))
    copyMem(addr plainText[0+len(iv0)], unsafeAddr intKey[0], len(intKey))
    encryptor1.encrypt(plainText, c_iv_key)
    
    # calculate HMAC-SHA256 of the encrypted iv and key
    var hmac1: HMAC[sha512_256]
    hmac1.init(key)
    hmac1.update(c_iv_key)

    # write header
    fOut.write("AES")
    
    # write version (AES Crypt version 2 file format -
    # see https://www.aescrypt.com/aes_file_format.html)
    fOut.write([byte 2])
    
    # reserved byte (set to zero)
    fOut.write([byte 0])
    
    # setup "CREATED-BY" extension
    var cby = "Confidential "

    # write "CREATED-BY" extension length
    fOut.write([byte 0, cast[uint8](1+len("CREATED_BY")+len(cby))])
        
    # write "CREATED-BY" extension
    fOut.write("CREATED_BY")
    fOut.write([byte 0])
    fOut.write(cby)
    
    # write "container" extension length
    fOut.write([byte 0, 128])
    
    # write "container" extension
    for i in 1 .. 128:
        fOut.write([byte 0])
        
    # write end-of-extensions tag
    fOut.write([byte 0, 0])
    
    # write the iv used to encrypt the main iv and the
    # encryption key
    fOut.write(iv1)
    
    # write encrypted main iv and key
    fOut.write(c_iv_key)
    
    # write HMAC-SHA256 of the encrypted iv and key
    fOut.write(hmac1.finish())
    
    var fs16 = 0
    # encrypt file while reading it
    var fdata = newString(bufferSize)
    var cText = newString(bufferSize)
    while true:
        # try to read bufferSize bytes
        let bytesRead = fIn.readData(addr fdata[0], bufferSize)
        
        # check if EOF was reached
        if bytesRead < bufferSize:
            # file size mod 16, lsb positions
            fs16 = bytesRead mod AESBlockSize
            # pad data (this is NOT PKCS#7!)
            # ...unless no bytes or a multiple of a block size
            # of bytes was read
            var padLen: int
            if bytesRead mod AESBlockSize == 0:
                padLen = 0
            else:
                padLen = 16 - bytesRead mod AESBlockSize

            # todo handl the pading to get the nb AES block & file with padLen, restrict the input of encrypt to x block
#             fdata += bytes([padLen])*padLen
            for i in bytesRead..bytesRead+padLen:
                fdata[i]=cast[char](padLen)
                
            # encrypt data
            encryptor0.encrypt(fdata[0..bytesRead+padLen-1], cText)

            # update HMAC
            hmac0.update(cText[0..bytesRead+padLen-1])
            # write encrypted file content
            fOut.write(cText[0..bytesRead+padLen-1])
            break
        # ...otherwise a full bufferSize was read
        else:
            # encrypt data
            encryptor0.encrypt(fdata, cText)          
            # update HMAC
            hmac0.update(cText)
            # write encrypted file content
            fOut.write(cText)
    
    # write plaintext file size mod 16 lsb positions
    fOut.write(cast[uint8](fs16))
    
    # write HMAC-SHA256 of the encrypted file
    fOut.write(hmac0.finish())


# encrypt file function
# arguments:
# infile: plaintext file path
# outfile: ciphertext file path
# passw: encryption password
# bufferSize: optional buffer size, must be a multiple of
#             AES block size (16)
#             using a larger buffer speeds up things when dealing
#             with big files
#             Default is 64KB.
proc encryptFile*(infile: string, outfile: string, passw: string, bufferSize: int = bufferSizeDef) =
    let fIn =  newFileStream(infile, mode = fmRead)
    defer: fIn.close()
    let fOut = newFileStream(outfile, mode = fmWrite)
    defer: fOut.close()
    encryptStream(fIn, fOut, passw, bufferSize)



#encryptFile("dza.png", "file.aes", "long-and-random-password", 1024)
#decryptFile("file.aes", "fileDecrypt.png", "long-and-random-password", 1024)