A Lisp implemented in AWK
# SPDX-License-Identifier: BSD-2-Clause

BEGIN {
    if(!KEY_FILE) {
        KEY_FILE = "/usr/local/etc/lacrum/key"
    }
    # ciphertext must be base64-encoded (as well as, you know,
    # encrypted)
    ENCRYPT_COMMAND = ("openssl enc    -aes-256-ctr -pbkdf2 -a -kfile " KEY_FILE)
    DECRYPT_COMMAND = ("openssl enc -d -aes-256-ctr -pbkdf2 -a -kfile " KEY_FILE)
    S_REDACTED = "(redacted)"
    S_ENCRYPTED = "(encrypted)"
}

# eval cases for encrypt, decrypt, unsafe-reveal are in string.awk.
# eval.awk also has a case for when an encrypted string is evaluated.

function _obscure_string_in_array(arr, n) {
    gsub(/./, "x", arr[n])
}

function _decrypt_encrypted_string(n, env, d, hdlrs, st, errp,    line, out, status, dcmd, tfn, oors) {
    # attempts to write via pipe and read via pipe from the same
    # subprocess have failed. so we use a temporary file. note that we
    # put the ciphertext, not the plaintext, in the temporary file.
    if( (status = ("mktemp" | getline tfn)) < 0) {
        return _error(_cons(_string("could not mktemp"),_nil()))
    }
    close("mktemp")
    if(!tfn)
        return _error(_cons(_string("no filename resulted from mktemp"),
                            _nil()), env, d, hdlrs, st, errp)
    # ok now we have a temp file name
    print _ENCRYPTED_STRING[n] > tfn
    close(tfn)
    dcmd = DECRYPT_COMMAND " < " tfn
    while( (status = (dcmd | getline line)) > 0 ) {
        out = out line
    }
    close(dcmd)
    if(status < 0) {
        return _error(_cons(_string("decrypting an encrypted string failed"),
                            _nil()), env, d, hdlrs, st, errp)
    }
    _SECRET_STRING[n] = out
    _TYPE[n] = "S"
    _obscure_string_in_array(_ENCRYPTED_STRING, n)
    delete _ENCRYPTED_STRING[n]
    system("rm -f " tfn)
    return n
}

function _encrypt_string_base(typename, arr, n, env, d, hdlrs, st, errp,      line, out, status, ecmd, tfn, oors) {
    # attempts to write via pipe and read via pipe from the same
    # subprocess have failed. so we use a temporary file. note that we
    # put the ciphertext, not the plaintext, in the temporary file.
    if( (status = ("mktemp" | getline tfn)) < 0) {
        return _error(_cons(_string("could not mktemp"),_nil()))
    }
    close("mktemp")
    if(!tfn)
        return _error(_cons(_string("no filename resulted from mktemp"),
                            _nil()), env, d, hdlrs, st, errp)
    # ok now we have a temp file name
    ecmd = ENCRYPT_COMMAND " > " tfn
    oors = ORS
    ORS = ""
    print arr[n] | ecmd
    ORS = oors
    close(ecmd)
    while( (status = (getline line < tfn)) > 0 ) {
        out = out line
    }
    close(tfn)
    if(!out) {
        return _error(_cons(_string("encrypting a " typename " failed"),
                            _nil()))
    }
    _ENCRYPTED_STRING[n] = out
    _TYPE[n] = "e"
    _obscure_string_in_array(arr, n)
    delete arr[n]
    system("rm -f " tfn)
    return n
}

function _encrypt_secret_string(n, env, d, hdlrs, st, errp) {
    return _encrypt_string_base("secret string", _SECRET_STRING, n,
                                env, d, hdlrs, st, errp)
}

function _encrypt_string(n, env, d, hdlrs, st, errp) {
    return _encrypt_string_base("string", _STRING, n,
                                env, d, hdlrs, st, errp)
}

function _encrypt(n, env, d, hdlrs, st, errp,    t) {
    if(_atom_awk(n)) {
        if(!_is_literal(n)) {
            t = _TYPE[n]
            if(t == "S")
                return _encrypt_secret_string(n)
            else if(t == "s")
                return _encrypt_string(n)
            else if(t == "e")
                return _error(_cons(_string("cannot encrypt an encrypted string"),
                                    _nil()), env, d, hdlrs, st, errp)
        }
    }
    return _error(_cons(_string("cannot encrypt %s: " \
                                "need a string or secret string"),
                        _cons(_string(_repr(n)),
                              _nil())),
                  env, d, hdlrs, st, errp)
}

function _decrypt(n, env, d, hdlrs, st, errp,     t) {
    if(_atom_awk(n)) {
        if(!_is_literal(n)) {
            t = _TYPE[n]
            if(t == "e")
                return _decrypt_encrypted_string(n, env, d, hdlrs, st, errp)
        }
    }
    return _error(_cons(_string("cannot decrypt %s: " \
                                "need an encrypted string"),
                        _cons(_string(_repr(n)),
                              _nil())),
                  env, d, hdlrs, st, errp)
}

function _unsafe_reveal(n, env, d, hdlrs, st, errp,     t) {
    if(_atom_awk(n)) {
        if(!_is_literal(n)) {
            t = _TYPE[n]
            if(t == "S") {
                _STRING[n] = _SECRET_STRING[n]
                _TYPE[n] = "s"
                _obscure_string_in_array(_SECRET_STRING, n)
                delete _SECRET_STRING[n]
                return n
            } else if(t == "s") {
                return n
            } else if(t == "e") {
                return _unsafe_reveal(_decrypt(n), env, d, hdlrs, st, errp)
            }
        }
    }
    return _error(_cons(_string("cannot reveal %s: " \
                                "need a secret string"),
                        _cons(_string(_repr(n)),
                              _nil())),
                  env, d, hdlrs, st, errp)
}

function _reveal_encrypted(n, env, d, hdlrs, st, errp,     t) {
    if(_atom_awk(n)) {
        if(!_is_literal(n)) {
            t = _TYPE[n]
            if(t == "s") {
                n = _encrypt_string(n, env, d, hdlrs, st, errp)
                if(_is_null(n)) return n  # an error happened
                t = _TYPE[n]
            } else if(t == "S") {
                n = _encrypt_secret_string(n, env, d, hdlrs, st, errp)
                if(_is_null(n)) return n  # an error happened
                t = _TYPE[n]
            }
            # not else if. after the above, or because it was already,
            # t should now be "e".
            if(t == "e") {
                return _string(_ENCRYPTED_STRING[n])
            }
        }
    }
    # if we have not returned, n was or is a weird type
    return _error(_cons(_string("cannot reveal-encrypted %s: must be a string or secret string"),
                        _cons(n, _nil())),
                  env, d, hdlrs, st, errp)
}