홈으로 돌아가기

USB용 Python crypto: SecureBytes와 스트리밍 암호화

이 기사는 SecureBytes 클래스를 사용한 Python의 USB용 crypto 엔진을 설명합니다. 보안 키 저장, 대용량 파일의 스트리밍 암호화, 내결함성 메커니즘을 지원합니다. AES-GCM, ChaCha20 지원. 메모리 취약점과 전원 실패 문제를 제거하는 데 중점.

플래시 드라이브용 crypto 엔진 생성: 메모리 누출 없는 Python
Advertisement 728x90

파이썬으로 구현하는 안전한 USB 암호화: 메모리 관리와 스트림 처리

USB를 통한 파일 전송 시 가장 중요한 것은 간편함입니다. 드라이브를 꽂고 비밀번호를 입력하면, 암호화된 데이터에 즉시 접근할 수 있어야 합니다. 버라크립트는 컨테이너 파일을 사용하지만, 이는 다양한 운영체제 간 직접 파일 처리를 복잡하게 만듭니다. 주요 과제는 바로 암호화 중에 갑작스러운 전원 차단으로 인한 데이터 손상 위험을 어떻게 막느냐입니다.

파이썬 기반의 암호 엔진은 AES-GCM과 ChaCha20을 활용합니다. 핵심은 키를 안전하게 메모리에 저장하고, 메모리 고갈 없이 기가바이트 단위 파일을 스트리밍 방식으로 암호화하며, 강력한 데이터 무결성 검증 기능을 갖추는 것입니다.

SecureBytes 클래스는 파이썬과 자바에서 발생하는 치명적인 취약점을 제거합니다: 가비지 컬렉션(GC)이 민감한 데이터(비밀번호, 키 등)를 새 메모리 영역으로 복사하면서 잔여 데이터를 메모리에 남기는 문제입니다. 작동 방식은 다음과 같습니다:

Google AdInline article slot
class SecureBytes:
    def __init__(self, data: Union[bytes, bytearray, int]):
        if isinstance(data, int):
            self._buffer = bytearray(data)
        else:
            self._buffer = bytearray(data)
        self._finalized = False
        # 약한 최종자 등록
        self._weak_ref = weakref.ref(self, self._cleanup_callback)

    def wipe(self, passes: int = 3):
        if self._finalized or len(self._buffer) == 0:
            return
        # 첫 번째 패스: 랜덤 바이트로 덮기
        self._buffer[:] = secrets.token_bytes(len(self._buffer))
        # 두 번째 패스: 0으로 채우기
        self._buffer[:] = b'\x00' * len(self._buffer)
        self._finalized = True
        gc.collect()

    def __del__(self):
        if not self._finalized:
            self.wipe()

주요 특징:

  • 불변의 bytes가 아닌 bytearray를 사용해 메모리 내부에서 직접 덮어쓰기 가능.
  • 다중 패스 마킹: 랜덤 데이터 → 0으로 채우기 (NIST SP 800-88 기준).
  • 컨텍스트 매니저(with secure_key(...))와 함께 사용하도록 설계되어 예외 발생 시에도 정리 보장.

이로써 메모리 내 키 노출을 최소화하고, GC를 통한 유출을 방지합니다.

스트리밍 암호화: MemorySensitiveReader

4GB 메모리만 있는 시스템에서 10GB 파일을 암호화하려면 스트리밍 방식이 필수적입니다. MemorySensitiveReader 클래스는 파일 크기와 사용 가능한 메모리에 따라 자동으로 모드를 전환합니다:

Google AdInline article slot
class MemorySensitiveReader:
    def __init__(self, file_path: str, memory_threshold: int = 100 * 1024 * 1024):
        self.file_size = os.path.getsize(file_path)
        # 스트리밍 모드 전환 기준치
        self.use_streaming = self.file_size > memory_threshold 

    def iter_chunks(self, chunk_size: int = 8192):
        # 청크 단위로 읽고 암호화
        ...

Nonce 문제 해결: AES-GCM 또는 ChaCha20에서 동일한 키와 같은 Nonce를 반복 사용하면 치명적인 결함이 발생합니다. 이를 방지하기 위해 각 블록마다 기본 Nonce와 블록 인덱스를 기반으로 고유한 Nonce를 생성합니다:

def _derive_block_nonce_12bit(base_nonce: bytes, block_index: int) -> bytes:
    # 앞 8바이트 = 접두사, 뒤 4바이트 = 블록 카운터
    prefix = base_nonce[:8]
    block_counter = block_index.to_bytes(4, byteorder='big')
    return prefix + block_counter

블록은 8KB 단위로 읽히며, 기초 값과 인덱스로부터 Nonce가 생성되므로 파일 크기에 상관없이 암호학적으로 강력합니다.

작업의 장애 내성 확보

실패 시 원본 파일을 삭제하는 일반적인 암호화 방식은 데이터 손실을 초래합니다. 본 시스템은 다음과 같은 보안 메커니즘을 포함합니다:

Google AdInline article slot
  • 잠금 파일 .encryption_lock.json: 상태를 in_progress로 기록하고 처리된 파일 목록을 유지.
  • 임시 파일 .tmp: 암호화 대상 파일의 사본에 암호화 후, 성공적으로 검증되면 원본 삭제.
  • 무결성 검사: 블록을 복호화하고 HMAC 및 SHA-256 해시 비교.
  • 롤백 기능: 중단 시 자동으로 암호화된 파일에서 원본 복구.

이로 인해 전원 장애 이후 데이터는 완전히 암호화되었거나 전혀 손상되지 않은 상태로 유지되며, 부분적 상태가 남지 않습니다.

지원되는 알고리즘

  • AES-256-GCM: AES-NI 하드웨어 가속을 활용해 빠른 성능.
  • ChaCha20-Poly1305: AES-NI가 없는 ARM 기기에서 최적의 성능.
  • XChaCha20-Poly1305: 대규모 암호화에 적합한 24바이트 Nonce.

ThreadPoolExecutor을 활용한 병렬 처리로 소규모 파일 다수 처리 시 I/O 성능 향상, GIL 제약 극복.

핵심 가치:

  • SecureBytes는 다중 패스 마킹을 통해 GC로 인한 키 유출 방지.
  • 100MB 이상 파일에 대해 고유 Nonce를 사용한 스트리밍 암호화 — 충돌 위험 0.
  • 잠금 파일과 자동 롤백 기능을 통한 완전한 장애 내성.
  • AES-GCM, ChaCha20, XChaCha20 모두 보안 훼손 없이 지원.
  • .usb_crypt_meta.json에 메타데이터 저장, 비밀번호는 12자 이상이며 유효성 검사 적용.

제한 사항 및 위협 모델

이 도구는 분실 또는 도난된 드라이브를 보호하지만, 파일 이름이나 디렉토리 구조를 숨기지 않습니다. 호스트 머신의 키로거에는 대응하지 못합니다. 메모리 안전성과 회복성에 중점을 둔 중급 이상 개발자에게 적합합니다.

— Editorial Team

Advertisement 728x90

다음 읽기