]> source.dussan.org Git - rspamd.git/commitdiff
[Feature] Add rspamadm secretbox command
authorIvan Stakhov <50211739+left-try@users.noreply.github.com>
Wed, 18 Sep 2024 16:10:59 +0000 (19:10 +0300)
committerGitHub <noreply@github.com>
Wed, 18 Sep 2024 16:10:59 +0000 (17:10 +0100)
* [Minor] Small fix for error messages

* [Feature] Create rspamadm util to decrypt header

* [Feature] Create python example to encrypt/decrypt header

* [Minor] Small clean up

* [Minor] Change c-rspamadm util to lua-rspamadm util

* [Minor] Small clean up

* [Minor] Add some debug

* [Feature] Add secretbox command

* [Minor] Debug

* [Minor] Add additional return for encrypted string(noce + encrypted string

* [Minor] Small debug

* [Minor] Add a way to provide encrypted text concatenated with nonce

* [Minor] Add nonce to encrypt text

* [Minor] Clean up

* [Minor] Clean up unused variable

* [Minor] Small fix

* [Minor] Fix return issue

* [Minor] Add blake2b for key derivation

* [Minor] Small upgrade to debug

* [Minor] Small clean up

* [Minor] Change return to more convenient form

* [Minor] Change print to test form

* [Test] Provide tests for encrypt/decrypt with rspamadm util and python script

* [Minor] Change python to python3

* [Minor] Add stderr check

* [Minor] Make the function return nonce+text

* [Minor] Change unit tests to new return format

* [Minor] Add flag to manage encodings

* [Minor] Add --encoding argument to manage encodings

* [Minor] Change tests for new input format

* [Minor] Fix lua format

* [Minor] Small fix

* [Minor] Provide full support for new return format of maybe_encrypt_header

* [Test] Test small fix

* [Test] Small fix

* [Minor] Clean up

* [Minor] Small fix for name of variable

* [Minor] Small clean up

* [Minor] Change format of command to a mre convenient

* [Minor] Change tests to be same as a format of a command

* [Minor] Change description of flags

* [Minor] Small fix

---------

Co-authored-by: Ivan Stakhov <50211739+LeftTry@users.noreply.github.com>
lualib/lua_util.lua
lualib/rspamadm/secretbox.lua [new file with mode: 0644]
test/functional/cases/150_rspamadm.robot
test/lua/unit/lua_util.maybe_encrypt_decrypt_header.lua
utils/encrypt_decrypt_header.py [new file with mode: 0644]

index ffc07842eb71e5561caed5577bc1c0fb813dc048..62b38c87e37734dad4aaabb94483751cd5245052 100644 (file)
@@ -1305,12 +1305,12 @@ exports.maybe_encrypt_header = function(header, settings, prefix)
   local rspamd_secretbox = require "rspamd_cryptobox_secretbox"
 
   if not header or header == '' then
-    logger.errx(rspamd_config, "Header: %s is empty or nil", header)
+    logger.errx(rspamd_config, "Header is empty or nil. Header: %s", header)
     return nil
   elseif settings[prefix .. '_encrypt'] then
     local key = settings[prefix .. '_key']
     if not key or key == '' then
-      logger.errx(rspamd_config, "Key: %s is empty or nil", key)
+      logger.errx(rspamd_config, "Key is empty or nil. Key: %s", key)
       return header
     end
     local cryptobox = rspamd_secretbox.create(key)
@@ -1322,7 +1322,8 @@ exports.maybe_encrypt_header = function(header, settings, prefix)
     else
       encrypted_header = cryptobox:encrypt(header, nonce)
     end
-    return encrypted_header, nonce
+    encrypted_header = nonce .. encrypted_header
+    return encrypted_header
   end
 end
 
@@ -1342,12 +1343,12 @@ exports.maybe_decrypt_header = function(encrypted_header, settings, prefix, nonc
   local rspamd_secretbox = require "rspamd_cryptobox_secretbox"
 
   if not encrypted_header or encrypted_header == '' then
-    logger.errx(rspamd_config, "Encoded header: %s is empty or nil")
+    logger.errx(rspamd_config, "Encrypted header is empty or nil. Encrypted header: %s", encrypted_header)
     return nil
   elseif settings[prefix .. '_encrypt'] then
     local key = settings[prefix .. '_key']
     if not key or key == '' then
-      logger.errx(rspamd_config, "Key: %s is empty or nil")
+      logger.errx(rspamd_config, "Key is empty or nil. Key: %s", key)
       return encrypted_header
     end
     local cryptobox = rspamd_secretbox.create(key)
diff --git a/lualib/rspamadm/secretbox.lua b/lualib/rspamadm/secretbox.lua
new file mode 100644 (file)
index 0000000..3ab10ce
--- /dev/null
@@ -0,0 +1,157 @@
+local util = require 'lua_util'
+local rspamd_util = require 'rspamd_util'
+local argparse = require 'argparse'
+
+local parser = argparse()
+    :name "secretbox"
+    :description "Encrypt/decrypt given text with given key and nonce"
+    :help_description_margin(32)
+    :command_target('command')
+    :require_command(true)
+
+parser:mutex(
+    parser:flag '-R --raw'
+          :description('Encrypted text(and nonce if it is there) will be given in raw'),
+    parser:flag '-H --hex'
+          :description('Encrypted text(and nonce if it is there) will be given in hex'),
+    parser:flag '-b --base32'
+          :description('Encrypted text(and nonce if it is there) will be given in base32'),
+    parser:flag '-B --base64'
+          :description('Encrypted text(and nonce if it is there) will be given in base64')
+)
+
+local decrypt = parser:command 'decrypt'
+                      :description 'Decrypt text with given key and nonce'
+
+decrypt:option "-t --text"
+       :description("Encrypted text(Base 64)")
+       :argname("<text>")
+decrypt:option "-k --key"
+       :description("Key used to encrypt text")
+       :argname("<key>")
+decrypt:option "-n --nonce"
+       :description("Nonce used to encrypt text(Base 64)")
+       :argname("<nonce>")
+       :default(nil)
+
+local encrypt = parser:command 'encrypt'
+                      :description 'Encrypt text with given key'
+
+encrypt:option "-t --text"
+       :description("Text to encrypt")
+       :argname("<text>")
+encrypt:option "-k --key"
+       :description("Key to encrypt text")
+       :argname("<key>")
+encrypt:option "-n --nonce"
+       :description("Nonce to encrypt text(Base 64)")
+       :argname("<nonce>")
+       :default(nil)
+
+local function set_up_encoding(args, type, text)
+    local function fromhex(str)
+        return (str:gsub('..', function (cc)
+            return string.char(tonumber(cc, 16))
+        end))
+    end
+
+    local function tohex(str)
+        return (str:gsub('.', function (c)
+            return string.format('%02X', string.byte(c))
+        end))
+    end
+
+    local output = text
+
+    if type == 'encode' then
+        if args.hex then
+            output = tohex(text)
+        elseif args.base32 then
+            output = rspamd_util.encode_base32(text)
+        elseif args.base64 then
+            output = rspamd_util.encode_base64(text)
+        end
+    elseif type == 'decode' then
+        if args.hex then
+            output = fromhex(text)
+        elseif args.base32 then
+            output = rspamd_util.decode_base32(text)
+        elseif args.base64 then
+            output = rspamd_util.decode_base64(text)
+        end
+    end
+
+    return output
+end
+
+local function decryption_handler(args)
+    local settings = {
+        prefix = 'dec',
+        dec_encrypt = true,
+        dec_key = args.key
+    }
+
+    local decrypted_header = ''
+    if(args.nonce ~= nil) then
+        local decoded_text = set_up_encoding(args, 'decode', tostring(args.text))
+        local decoded_nonce = set_up_encoding(args, 'decode', tostring(args.nonce))
+
+        decrypted_header = util.maybe_decrypt_header(decoded_text, settings, settings.prefix, decoded_nonce)
+    else
+        local text_with_nonce = set_up_encoding(args, 'decode', tostring(args.text))
+        local nonce = string.sub(tostring(text_with_nonce), 1, 24)
+        local text = string.sub(tostring(text_with_nonce), 25)
+
+        decrypted_header = util.maybe_decrypt_header(text, settings, settings.prefix, nonce)
+    end
+
+    if decrypted_header ~= nil then
+        print(decrypted_header)
+    else
+        print('The decryption failed. Please check the correctness of the arguments given.')
+    end
+end
+
+local function encryption_handler(args)
+    local settings = {
+        prefix = 'dec',
+        dec_encrypt = true,
+        dec_key = args.key,
+    }
+
+    if args.nonce ~= nil then
+        settings.dec_nonce = set_up_encoding(args, 'decode', tostring(args.nonce))
+    end
+
+    local encrypted_text = util.maybe_encrypt_header(args.text, settings, settings.prefix)
+
+    if encrypted_text ~= nil then
+        print(set_up_encoding(args, 'encode', tostring(encrypted_text)))
+    else
+        print('The encryption failed. Please check the correctness of the arguments given.')
+    end
+end
+
+local command_handlers = {
+    decrypt = decryption_handler,
+    encrypt = encryption_handler,
+}
+
+local function handler(args)
+    local cmd_opts = parser:parse(args)
+
+    local f = command_handlers[cmd_opts.command]
+    if not f then
+        parser:error(string.format("command isn't implemented: %s",
+                cmd_opts.command))
+    end
+    f(cmd_opts)
+end
+
+
+return {
+    name = 'secret_box',
+    aliases = { 'secretbox', 'secret_box' },
+    handler = handler,
+    description = parser._description
+}
\ No newline at end of file
index 6bff14b2e3775816c52d5fdaad029da47dd51968..257b0b50138767c5725ec9fae1249afe77bef06a 100644 (file)
@@ -4,6 +4,13 @@ Suite Teardown  Rspamadm Teardown
 Library         ${RSPAMD_TESTDIR}/lib/rspamd.py
 Resource        ${RSPAMD_TESTDIR}/lib/rspamd.robot
 
+*** Variables ***
+${TEXT}                text
+${KEY}                 12345678901234567890123456789012
+${NONCE}               9pyeEd986hrjcpozCIZ41jEo6dCDbgjg
+${ENCRYPTED_TEXT}      8KGF6VLI7vnweUdR8FuQZuT+ID8=
+${PYTHON_SCRIPT}       ${RSPAMD_TESTDIR}/../../utils/encrypt_decrypt_header.py
+
 *** Test Cases ***
 Config Test
   ${result} =  Rspamadm  configtest
@@ -46,3 +53,43 @@ Verbose mode
   Should Match Regexp  ${result.stderr}  ^$
   Should Match Regexp  ${result.stdout}  hello world\n
   Should Be Equal As Integers  ${result.rc}  0
+  
+SecretBox rspamadm encrypt/decrypt
+  ${result} =  Rspamadm  secret_box  -B  encrypt  -t  ${TEXT}  -k  ${KEY}  -n  ${NONCE}
+  Should Match Regexp  ${result.stderr}  ^$
+  Should Be Equal As Strings  ${result.stdout}  ${NONCE}${ENCRYPTED_TEXT}
+  ${result1} =  Rspamadm  secret_box  -B  decrypt  -t  ${ENCRYPTED_TEXT}  -k  ${KEY}  -n  ${NONCE}
+  Should Match Regexp  ${result.stderr}  ^$
+  Should Be Equal As Strings  ${result1.stdout}  ${TEXT}
+
+SecretBox python encrypt/decrypt
+  ${result} =  Run Process  python3  ${PYTHON_SCRIPT}  -B  encrypt  -t  ${TEXT}  -k  ${KEY}  -n  ${NONCE}
+  Should Match Regexp  ${result.stderr}  ^$
+  Should Be Equal As Strings  ${result.stdout}  ${NONCE}${ENCRYPTED_TEXT}
+  ${result1} =  Run Process  python3  ${PYTHON_SCRIPT}  -B  decrypt  -t  ${NONCE}${ENCRYPTED_TEXT}  -k  ${KEY}
+  Should Match Regexp  ${result.stderr}  ^$
+  Should Be Equal As Strings  ${result1.stdout}  ${TEXT}
+  
+SecretBox encrypt python with nonce decrypt rspamadm
+  ${result} =  Run Process  python3  ${PYTHON_SCRIPT}  -B  encrypt  -t  ${TEXT}  -k  ${KEY}  -n  ${NONCE}
+  ${result1} =  Rspamadm  secret_box  -B  decrypt  -t  ${result.stdout}  -k  ${KEY}
+  Should Match Regexp  ${result.stderr}  ^$
+  Should Be Equal As Strings  ${TEXT}  ${result1.stdout}
+
+SecretBox encrypt python without nonce decrypt rspamadm
+  ${result} =  Run Process  python3  ${PYTHON_SCRIPT}  -B  encrypt  -t  ${TEXT}  -k  ${KEY}
+  ${result1} =  Rspamadm  secret_box  -B  decrypt  -t  ${result.stdout}  -k  ${KEY}
+  Should Match Regexp  ${result.stderr}  ^$
+  Should Be Equal As Strings  ${TEXT}  ${result1.stdout}
+
+SecretBox encrypt rspamadm with nonce decrypt python
+  ${result} =  Rspamadm  secret_box  -B  encrypt  -t  ${TEXT}  -k  ${KEY}  -n  ${NONCE}
+  ${result1} =  Run Process  python3  ${PYTHON_SCRIPT}  -B  decrypt  -t  ${result.stdout}  -k  ${KEY}
+  Should Match Regexp  ${result.stderr}  ^$
+  Should Be Equal As Strings  ${TEXT}  ${result1.stdout}
+
+SecretBox encrypt rspamadm without nonce decrypt python
+  ${result} =  Rspamadm  secret_box  -B  encrypt  -t  ${TEXT}  -k  ${KEY}
+  ${result1} =  Run Process  python3  ${PYTHON_SCRIPT}  -B  decrypt  -t  ${result.stdout}  -k  ${KEY}
+  Should Match Regexp  ${result.stderr}  ^$
+  Should Be Equal As Strings  ${TEXT}  ${result1.stdout}
index 61310106838daf29687286bc3be70434a5c628a0..ef31f5e9b05f297f4f2171482609a9144adfc143 100644 (file)
@@ -15,7 +15,8 @@ context("Lua util - maybe encrypt/decrypt header", function()
             assert_true(false, 'Failed to encrypt header')
         end
 
-        local decrypted_header = util.maybe_decrypt_header(encrypted_header, settings, settings.prefix)
+        local text = string.sub(tostring(encrypted_header), 6)
+        local decrypted_header = util.maybe_decrypt_header(text, settings, settings.prefix)
         if decrypted_header == encrypted_header or decrypted_header == nil then
             assert_true(false, 'Failed to decrypt header')
         end
@@ -36,13 +37,15 @@ context("Lua util - maybe encrypt/decrypt header", function()
             prefix_key = 'key'
         }
 
-        local encrypted_header, nonce = util.maybe_encrypt_header(header, settings, settings.prefix)
+        local encrypted_header = util.maybe_encrypt_header(header, settings, settings.prefix)
         if encrypted_header == header or encrypted_header == nil then
             assert_true(false, 'Failed to encrypt header')
         end
 
-        local decrypted_header = util.maybe_decrypt_header(encrypted_header, settings,
-                settings.prefix, nonce)
+        local nonce = string.sub(tostring(encrypted_header), 1, 24)
+        local text = string.sub(tostring(encrypted_header), 25)
+        local decrypted_header = util.maybe_decrypt_header(text, settings, settings.prefix, nonce)
+
         if decrypted_header == encrypted_header or decrypted_header == nil then
             assert_true(false, 'Failed to decrypt header')
         end
diff --git a/utils/encrypt_decrypt_header.py b/utils/encrypt_decrypt_header.py
new file mode 100644 (file)
index 0000000..5f2ea75
--- /dev/null
@@ -0,0 +1,97 @@
+import argparse
+import base64
+
+import nacl.encoding
+from nacl.secret import SecretBox
+from nacl.hash import blake2b
+
+
+def create_secret_box(key):
+    key = blake2b(key, encoder=nacl.encoding.RawEncoder)
+    box = SecretBox(key)
+    return box
+
+def encrypt_text(header, key, nonce):
+    box = create_secret_box(key)
+    if nonce is not None:
+        encrypted_header = box.encrypt(header, nonce=nonce)
+    else:
+        encrypted_header = box.encrypt(header)
+    return encrypted_header
+
+def decrypt_text(encrypted_header, key):
+    box = create_secret_box(key)
+    decrypted_header = box.decrypt(encrypted_header)
+    return decrypted_header
+
+def set_encoding(args, type_, text):
+    output = text
+    if type_ == 'encode':
+        if args.hex:
+            output = base64.b16encode(text)
+        elif args.base32:
+            output = base64.b32encode(text)
+        elif args.base64:
+            output = base64.b64encode(text)
+    elif type_ == 'decode':
+        if args.hex:
+            output = base64.b16decode(text)
+        elif args.base32:
+            output = base64.b32decode(text)
+        elif args.base64:
+            output = base64.b64decode(text)
+    return output
+
+def set_up_parser_args():
+    new_parser = argparse.ArgumentParser(description="Encrypt or Decrypt a text.")
+    enc_group = new_parser.add_mutually_exclusive_group()
+
+    enc_group.add_argument("-r", "--raw", action="store_true",
+                           help="Encrypted text(and nonce if it is there) will be given in raw")
+    enc_group.add_argument("-H", "--hex", action="store_true",
+                           help="Encrypted text(and nonce if it is there) will be given in hex")
+    enc_group.add_argument("-b", "--base32", action="store_true",
+                           help="Encrypted text(and nonce if it is there) will be given in base32")
+    enc_group.add_argument("-B", "--base64", action="store_true",
+                           help="Encrypted text(and nonce if it is there) will be given in base64")
+
+    subparsers = new_parser.add_subparsers(dest="command", help="encrypt or decrypt")
+
+    encrypt_parser = subparsers.add_parser("encrypt", help="Encrypt a text")
+    encrypt_parser.add_argument("-t", "--text", type=str, required=True, help="Text to encrypt")
+    encrypt_parser.add_argument("-k", "--key", type=str, required=True, help="Encryption key")
+    encrypt_parser.add_argument("-n", "--nonce", type=str, required=False, help="Encryption nonce")
+
+    decrypt_parser = subparsers.add_parser("decrypt", help="Decrypt a text")
+    decrypt_parser.add_argument("-t", "--encrypted_text", type=str, required=True, help="Encrypted text")
+    decrypt_parser.add_argument("-k", "--key", type=str, required=True, help="Decryption key")
+
+    args = new_parser.parse_args()
+    return args
+
+def main():
+    args = set_up_parser_args()
+
+    if args.command == "encrypt":
+        text = args.text.encode()
+        key = args.key.encode()
+        if args.nonce is not None:
+            nonce = set_encoding(args, 'decode', args.nonce)
+        else:
+            nonce = None
+
+        encrypted_text = encrypt_text(text, key, nonce)
+        if args.raw:
+            print(set_encoding(args, 'encode', encrypted_text))
+        else:
+            print(set_encoding(args, 'encode', encrypted_text).decode())
+
+    elif args.command == "decrypt":
+        encrypted_text = set_encoding(args, 'decode', args.encrypted_text)
+        key = args.key.encode()
+
+        decrypted_text = decrypt_text(encrypted_text, key)
+        print(decrypted_text.decode())
+
+if __name__ == "__main__":
+    main()
\ No newline at end of file