본문으로 건너뛰기
← 블로그로 돌아가기
튜토리얼 2026년 3월 12일

Rust로 AES-256-GCM 암호화 구현하기

KeyBox에서 사용한 AES-256-GCM 암호화 구현 과정. PBKDF2 키 파생부터 nonce 관리까지.

Rust AES-256 암호화 보안

왜 AES-256-GCM인가

시크릿 매니저에서 암호화 알고리즘을 고를 때 고려한 것은 세 가지다.

  • 기밀성: 데이터를 읽을 수 없어야 한다
  • 무결성: 변조를 감지할 수 있어야 한다
  • 성능: 로컬 앱에서 지연 없이 동작해야 한다

AES-256-GCM은 세 가지를 모두 만족한다. GCM 모드는 암호화와 인증을 동시에 처리하므로 별도의 HMAC이 필요 없다.

마스터 패스워드에서 키 파생

사용자가 입력한 패스워드를 그대로 암호화 키로 쓸 수 없다. 길이가 32바이트가 아닐 수 있고, 엔트로피도 낮다. PBKDF2로 키를 파생한다.

use pbkdf2::pbkdf2_hmac;
use sha2::Sha256;

fn derive_key(password: &str, salt: &[u8]) -> [u8; 32] {
    let mut key = [0u8; 32];
    pbkdf2_hmac::<Sha256>(
        password.as_bytes(),
        salt,
        600_000, // OWASP 2023 권장 반복 횟수
        &mut key,
    );
    key
}

반복 횟수는 OWASP 권장인 600,000회를 사용했다. 로컬 앱이라 서버 부하 걱정이 없으니 넉넉하게 잡았다.

암호화와 복호화

Rust의 aes-gcm 크레이트를 사용한다. nonce는 반드시 암호화마다 고유해야 한다.

use aes_gcm::{Aes256Gcm, KeyInit, Nonce};
use aes_gcm::aead::Aead;
use rand::RngCore;

fn encrypt(plaintext: &[u8], key: &[u8; 32]) -> Vec<u8> {
    let cipher = Aes256Gcm::new_from_slice(key).unwrap();

    let mut nonce_bytes = [0u8; 12];
    rand::thread_rng().fill_bytes(&mut nonce_bytes);
    let nonce = Nonce::from_slice(&nonce_bytes);

    let ciphertext = cipher.encrypt(nonce, plaintext).unwrap();

    // nonce + ciphertext를 합쳐서 저장
    [nonce_bytes.to_vec(), ciphertext].concat()
}

fn decrypt(data: &[u8], key: &[u8; 32]) -> Vec<u8> {
    let cipher = Aes256Gcm::new_from_slice(key).unwrap();
    let (nonce_bytes, ciphertext) = data.split_at(12);
    let nonce = Nonce::from_slice(nonce_bytes);
    cipher.decrypt(nonce, ciphertext).unwrap()
}

핵심은 nonce를 암호문 앞에 붙여서 저장하는 것이다. 복호화할 때 앞 12바이트를 잘라내면 nonce를 복원할 수 있다.

Salt 관리

Salt는 사용자별로 한 번 생성하고 평문으로 저장한다. Salt는 비밀이 아니다 — 같은 패스워드에서 서로 다른 키를 파생하는 역할만 한다.

fn generate_salt() -> [u8; 16] {
    let mut salt = [0u8; 16];
    rand::thread_rng().fill_bytes(&mut salt);
    salt
}

KeyBox에서는 SQLite에 salt를 별도 테이블로 저장한다. 앱을 처음 실행할 때 한 번 생성되고 이후 변경되지 않는다.

실수하기 쉬운 부분

  • nonce 재사용: 같은 nonce + 같은 키 조합은 보안을 완전히 무너뜨린다. 반드시 매 암호화마다 랜덤 생성
  • 키를 메모리에 남기기: Rust에서는 zeroize 크레이트로 사용 후 메모리에서 키를 지운다
  • 에러 메시지에 정보 노출: 복호화 실패 시 “잘못된 패스워드”와 “데이터 손상”을 구분하지 않는다. 공격자에게 힌트를 주지 않기 위해서다

정리

로컬 앱에서의 암호화는 서버 환경보다 단순하다. 키 교환이나 인증서 관리가 필요 없고, 마스터 패스워드 → PBKDF2 → AES-256-GCM이면 충분하다. Rust의 타입 시스템이 바이트 배열 길이 실수를 컴파일 타임에 잡아주는 점도 큰 이점이다.