Skip to content

Commit ee35ee5

Browse files
authored
Add AES support to crypto lib (#69)
* Add AES encryption and decryption to crypto package Signed-off-by: Dusan Borovcanin <[email protected]>
1 parent ccb2795 commit ee35ee5

File tree

6 files changed

+412
-16
lines changed

6 files changed

+412
-16
lines changed

crypto/README.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@
33
## Functions
44
- `md5(string)` - return md5 checksum from string.
55
- `sha256(string)` - return sha256 checksum from string.
6+
- `aes_encrypt(string, string, string, string)` - return AES encrypted hex-encoded ciphertext
7+
- `aes_decrypt(string, string, string, string)` - return AES decrypted hex-encoded plain text
8+
9+
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.
610

711
## Examples
812

@@ -18,5 +22,19 @@ end
1822
if not(crypto.sha256("1\n") == "4355a46b19d348dc2f57c046f8ef63d4538ebb936000f3c9ee954a27460dd865") then
1923
error("sha256")
2024
end
21-
```
2225

26+
--- aes encrypt in GCM mode
27+
s, err = crypto.aes_encrypt(1, "86e15cbc1cbf510d8f2e51d4b63a2144", "b6b86d581a991a652158bd10", "48656c6c6f20776f726c64")
28+
if not(s == "7ec4e38508a26abf7b46e8dc90a7299d5144bcf045e460c3ef6b3e") then
29+
error("encrypt AES")
30+
end
31+
assert(not err, err)
32+
33+
--- aes decrypt in GCM mode
34+
s, err = crypto.aes_decrypt(1, "86e15cbc1cbf510d8f2e51d4b63a2144", "b6b86d581a991a652158bd10", "7ec4e38508a26abf7b46e8dc90a7299d5144bcf045e460c3ef6b3e")
35+
if not(s == "48656c6c6f20776f726c64") then
36+
error("decrypt AES)
37+
end
38+
assert(not err, err)
39+
40+
```

crypto/aes.go

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
package crypto
2+
3+
import (
4+
"bytes"
5+
"crypto/aes"
6+
"crypto/cipher"
7+
"encoding/hex"
8+
"fmt"
9+
"strings"
10+
11+
lua "github.com/yuin/gopher-lua"
12+
)
13+
14+
type mode uint
15+
16+
const (
17+
GCM mode = iota + 1
18+
CBC
19+
CTR
20+
)
21+
22+
var modeNames = map[string]mode{
23+
"GCM": GCM,
24+
"CBC": CBC,
25+
"CTR": CTR,
26+
}
27+
28+
func (m mode) String() string {
29+
switch m {
30+
case GCM:
31+
return "GCM"
32+
case CBC:
33+
return "CBC"
34+
case CTR:
35+
return "CTR"
36+
default:
37+
return "unknown"
38+
}
39+
}
40+
41+
func parseString(s string) (mode, error) {
42+
ret, ok := modeNames[strings.ToUpper(s)]
43+
if !ok {
44+
return 0, fmt.Errorf("invalid mode: %s", s)
45+
}
46+
return ret, nil
47+
}
48+
49+
func decodeParams(l *lua.LState) (m mode, key, iv, data []byte, err error) {
50+
modeString := l.ToString(1)
51+
m, err = parseString(modeString)
52+
if err != nil {
53+
return 0, nil, nil, nil, err
54+
}
55+
56+
keyStr := l.ToString(2)
57+
key, err = hex.DecodeString(keyStr)
58+
if err != nil {
59+
return 0, nil, nil, nil, fmt.Errorf("failed to decode key: %v", err)
60+
}
61+
62+
ivStr := l.ToString(3)
63+
iv, err = hex.DecodeString(ivStr)
64+
if err != nil {
65+
return 0, nil, nil, nil, fmt.Errorf("failed to decode IV: %v", err)
66+
}
67+
68+
dataStr := l.ToString(4)
69+
data, err = hex.DecodeString(dataStr)
70+
if err != nil {
71+
return 0, nil, nil, nil, fmt.Errorf("failed to decode data: %v", err)
72+
}
73+
return m, key, iv, data, nil
74+
}
75+
76+
// encryptAES implements AES encryption given mode, key, plaintext, and init value.
77+
// Init value is either initialization vector or nonce, depending on the mode.
78+
func encryptAES(m mode, key, init, plaintext []byte) ([]byte, error) {
79+
block, err := aes.NewCipher(key)
80+
if err != nil {
81+
return nil, err
82+
}
83+
switch m {
84+
case GCM:
85+
aesGCM, err := cipher.NewGCM(block)
86+
if err != nil {
87+
return nil, err
88+
}
89+
if len(init) != aesGCM.NonceSize() {
90+
return nil, fmt.Errorf("incorrect GCM nonce size: %d, expected: %d", len(init), aesGCM.NonceSize())
91+
}
92+
ciphertext := aesGCM.Seal(nil, init, plaintext, nil)
93+
return ciphertext, nil
94+
case CBC:
95+
if len(init) != block.BlockSize() {
96+
return nil, fmt.Errorf("invalid IV size: %d, expected: %d", len(init), block.BlockSize())
97+
}
98+
padded := pad(plaintext, aes.BlockSize)
99+
mode := cipher.NewCBCEncrypter(block, init)
100+
ciphertext := make([]byte, len(padded))
101+
mode.CryptBlocks(ciphertext, padded)
102+
return ciphertext, nil
103+
case CTR:
104+
if len(init) != block.BlockSize() {
105+
return nil, fmt.Errorf("invalid IV size: %d, expected: %d", len(init), block.BlockSize())
106+
}
107+
stream := cipher.NewCTR(block, init)
108+
ciphertext := make([]byte, len(plaintext))
109+
stream.XORKeyStream(ciphertext, plaintext)
110+
return ciphertext, nil
111+
default:
112+
return nil, fmt.Errorf("unsupported mode: %d", m)
113+
}
114+
}
115+
116+
// decryptAES implements AES decryption given mode, key, ciphertext, and init value.
117+
// Init value is either initialization vector or nonce, depending on the mode.
118+
func decryptAES(m mode, key, init, ciphertext []byte) ([]byte, error) {
119+
block, err := aes.NewCipher(key)
120+
if err != nil {
121+
return nil, err
122+
}
123+
switch m {
124+
case GCM:
125+
aesGCM, err := cipher.NewGCM(block)
126+
if err != nil {
127+
return nil, err
128+
}
129+
l := len(init)
130+
if l != aesGCM.NonceSize() {
131+
return nil, fmt.Errorf("incorrect GCM nonce size: %d, expected: %d", len(init), aesGCM.NonceSize())
132+
}
133+
plaintext, err := aesGCM.Open(nil, init, ciphertext, nil)
134+
if err != nil {
135+
return nil, err
136+
}
137+
return plaintext, nil
138+
case CBC:
139+
if len(ciphertext)%aes.BlockSize != 0 {
140+
return nil, fmt.Errorf("ciphertext is not a multiple of block size")
141+
}
142+
mode := cipher.NewCBCDecrypter(block, init)
143+
plaintext := make([]byte, len(ciphertext))
144+
mode.CryptBlocks(plaintext, ciphertext)
145+
// Padding reversal is intentionally delegated to the application layer.
146+
// On constrained devices with fixed-length payloads, padding is sometimes omitted
147+
// to avoid unnecessary processing load and data overhead.
148+
return plaintext, nil
149+
case CTR:
150+
stream := cipher.NewCTR(block, init)
151+
plaintext := make([]byte, len(ciphertext))
152+
stream.XORKeyStream(plaintext, ciphertext)
153+
return plaintext, nil
154+
default:
155+
return nil, fmt.Errorf("unsupported mode: %s", m)
156+
}
157+
}
158+
159+
func pad(data []byte, blockSize int) []byte {
160+
padLen := blockSize - len(data)%blockSize
161+
padding := bytes.Repeat([]byte{byte(padLen)}, padLen)
162+
return append(data, padding...)
163+
}

crypto/api.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ package crypto
44
import (
55
"crypto/md5"
66
"crypto/sha256"
7+
"encoding/hex"
78
"fmt"
89

910
lua "github.com/yuin/gopher-lua"
@@ -24,3 +25,42 @@ func SHA256(L *lua.LState) int {
2425
L.Push(lua.LString(fmt.Sprintf("%x", hash)))
2526
return 1
2627
}
28+
29+
// AESEncrypt implements AES encryption in Lua.
30+
func AESEncrypt(l *lua.LState) int {
31+
m, key, iv, data, err := decodeParams(l)
32+
if err != nil {
33+
l.Push(lua.LNil)
34+
l.Push(lua.LString(fmt.Sprintf("failed to decode params: %v", err)))
35+
return 2
36+
}
37+
38+
enc, err := encryptAES(m, key, iv, data)
39+
if err != nil {
40+
l.Push(lua.LNil)
41+
l.Push(lua.LString(fmt.Sprintf("failed to encrypt: %v", err)))
42+
return 2
43+
}
44+
l.Push(lua.LString(hex.EncodeToString(enc)))
45+
return 1
46+
}
47+
48+
// AESDecrypt implement AES decryption in Lua.
49+
func AESDecrypt(l *lua.LState) int {
50+
m, key, iv, data, err := decodeParams(l)
51+
if err != nil {
52+
l.Push(lua.LNil)
53+
l.Push(lua.LString(fmt.Sprintf("failed to decode params: %v", err)))
54+
return 2
55+
}
56+
57+
dec, err := decryptAES(mode(m), key, iv, data)
58+
if err != nil {
59+
l.Push(lua.LNil)
60+
l.Push(lua.LString(fmt.Sprintf("failed to decrypt: %v", err)))
61+
return 2
62+
}
63+
64+
l.Push(lua.LString(hex.EncodeToString(dec)))
65+
return 1
66+
}

crypto/api_test.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package crypto
22

33
import (
4+
"testing"
5+
46
"github.com/stretchr/testify/assert"
57
"github.com/vadv/gopher-lua-libs/tests"
6-
"testing"
78
)
89

910
func TestApi(t *testing.T) {
10-
assert.NotZero(t, tests.RunLuaTestFile(t, Preload, "./test/test_api.lua"))
11+
preload := tests.SeveralPreloadFuncs(Preload)
12+
assert.NotZero(t, tests.RunLuaTestFile(t, preload, "./test/test_api.lua"))
1113
}

crypto/loader.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,11 @@
11
package crypto
22

3-
import (
4-
lua "github.com/yuin/gopher-lua"
5-
)
3+
import lua "github.com/yuin/gopher-lua"
64

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

2321
var api = map[string]lua.LGFunction{
24-
"md5": MD5,
25-
"sha256": SHA256,
22+
"md5": MD5,
23+
"sha256": SHA256,
24+
"aes_encrypt": AESEncrypt,
25+
"aes_decrypt": AESDecrypt,
2626
}

0 commit comments

Comments
 (0)