Arduino와 Golang간 AES 암호화통신 구현하기
IoT용으로 원격에서 아두이노 컨트롤 기능을 HTTPS위에 Rest API로 구현을 했었는데
아두이노를 산간오지에 둬야하다보니
데이터통신요금을 줄이고자 TCP소켓통신으로 변경.
암호화가 필요할거 같아 AES암호화를 적용하면서 좌충우돌했던 경험담을 정리합니다.
<준비물>
Wemos D1 mini
Golang돌릴수 있는 PC
<참고사이트>
아두이노쪽 : https://github.com/kakopappa/arduino-esp8266-aes-lib
golang 서버: http://pyrasis.com/book/GoForTheReallyImpatient/Unit53/02
처음엔 결과값이 달라서 라이브러리만 찾으러다니다 몇가지 사실을 알게됐음.
정리하자면 AES128 CBC를 쓰는 라이브러리인데 pkcs7으로 패딩해줍니다.
다만 암호화할 대상 문자열을 base64인코딩부터 해준다음 AES암호화 합니다.
좀 더 부연설명하자면 암호화는 아래와 같이 이뤄집니다.
암호화할 대상이 “text”, 암호화키는 아두이노와 서버간 이미 공유했다고 가정
- “text”를 base64로 인코딩 “dGV4dA==”을 편의상 b64_text 변수라 가정
- AES암호화를 위해 랜덤 IV생성 (IV가 뭔지 찾아보시면 좋을 것 같아요)
- b64_text를 AES 암호화 키와 IV를 이용해서 암호화 함 => encrypted_text
- IV와 encrypted_text 문자열을 합쳐 다시 base64로 인코딩
- b64_encrypted_text 전송
복호화는 당연히 반대순이겠죠? ㅎㅎ
아두이노 쪽입니다.
bool auth_server() {
if (!is_connected()) {
Serial.println("Client doesn't connect.");
return false;
}
String json_data = "";
StaticJsonDocument<200> doc;
doc["cmd"] = "a";
doc["id"] = ESP.getChipId();
doc["mac"] = WiFi.macAddress();
serializeJson(doc, json_data);
String enc_data = encrypt(json_data);
Serial.println("auth " + enc_data);
client.print(enc_data);
delay(3000); // wait for reply
Serial.print("AuthReceived: ");
if (client.available()) {
String line = client.readStringUntil('\n');
Serial.println(line);
return true;
}
else {
Serial.println("can't wait");
return false;
}
}
위 코드는 golang서버와 인증하는 부분인데
사용할 때 encrypt()이 1~5단계를 수행해줍니다.
호출부분은 encrypt(json_data) 입니다
encrypt와 decrypt는 아래에 있습니다.
String encrypt(String msg) {
char b64data[200];
byte cipher[500];
byte iv [N_BLOCK] ;
// The unitialized Initialization vector
byte my_iv[N_BLOCK] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0};
// Our message to encrypt. Static for this example.
//String msg = "{\"data\":{\"value\":300}, \"SEQN\":700 , \"msg\":\"IT WORKS!!\" }";
aes.set_key( key , sizeof(key)); // Get the globally defined key
gen_iv( my_iv ); // Generate a random IV
// Print the IV
base64_encode( b64data, (char *)my_iv, N_BLOCK);
char pre_iv[16];
//Serial.println(sizeof(my_iv));
//Serial.print(" iv : ");
for (int i=0; i<sizeof(my_iv); i++) {
if (my_iv[i]<16) {
//Serial.print(0,HEX);
}
//Serial.print(my_iv[i], HEX);
pre_iv[i] = my_iv[i];
}
Serial.println("");
//Serial.println(String(b64data));
/*Serial.print(" key:");
for (int i=0; i<sizeof(key); i++) {
if (key[i]<16) {
Serial.print(0,HEX);
}
Serial.print(key[i], HEX);
}
Serial.println("");*/
//Serial.println(" Message: " + msg );
int b64len = base64_encode(b64data, (char *)msg.c_str(),msg.length());
// Serial.println (" Message in B64: " + String(b64data) );
//Serial.println (" The lenght is: " + String(b64len) );
//aes.do_aes_encrypt((byte *)(char *)msg.c_str(), msg.length() , cipher, key, 128, my_iv);
aes.do_aes_encrypt((byte *)b64data, b64len , cipher, key, 128, my_iv);
//Serial.println("Encryption done!");
int iv_cipher_size = sizeof(my_iv) + aes.get_size();
char* iv_cipher = (char*)malloc(sizeof(char) * iv_cipher_size);
for (int i=0; i<sizeof(pre_iv); i++) {
if (pre_iv[i]<16) {
//Serial.print(0,HEX);
}
//Serial.print(pre_iv[i], HEX);
iv_cipher[i] = pre_iv[i];
}
//Serial.println("");
//strcpy(iv_cipher, (char*)cipher); // PANIC
/*
Serial.print("Cipher: ");
for (int i=0; i<aes.get_size(); i++) {
if (cipher[i] <16) {
Serial.print(0, HEX);
}
Serial.print(cipher[i], HEX);
}
Serial.println("");
*/
for (int i=0; i<aes.get_size();i++) {
iv_cipher[i+16] = cipher[i];
}
//Serial.println("Cipher size: " + String(aes.get_size()));
//base64_encode(b64data, (char *)cipher, aes.get_size());
base64_encode(b64data, iv_cipher, iv_cipher_size);
//Serial.println ("Encrypted data in base64: " + String(b64data) );
//Serial.println("");
Serial.print("Encrypted: " + String(b64data));
/*
for (int i=0; i<iv_cipher_size; i++) {
if (iv_cipher[i]<16) {
Serial.print(0, HEX);
}
Serial.print(iv_cipher[i], HEX);
}
Serial.println("");
*/
free(iv_cipher);
return String(b64data);
}
String decrypt(String cipher_b64) {
char b64_decoded[200];
byte msg[500];
byte iv [N_BLOCK] ;
byte cipher[500];
aes.set_key( key , sizeof(key)); // Get the globally defined key
int encrypted_length = base64_decode(b64_decoded, (char *)cipher_b64.c_str(), cipher_b64.length());
int cipher_length = encrypted_length-16;
//Serial.print("iv: ");
for (int i=0; i<sizeof(iv); i++) {
iv[i] = b64_decoded[i];
if (b64_decoded[i] < 16) {
//Serial.print(0, HEX);
}
//Serial.print(b64_decoded[i], HEX);
}
//Serial.println("");
//Serial.print("Cipher: ");
for (int i=0; i<cipher_length; i++) {
cipher[i] = b64_decoded[i+16];
if (cipher[i] <16) {
//Serial.print(0, HEX);
}
//Serial.print(cipher[i], HEX);
}
//Serial.println();
aes.do_aes_decrypt((byte*)cipher, cipher_length, msg, key, 128, iv);
base64_decode(b64_decoded, (char*)msg, aes.get_size());
String plain_text = String(b64_decoded);
//Serial.println("Plain: " + plain_text);
return plain_text;
}
Golang 서버쪽입니다.
package lib
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"io"
"log"
"strings"
)
func encrypt(b cipher.Block, plaintext []byte) []byte {
plaintext = pkcs7Padding(plaintext)
/*if mod := len(plaintext) % aes.BlockSize; mod != 0 { // 블록 크기의 배수가 되어야함
padding := make([]byte, aes.BlockSize-mod) // 블록 크기에서 모자라는 부분을
plaintext = append(plaintext, padding...) // 채워줌
}*/
ciphertext := make([]byte, aes.BlockSize+len(plaintext)) // 초기화 벡터 공간(aes.BlockSize)만큼 더 생성
iv := ciphertext[:aes.BlockSize] // 부분 슬라이스로 초기화 벡터 공간을 가져옴
if _, err := io.ReadFull(rand.Reader, iv); err != nil { // 랜덤 값을 초기화 벡터에 넣어줌
fmt.Println(err)
return nil
}
mode := cipher.NewCBCEncrypter(b, iv) // 암호화 블록과 초기화 벡터를 넣어서 암호화 블록 모드 인스턴스 생성
mode.CryptBlocks(ciphertext[aes.BlockSize:], plaintext) // 암호화 블록 모드 인스턴스로
// 암호화
return ciphertext
}
func decrypt(b cipher.Block, ciphertext []byte) []byte {
if len(ciphertext)%aes.BlockSize != 0 { // 블록 크기의 배수가 아니면 리턴
fmt.Println("암호화된 데이터의 길이는 블록 크기의 배수가 되어야합니다.")
return nil
}
iv := ciphertext[:aes.BlockSize] // 부분 슬라이스로 초기화 벡터 공간을 가져옴
ciphertext = ciphertext[aes.BlockSize:] // 부분 슬라이스로 암호화된 데이터를 가져옴
plaintext := make([]byte, len(ciphertext)) // 평문 데이터를 저장할 공간 생성
mode := cipher.NewCBCDecrypter(b, iv) // 암호화 블록과 초기화 벡터를 넣어서
// 복호화 블록 모드 인스턴스 생성
mode.CryptBlocks(plaintext, ciphertext) // 복호화 블록 모드 인스턴스로 복호화
return plaintext
}
func Decrypt(CipherText []byte, key []byte) ([]byte, error) {
cipher, _ := base64.StdEncoding.DecodeString(string(CipherText))
if len(cipher) < 32 {
// b64디코딩했는데 최소 크기(aes 128bit=16bytes / iv+cipher=32bytes)보다 작으면 잘못된거죠
return []byte(""), errors.New("invalid ciphertext len")
}
block, err := aes.NewCipher([]byte(key)) // AES 대칭키 암호화 블록 생성
if err != nil {
fmt.Println(err)
return nil, err
}
plaintext := decrypt(block, cipher)
//log.Printf("%s\n",plaintext)
unpaded, _ := pkcs7UnPadding(plaintext)
//fmt.Printf("%s\n", unpaded)
tp, _ := base64DecodeStripped(string(unpaded))
//tp,_ := base64DecodeStripped(string(plaintext))
//fmt.Println(string(tp))
return tp, nil
}
func Encrypt(plainText []byte, key []byte) ([]byte, error) {
log.Printf("PlainText: %s\t", string(plainText))
plainTextb64 := base64.StdEncoding.EncodeToString(plainText)
block, err := aes.NewCipher([]byte(key)) // AES 대칭키 암호화 블록 생성
if err != nil {
fmt.Println(err)
return []byte(""), err
}
ciphertext := encrypt(block, []byte(plainTextb64)) // 평문을 AES 알고리즘으로 암호화
cipherb64 := base64.StdEncoding.EncodeToString(ciphertext)
return []byte(cipherb64), nil
}
func pkcs7Padding(src []byte) []byte {
padding := aes.BlockSize - len(src)%aes.BlockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(src, padtext...)
}
func pkcs7UnPadding(src []byte) ([]byte, error) {
length := len(src)
unpadding := int(src[length-1])
if unpadding > aes.BlockSize || unpadding == 0 {
return nil, errors.New("Invalid pkcs7 padding (unpadding > aes.BlockSize || unpadding == 0)")
}
pad := src[len(src)-unpadding:]
for i := 0; i < unpadding; i++ {
if pad[i] != byte(unpadding) {
return nil, errors.New("Invalid pkcs7 padding (pad[i] != unpadding)")
}
}
return src[:(length - unpadding)], nil
}
func base64DecodeStripped(s string) ([]byte, error) {
if i := len(s) % 4; i != 0 {
s += strings.Repeat("=", 4-i)
}
decoded, err := base64.StdEncoding.DecodeString(s)
return decoded, err
}
참고 사이트에 있던 내용에 base64와 pkcs를 추가해서 조금 수정했습니다.
실제 사용은 Encrypt와 Decrypt를 호출해서 사용하면 되고
사용예제는 아래 lib.Decrypt 부분입니다
data := recvBuf[:n]
log.Printf("%s\n", data)
if len(data) > 20 {
data, err = lib.Decrypt(data, key)
if err != nil {
// error handle
log.Printf("error: %s\n", err)
conn.Close()
return
} else {
log.Println(string(data))
}
} else {
log.Println("Data Strange")
//conn.Write([]byte("Data strange")) // TODO 필요하면 암호화해서 전송해야함
conn.Close()
return
}

