유출되어선 안되는 민감한 정보나 중요한 정보는 보안을 위해서 암호화와 복호화, 무결성 검증이 들어가곤 합니다.
Flutter에서 대칭키를 이용하여 이를 구현하기 위한 방법을 몇 가지 찾아봤습니다.
crypto: ^3.0.6
encrypt: ^5.0.1
AES 알고리즘의 CBC 모드로 암호화와 복호화를 구현하고,
HMAC 알고리즘으로 무결성을 검증합니다.
import 'dart:convert';
import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart'
// String data : 암호화하고자하는 데이터
// String keyString : 암호화/복호화에 사용할 키값
String encryptData(String data, String keyString) {
final key = Key.fromUtf8(keyString);
// iv: 무작위 초기화 값. CBC 모드 암호화에서 첫 블록 암호화를 위한 필수 요소.
final iv = IV.fromSecureRandom(16);
final encrypter = Encrypter(
AES(
key,
mode: AESMode.cbc,
),
);
final encrypted = encrypter.encrypt(data, iv: iv);
// 무결성 검증을 위한 HMAC
final hmac = Hmac(sha256, utf8.encode(keyString));
final digest = hmac.convert(utf8.encode(encrypted.base64));
final digestBase64 = base64.encode(digest.bytes);
// 'IV:암호화한 데이터:HMAC'을 base64로 인코딩한 형태
final result = '${iv.base64}:${encrypted.base64}:$digestBase64';
return result;
}
String decryptData(String receivedCode, String keyString) {
// 유효성 검사
final parts = receivedCode.split(':');
if (parts.length != 3) {
throw Exception('암호화 데이터의 형식이 올바르지 않습니다.');
}
final receivedIvBase64 = parts[0];
final receivedEncryptedBase64 = parts[1];
final receivedHmacBase64 = parts[2];
// 무결성 검증
// (시크릿 키, 암호화된 데이터를 기반으로 새로 만든 HMAC이 받은 HMAC과 같은지 체크)
final hmac = Hmac(sha256, utf8.encode(keyString));
final digest = hmac.convert(utf8.encode(receivedEncryptedBase64));
final digestBase64 = base64.encode(digest.bytes);
if (digestBase64 != receivedHmacBase64) {
return '무결성 검증에 실패했습니다.';
}
final key = Key.fromUtf8(keyString);
final iv = encrypt.IV.fromBase64(receivedIvBase64);
final encrypter = Encrypter(
AES(
key,
mode: AESMode.cbc,
),
);
// 복호화
final data = encrypter.decrypt64(receivedEncryptedBase64, iv: iv);
return data;
}
예를 들어 'flutter'를 암호화하면
IV:암호화한 데이터:HMAC의 형태로 return되고,
이 값을 복호화하면 flutter가 return됩니다.
cryptography: ^2.7.0
AES 알고리즘의 GCM 모드는 crpytography 패키지에서 구현 가능합니다.
GCM 모드의 경우에는 무결성 검증이 알고리즘 자체에 포함되어 있기 때문에 따로 HMAC을 이용해서 만들 필요가 없습니다.
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
// String data : 암호화하고자하는 데이터
// String keyString : 암호화/복호화에 사용할 키값
Future<String> encryptData(String data, String keyString) async {
final algorithm = AesGcm.with256bits();
final secretKey = SecretKey(utf8.encode(keyString));
// nonce: 무작위 초기화 값. GCM 모드 암호화에서 고유한 암호화를 보장하고 인증 태그 생성을 위한 필수 요소.
final nonce = algorithm.newNonce();
final secretBox = await algorithm.encrypt(
utf8.encode(data),
secretKey: secretKey,
nonce: nonce,
);
final cipherText = base64Encode(secretBox.cipherText);
final nonceBase64 = base64Encode(secretBox.nonce);
final macBase64 = base64Encode(secretBox.mac.bytes);
// 'Nonce:암호화한 데이터:MAC'을 base64로 인코딩한 형태
final result = '$nonceBase64:$cipherText:$macBase64';
return result;
}
Future<String> decryptData(String encrypted, String keyString) async {
// 유효성 검사
final parts = encrypted.split(':');
if (parts.length != 3) {
throw Exception('암호화 데이터의 형식이 올바르지 않습니다.');
}
final algorithm = AesGcm.with256bits();
final nonce = base64Decode(parts[0]);
final cipherText = base64Decode(parts[1]);
final mac = Mac(base64Decode(parts[2]));
final secretBox = SecretBox(cipherText, nonce: nonce, mac: mac);
final secretKey = SecretKey(utf8.encode(keyString));
// 무결성 검증 (실패할 경우 catch) 및 복호화
try {
final clearText =
await algorithm.decrypt(secretBox, secretKey: secretKey);
return utf8.decode(clearText);
} catch (e) {
return e.toString();
}
}
SecretBox라는 객체에 암호화한 데이터, Nonce, MAC이 포함되어 있어서 간편히 구현할 수 있었던 것 같습니다.
최근에는 AES-CBC + HMAC보다는 GCM을 사용할 것을 권장한다고도 합니다.