[Flutter] 대칭키 암호화+복호화+무결성 검증 구현 (encrypt, crypto, cryptography, AES, CBC, HMAC, GCM)

박세훈·2025년 5월 21일

Flutter & Dart

목록 보기
9/10

유출되어선 안되는 민감한 정보나 중요한 정보는 보안을 위해서 암호화와 복호화, 무결성 검증이 들어가곤 합니다.
Flutter에서 대칭키를 이용하여 이를 구현하기 위한 방법을 몇 가지 찾아봤습니다.

  1. crypto+encrypt 패키지 사용 (AES-CBC + HMAC)
  2. cryptography 패키지 사용 (AES-GCM)

crypto+encrypt 패키지 사용 (AES-CBC + HMAC)

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 패키지 사용 (AES-GCM)

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을 사용할 것을 권장한다고도 합니다.

profile
초짜의 마음가짐

0개의 댓글