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 }