diff --git a/crypto/README.md b/crypto/README.md index 98c1e7f..2a3d834 100644 --- a/crypto/README.md +++ b/crypto/README.md @@ -3,15 +3,22 @@ ## Functions - `md5(string)` - return md5 checksum from string. - `sha256(string)` - return sha256 checksum from string. -- `aes_encrypt(string, string, string, string)` - return AES encrypted hex-encoded ciphertext -- `aes_decrypt(string, string, string, string)` - return AES decrypted hex-encoded plain text +- `aes_encrypt(string, string, string, string)` - return AES encrypted binary ciphertext +- `aes_decrypt(string, string, string, string)` - return AES decrypted binary text +- `aes_encrypt_hex(string, string, string, string)` - return AES encrypted hex-encoded ciphertext +- `aes_decrypt_hex(string, string, string, string)` - return AES decrypted hex-encoded plain text -AES support 3 modes: GCM, CBC, and CTR - first parameter is mode, second is hex-encoded key, third is hex-encoded initialization vector or nonce - depending on the mode, and forth is hex-encoded plain text or ciphertext. +AES support 3 modes: GCM, CBC, and CTR - first parameter is mode, second is hex-encoded key, third is hex-encoded +initialization vector or nonce - depending on the mode, and forth is hex-encoded plain text or ciphertext. + +Since lua strings are binary safe, you can use any binary data as input and output and, for your convenience, the +library also provides hex-encoded versions of the encrypt and decrypt functions. The first argument (the mode string) +can be one of the following: "GCM", "CBC", or "CTR" (case-insensitive) and is not hex-encoded for the hex variants. ## Examples ```lua -local crypto = require("crypto") +local crypto = require 'crypto' -- md5 if not(crypto.md5("1\n") == "b026324c6904b2a9cb4b88d6d61c81d1") then @@ -23,18 +30,46 @@ if not(crypto.sha256("1\n") == "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9 error("sha256") end ---- aes encrypt in GCM mode -s, err = crypto.aes_encrypt(1, "86e15cbc1cbf510d8f2e51d4b63a2144", "b6b86d581a991a652158bd10", "48656c6c6f20776f726c64") +--- aes encrypt in GCM mode with hex-encoded data +s, err = crypto.aes_encrypt_hex(crypto.GCM, "86e15cbc1cbf510d8f2e51d4b63a2144", "b6b86d581a991a652158bd10", "48656c6c6f20776f726c64") +assert(not err, err) if not(s == "7ec4e38508a26abf7b46e8dc90a7299d5144bcf045e460c3ef6b3e") then error("encrypt AES") end -assert(not err, err) ---- aes decrypt in GCM mode -s, err = crypto.aes_decrypt(1, "86e15cbc1cbf510d8f2e51d4b63a2144", "b6b86d581a991a652158bd10", "7ec4e38508a26abf7b46e8dc90a7299d5144bcf045e460c3ef6b3e") +--- aes decrypt in GCM mode with hex-encoded data +s, err = crypto.aes_decrypt_hex(crypto.GCM, "86e15cbc1cbf510d8f2e51d4b63a2144", "b6b86d581a991a652158bd10", "7ec4e38508a26abf7b46e8dc90a7299d5144bcf045e460c3ef6b3e") +assert(not err, err) if not(s == "48656c6c6f20776f726c64") then - error("decrypt AES) + error("decrypt AES") +end + +--- Binary examples setup of binary strings equivalent to the hex-encoded strings above: +local hex = require 'hex' +local key, iv, plaintext, encrypted, err +key, err = hex.decode_string('86e15cbc1cbf510d8f2e51d4b63a2144') +assert(not err, err) +iv, err = hex.decode_string('b6b86d581a991a652158bd10') +assert(not err, err) +plaintext, err = hex.decode_string('48656c6c6f20776f726c64') +assert(not err, err) +s, err = crypto.aes_encrypt(crypto.GCM, key, iv, plaintext) +assert(not err, err) +encrypted, err = hex.decode_string("7ec4e38508a26abf7b46e8dc90a7299d5144bcf045e460c3ef6b3e") +assert(not err, err) + +--- aes encrypt binary in GCM mode +s, err = crypto.aes_encrypt(crypto.GCM, key, iv, plaintext) +assert(not err, err) +if not(s == encrypted) then + error("encrypt AES") end + +--- aes decrypt in GCM mode +s, err = crypto.aes_decrypt(crypto.GCM, key, iv, encrypted) assert(not err, err) +if not(s == plaintext) then + error("decrypt AES") +end ``` \ No newline at end of file diff --git a/crypto/api.go b/crypto/api.go index 24af0de..ba0e2c7 100644 --- a/crypto/api.go +++ b/crypto/api.go @@ -28,6 +28,26 @@ func SHA256(L *lua.LState) int { // AESEncrypt implements AES encryption in Lua. func AESEncrypt(l *lua.LState) int { + modeStr := l.CheckString(1) + m, err := parseString(modeStr) + if err != nil { + l.ArgError(1, err.Error()) + } + key := []byte(l.CheckString(2)) + iv := []byte(l.CheckString(3)) + data := []byte(l.CheckString(4)) + enc, err := encryptAES(m, key, iv, data) + if err != nil { + l.Push(lua.LNil) + l.Push(lua.LString(fmt.Sprintf("failed to encrypt: %v", err))) + return 2 + } + l.Push(lua.LString(enc)) + return 1 +} + +// AESEncryptHex implements AES encryption in Lua. +func AESEncryptHex(l *lua.LState) int { m, key, iv, data, err := decodeParams(l) if err != nil { l.Push(lua.LNil) @@ -47,6 +67,26 @@ func AESEncrypt(l *lua.LState) int { // AESDecrypt implement AES decryption in Lua. func AESDecrypt(l *lua.LState) int { + modeStr := l.CheckString(1) + m, err := parseString(modeStr) + if err != nil { + l.ArgError(1, err.Error()) + } + key := []byte(l.CheckString(2)) + iv := []byte(l.CheckString(3)) + data := []byte(l.CheckString(4)) + dec, err := decryptAES(m, key, iv, data) + if err != nil { + l.Push(lua.LNil) + l.Push(lua.LString(fmt.Sprintf("failed to decrypt: %v", err))) + return 2 + } + l.Push(lua.LString(dec)) + return 1 +} + +// AESDecryptHex implement AES decryption in Lua. +func AESDecryptHex(l *lua.LState) int { m, key, iv, data, err := decodeParams(l) if err != nil { l.Push(lua.LNil) diff --git a/crypto/api_test.go b/crypto/api_test.go index cade66f..dd6f7b1 100644 --- a/crypto/api_test.go +++ b/crypto/api_test.go @@ -4,10 +4,18 @@ import ( "testing" "github.com/stretchr/testify/assert" + "github.com/vadv/gopher-lua-libs/filepath" + "github.com/vadv/gopher-lua-libs/hex" + "github.com/vadv/gopher-lua-libs/ioutil" "github.com/vadv/gopher-lua-libs/tests" ) func TestApi(t *testing.T) { - preload := tests.SeveralPreloadFuncs(Preload) + preload := tests.SeveralPreloadFuncs( + Preload, + filepath.Preload, + hex.Preload, + ioutil.Preload, + ) assert.NotZero(t, tests.RunLuaTestFile(t, preload, "./test/test_api.lua")) } diff --git a/crypto/loader.go b/crypto/loader.go index a7dd662..e346c62 100644 --- a/crypto/loader.go +++ b/crypto/loader.go @@ -13,14 +13,20 @@ func Preload(L *lua.LState) { // Loader is the module loader function. func Loader(L *lua.LState) int { t := L.NewTable() + // Load the constants + for name := range modeNames { + t.RawSetString(name, lua.LString(name)) + } L.SetFuncs(t, api) L.Push(t) return 1 } var api = map[string]lua.LGFunction{ - "md5": MD5, - "sha256": SHA256, - "aes_encrypt": AESEncrypt, - "aes_decrypt": AESDecrypt, + "md5": MD5, + "sha256": SHA256, + "aes_encrypt_hex": AESEncryptHex, + "aes_decrypt_hex": AESDecryptHex, + "aes_encrypt": AESEncrypt, + "aes_decrypt": AESDecrypt, } diff --git a/crypto/test/data/1.data.bin b/crypto/test/data/1.data.bin new file mode 100644 index 0000000..70c379b --- /dev/null +++ b/crypto/test/data/1.data.bin @@ -0,0 +1 @@ +Hello world \ No newline at end of file diff --git a/crypto/test/data/1.expected.bin b/crypto/test/data/1.expected.bin new file mode 100644 index 0000000..7bd1457 --- /dev/null +++ b/crypto/test/data/1.expected.bin @@ -0,0 +1 @@ +�4� ����� \ No newline at end of file diff --git a/crypto/test/data/1.init.bin b/crypto/test/data/1.init.bin new file mode 100644 index 0000000..681c00d --- /dev/null +++ b/crypto/test/data/1.init.bin @@ -0,0 +1 @@ +�¿: ������ \ No newline at end of file diff --git a/crypto/test/data/1.key.bin b/crypto/test/data/1.key.bin new file mode 100644 index 0000000..6bf8cd8 --- /dev/null +++ b/crypto/test/data/1.key.bin @@ -0,0 +1 @@ +��\��Q �.QԶ:!D \ No newline at end of file diff --git a/crypto/test/test_api.lua b/crypto/test/test_api.lua index 4de4183..f25bbaa 100644 --- a/crypto/test/test_api.lua +++ b/crypto/test/test_api.lua @@ -1,5 +1,9 @@ local crypto = require("crypto") local assert = require 'assert' +local hex = require 'hex' +local require = require 'require' +local filepath = require 'filepath' +local ioutil = require 'ioutil' function TestMD5(t) local tests = { @@ -39,11 +43,11 @@ function TestSha256(t) end end -function TestAESEncrypt(t) +function TestAESEncryptHex(t) local tests = { { data = "48656c6c6f207w76f726c64", -- "Hello world" in hex - mode = "GCM", + mode = crypto.GCM, key = "86e15cbc1cbf510d8f2e51d4b63a2144", init = "b6b86d581a991a652158bd10", expected = nil, @@ -51,7 +55,7 @@ function TestAESEncrypt(t) }, { data = "48656c6c6f20776f726c64", -- "Hello world" in hex - mode = "GCM", + mode = crypto.GCM, key = "86e15cbc1cbf51d8f2e51d4b63a2144", init = "b6b86d581a991a652158bd10", expected = nil, @@ -59,7 +63,7 @@ function TestAESEncrypt(t) }, { data = "48656c6c6f20776f726c64", -- "Hello world" in hex - mode = "GCM", + mode = crypto.GCM, key = "86e15cbc1cbf510d8f2e51d4b63a2144", init = "b6b86d581a991a652158bd10", expected = "7ec4e38508a26abf7b46e8dc90a7299d5144bcf045e460c3ef6b3e", @@ -67,7 +71,7 @@ function TestAESEncrypt(t) }, { data = "48656c6c6f20776f726c64", -- "Hello world" in hex - mode = "GCM", + mode = crypto.GCM, key = "86e15cbc1cbf510d8f2e51d4b63a2144", init = "b6b86d581a991a652158bd010211", expected = nil, @@ -75,23 +79,23 @@ function TestAESEncrypt(t) }, { data = "48656c6c6f20776f726c64", -- "Hello world" in hex - mode = "GCM", + mode = crypto.GCM, key = "86e15cbc1cbf510d8f2e51d4b63a2144", init = "b6b86d581a991a652158bd010211", expected = nil, err = "failed to encrypt: incorrect GCM nonce size: 14, expected: 12", }, { - data = "48656c6c6f20776f726c64", -- "Hello world" in hex - mode = "cbc", - key = "86e15cbc1cbf510d8f2e51d4b63a2144", - init = "068bb92e032884ba8b260fa7d3a80005", + data = "48656c6c6f20776f726c64", -- "Hello world" in hex + mode = "cbc", + key = "86e15cbc1cbf510d8f2e51d4b63a2144", + init = "068bb92e032884ba8b260fa7d3a80005", expected = "dfba6f71cce4d4b76be301b577d9f095", - err = nil, + err = nil, }, { data = "48656c6c6f20776f726c64", -- "Hello world" in hex - mode = "CBC", + mode = crypto.CBC, key = "86e15cbc1cbf510d8f2e51d4b63a2144", init = "068bb92e03288884ba8b260fa7d3a80005", expected = nil, @@ -99,7 +103,7 @@ function TestAESEncrypt(t) }, { data = "48656c6c6f20776f726c64", -- "Hello world" in hex - mode = "CTR", + mode = crypto.CTR, key = "86e15cbc1cbf510d8f2e51d4b63a2144", init = "e3057fc2bf103a09a1b2c3d4e5f60718", expected = "138434a80bd7dcd9ee8adc", @@ -107,7 +111,7 @@ function TestAESEncrypt(t) }, { data = "48656c6c6f20776f726c64", -- "Hello world" in hex - mode = "CTR", + mode = crypto.CTR, key = "86e15cbc1cbf510d8f2e51d4b63a2144", init = "e3057fc2b9f103a909a1b2c3d4e5f60718", expected = nil, @@ -116,18 +120,18 @@ function TestAESEncrypt(t) } for _, tt in ipairs(tests) do t:Run("aes_encrypt in " .. tostring(tt.mode) .. " mode", function(t) - local got, err = crypto.aes_encrypt(tt.mode, tt.key, tt.init, tt.data) + local got, err = crypto.aes_encrypt_hex(tt.mode, tt.key, tt.init, tt.data) assert:Equal(t, tt.expected, got) assert:Equal(t, tt.err, err) end) end end -function TestAESDecrypt(t) +function TestAESDecryptHex(t) local tests = { { data = "7ec4e38508a26abf7b46e8dc90a7299d5144bcf045e460c3efwb3e", - mode = "GCM", + mode = crypto.GCM, key = "86e15cbc1cbf510d8f2e51d4b63a2144", init = "b6b86d581a991a652158bd10", expected = nil, @@ -135,7 +139,7 @@ function TestAESDecrypt(t) }, { data = "7ec4e38508a26abf7b46e8dc90a7299d5144bcf045e460c3ef6b3e", - mode = "GCM", + mode = crypto.GCM, key = "86e15cbc1cbf51d8f2e51d4b63a2144", init = "b6b86d581a991a652158bd10", expected = nil, @@ -143,7 +147,7 @@ function TestAESDecrypt(t) }, { data = "7ec4e38508a26abf7b46e8dc90a7299d5144bcf045e460c3ef6b3e", - mode = "GCM", + mode = crypto.GCM, key = "86e15cbc1cbf510d8f2e51d4b63a2144", init = "b6b86d581a991a652158bd10", expected = "48656c6c6f20776f726c64", -- "Hello world" in hex @@ -151,7 +155,7 @@ function TestAESDecrypt(t) }, { data = "7ec4e38508a26abf7b46e8dc90a7299d5144bcf045e460c3ef6b3e", - mode = "GCM", + mode = crypto.GCM, key = "86e15cbc1cbf510d8f2e51d4b63a2144", init = "b6b86d581a991a652158bd010211", expected = nil, @@ -167,7 +171,7 @@ function TestAESDecrypt(t) }, { data = "138434a80bd7dcd9ee8adc", - mode = "CTR", + mode = crypto.CTR, key = "86e15cbc1cbf510d8f2e51d4b63a2144", init = "e3057fc2bf103a09a1b2c3d4e5f60718", expected = "48656c6c6f20776f726c64", -- "Hello world" in hex @@ -176,9 +180,93 @@ function TestAESDecrypt(t) } for _, tt in ipairs(tests) do t:Run("aes_decrypt in " .. tostring(tt.mode) .. " mode", function(t) - local got, err = crypto.aes_decrypt(tt.mode, tt.key, tt.init, tt.data) + local got, err = crypto.aes_decrypt_hex(tt.mode, tt.key, tt.init, tt.data) assert:Equal(t, tt.expected, got) assert:Equal(t, tt.err, err) end) end end + +function TestAESEncrypt(t) + tests = { + { + data = "48656c6c6f20776f726c64", + mode = crypto.CBC, + key = "86e15cbc1cbf510d8f2e51d4b63a2144", + init = "068bb92e032884ba8b260fa7d3a80005", + expected = "dfba6f71cce4d4b76be301b577d9f095", + wantErr = false, + }, + } + for _, tt in ipairs(tests) do + local key, err = hex.decode_string(tt.key) + require:NoError(t, err) + local init, err = hex.decode_string(tt.init) + require:NoError(t, err) + local data, err = hex.decode_string(tt.data) + require:NoError(t, err) + local got, err = crypto.aes_encrypt(tt.mode, key, init, data) + if tt.wantErr then + require:Error(t, err) + return + end + require:NoError(t, err) + got = hex.encode_to_string(got) + assert:Equal(t, tt.err, err) + end +end + +function TestAESDecrypt(t) + tests = { + { + data = "138434a80bd7dcd9ee8adc", + mode = crypto.CTR, + key = "86e15cbc1cbf510d8f2e51d4b63a2144", + init = "e3057fc2bf103a09a1b2c3d4e5f60718", + expected = "48656c6c6f20776f726c64", -- "Hello world" in hex + wantErr = false, + }, + } + for _, tt in ipairs(tests) do + t:Run("aes_decrypt in " .. tostring(tt.mode) .. " mode", function(t) + local key, err = hex.decode_string(tt.key) + require:NoError(t, err) + local init, err = hex.decode_string(tt.init) + require:NoError(t, err) + local data, err = hex.decode_string(tt.data) + require:NoError(t, err) + local got, err = crypto.aes_decrypt(tt.mode, key, init, data) + if tt.wantErr then + require:Error(t, err) + return + end + require:NoError(t, err) + got, err = hex.encode_to_string(got) + require:NoError(t, err) + assert:Equal(t, tt.expected, got) + end) + end +end + +function TestAESCodecFile(t) + for i = 1, 1 do + local data, err = ioutil.read_file(filepath.join("test/data", tostring(i) .. ".data.bin")) + require:NoError(t, err) + local expected, err = ioutil.read_file(filepath.join("test/data", tostring(i) .. ".expected.bin")) + require:NoError(t, err) + local init, err = ioutil.read_file(filepath.join("test/data", tostring(i) .. ".init.bin")) + require:NoError(t, err) + local key, err = ioutil.read_file(filepath.join("test/data", tostring(i) .. ".key.bin")) + require:NoError(t, err) + t:Run("TestAESEncryptFile " .. tostring(i), function(t) + local got, err = crypto.aes_encrypt(crypto.CTR, key, init, data) + require:NoError(t, err) + assert:Equal(t, expected, got) + + local decrypted, err = crypto.aes_decrypt(crypto.CTR, key, init, got) + t:Logf('data: "%s", decrypted: "%s"', data, decrypted) + require:NoError(t, err) + assert:Equal(t, data, decrypted) + end) + end +end