Skip to content

Add AES support to crypto lib #69

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Jun 23, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 19 additions & 1 deletion crypto/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
## 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 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.

## Examples

Expand All @@ -18,5 +22,19 @@ end
if not(crypto.sha256("1\n") == "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865") then
error("sha256")
end
```

--- aes encrypt in GCM mode
s, err = crypto.aes_encrypt(1, "86e15cbc1cbf510d8f2e51d4b63a2144", "b6b86d581a991a652158bd10", "48656c6c6f20776f726c64")
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")
if not(s == "48656c6c6f20776f726c64") then
error("decrypt AES)
end
assert(not err, err)

```
163 changes: 163 additions & 0 deletions crypto/aes.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
package crypto

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/hex"
"fmt"
"strings"

lua "github.com/yuin/gopher-lua"
)

type mode uint

const (
GCM mode = iota + 1
CBC
CTR
)

var modeNames = map[string]mode{
"GCM": GCM,
"CBC": CBC,
"CTR": CTR,
}

func (m mode) String() string {
switch m {
case GCM:
return "GCM"
case CBC:
return "CBC"
case CTR:
return "CTR"
default:
return "unknown"
}
}

func parseString(s string) (mode, error) {
ret, ok := modeNames[strings.ToUpper(s)]
if !ok {
return 0, fmt.Errorf("invalid mode: %s", s)
}
return ret, nil
}

func decodeParams(l *lua.LState) (m mode, key, iv, data []byte, err error) {
modeString := l.ToString(1)
m, err = parseString(modeString)
if err != nil {
return 0, nil, nil, nil, err
}

keyStr := l.ToString(2)
key, err = hex.DecodeString(keyStr)
if err != nil {
return 0, nil, nil, nil, fmt.Errorf("failed to decode key: %v", err)
}

ivStr := l.ToString(3)
iv, err = hex.DecodeString(ivStr)
if err != nil {
return 0, nil, nil, nil, fmt.Errorf("failed to decode IV: %v", err)
}

dataStr := l.ToString(4)
data, err = hex.DecodeString(dataStr)
if err != nil {
return 0, nil, nil, nil, fmt.Errorf("failed to decode data: %v", err)
}
return m, key, iv, data, nil
}

// encryptAES implements AES encryption given mode, key, plaintext, and init value.
// Init value is either initialization vector or nonce, depending on the mode.
func encryptAES(m mode, key, init, plaintext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
switch m {
case GCM:
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
if len(init) != aesGCM.NonceSize() {
return nil, fmt.Errorf("incorrect GCM nonce size: %d, expected: %d", len(init), aesGCM.NonceSize())
}
ciphertext := aesGCM.Seal(nil, init, plaintext, nil)
return ciphertext, nil
case CBC:
if len(init) != block.BlockSize() {
return nil, fmt.Errorf("invalid IV size: %d, expected: %d", len(init), block.BlockSize())
}
padded := pad(plaintext, aes.BlockSize)
mode := cipher.NewCBCEncrypter(block, init)
ciphertext := make([]byte, len(padded))
mode.CryptBlocks(ciphertext, padded)
return ciphertext, nil
case CTR:
if len(init) != block.BlockSize() {
return nil, fmt.Errorf("invalid IV size: %d, expected: %d", len(init), block.BlockSize())
}
stream := cipher.NewCTR(block, init)
ciphertext := make([]byte, len(plaintext))
stream.XORKeyStream(ciphertext, plaintext)
return ciphertext, nil
default:
return nil, fmt.Errorf("unsupported mode: %d", m)
}
}

// decryptAES implements AES decryption given mode, key, ciphertext, and init value.
// Init value is either initialization vector or nonce, depending on the mode.
func decryptAES(m mode, key, init, ciphertext []byte) ([]byte, error) {
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
switch m {
case GCM:
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
l := len(init)
if l != aesGCM.NonceSize() {
return nil, fmt.Errorf("incorrect GCM nonce size: %d, expected: %d", len(init), aesGCM.NonceSize())
}
plaintext, err := aesGCM.Open(nil, init, ciphertext, nil)
if err != nil {
return nil, err
}
return plaintext, nil
case CBC:
if len(ciphertext)%aes.BlockSize != 0 {
return nil, fmt.Errorf("ciphertext is not a multiple of block size")
}
mode := cipher.NewCBCDecrypter(block, init)
plaintext := make([]byte, len(ciphertext))
mode.CryptBlocks(plaintext, ciphertext)
// Padding reversal is intentionally delegated to the application layer.
// On constrained devices with fixed-length payloads, padding is sometimes omitted
// to avoid unnecessary processing load and data overhead.
return plaintext, nil
case CTR:
stream := cipher.NewCTR(block, init)
plaintext := make([]byte, len(ciphertext))
stream.XORKeyStream(plaintext, ciphertext)
return plaintext, nil
default:
return nil, fmt.Errorf("unsupported mode: %s", m)
}
}

func pad(data []byte, blockSize int) []byte {
padLen := blockSize - len(data)%blockSize
padding := bytes.Repeat([]byte{byte(padLen)}, padLen)
return append(data, padding...)
}
40 changes: 40 additions & 0 deletions crypto/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package crypto
import (
"crypto/md5"
"crypto/sha256"
"encoding/hex"
"fmt"

lua "github.com/yuin/gopher-lua"
Expand All @@ -24,3 +25,42 @@ func SHA256(L *lua.LState) int {
L.Push(lua.LString(fmt.Sprintf("%x", hash)))
return 1
}

// AESEncrypt implements AES encryption in Lua.
func AESEncrypt(l *lua.LState) int {
m, key, iv, data, err := decodeParams(l)
if err != nil {
l.Push(lua.LNil)
l.Push(lua.LString(fmt.Sprintf("failed to decode params: %v", err)))
return 2
}

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(hex.EncodeToString(enc)))
return 1
}

// AESDecrypt implement AES decryption in Lua.
func AESDecrypt(l *lua.LState) int {
m, key, iv, data, err := decodeParams(l)
if err != nil {
l.Push(lua.LNil)
l.Push(lua.LString(fmt.Sprintf("failed to decode params: %v", err)))
return 2
}

dec, err := decryptAES(mode(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(hex.EncodeToString(dec)))
return 1
}
6 changes: 4 additions & 2 deletions crypto/api_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package crypto

import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/vadv/gopher-lua-libs/tests"
"testing"
)

func TestApi(t *testing.T) {
assert.NotZero(t, tests.RunLuaTestFile(t, Preload, "./test/test_api.lua"))
preload := tests.SeveralPreloadFuncs(Preload)
assert.NotZero(t, tests.RunLuaTestFile(t, preload, "./test/test_api.lua"))
}
12 changes: 6 additions & 6 deletions crypto/loader.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
package crypto

import (
lua "github.com/yuin/gopher-lua"
)
import lua "github.com/yuin/gopher-lua"

// Preload adds crypto to the given Lua state's package.preload table. After it
// has been preloaded, it can be loaded using require:
//
// local crypto = require("crypto")
// local crypto = require("crypto")
func Preload(L *lua.LState) {
L.PreloadModule("crypto", Loader)
}
Expand All @@ -21,6 +19,8 @@ func Loader(L *lua.LState) int {
}

var api = map[string]lua.LGFunction{
"md5": MD5,
"sha256": SHA256,
"md5": MD5,
"sha256": SHA256,
"aes_encrypt": AESEncrypt,
"aes_decrypt": AESDecrypt,
}
Loading