Metadata-Version: 2.4
Name: strcrypt
Version: 0.3.1
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: POSIX :: Linux
Classifier: Operating System :: Microsoft :: Windows
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Programming Language :: Rust
Classifier: Topic :: Security :: Cryptography
Classifier: Topic :: Software Development :: Libraries
License-File: LICENSE
License-File: LICENSE-THIRD-PARTY
Summary: Multi-algorithm encryption/decryption (AEGIS/AES/ARIA/ASCON/SEED/LEA) with Python bindings
Keywords: encryption,aes-gcm,aegis,aria,ascon,cryptography,security,rust
Author-email: shpark <shpark@cubiware.co.kr>
License: MIT
Requires-Python: >=3.9
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM

# STRCrypt Python 바인딩 (PyO3)

STRCrypt (Secure Trusted Rust Cryptography) Python 확장 모듈. 다중 알고리즘(AEGIS/AES/ARIA/ASCON/SEED/LEA) 지원.

## 개요

STRCrypt Python 바인딩은 PyO3를 사용하여 Rust로 구현된 고성능 네이티브 확장입니다.

### 주요 특징

- **고성능**: AEGIS-128L 1.2 GB/s, AES-256-GCM 631 MB/s (10KB, AES-NI 환경)
- **다중 알고리즘**: AEGIS-128L/256, AES-256-GCM, AES-256-GCM-SIV, ARIA-256-GCM, ASCON-128, SEED-128-GCM, LEA-128-GCM — 기본 알고리즘은 동작 모드별 (`STRCRYPT_MODE` 미설정 시 korean-standard: ARIA-256-GCM)
- **하드웨어 가속**: AES-NI 자동 활용
- **제로카피 최적화**: 바이너리 데이터 직접 처리
- **배치 처리**: Cipher 1회 생성 후 재사용으로 호출당 오버헤드 감소
- **데이터 키(DEK) 컨텍스트**: 키 보유 주체별 감싼 DEK, 레코드별 AAD 배치(`EncryptionContext`)
- **타입 스텁**: wheel 에 `strcrypt/__init__.pyi`·`py.typed` 동봉
- **다국어 지원**: UTF-8 완벽 지원

### 성능 비교

| 구현 방식 | 처리량 (10KB) | 비고 |
|----------|--------------|------|
| PyO3 | 1.2 GB/s (AEGIS-128L) / 631 MB/s (AES-256-GCM) | 네이티브 확장 |

## 설치

### 필수 요구사항

- Python 3.9 이상
- Rust 1.99 이상 (빌드 시)
- pip, maturin (빌드 시)

### 방법 1: Wheel 파일로 설치 (권장)

빌드된 wheel 파일이 있는 경우:

```bash
pip install target/wheels/strcrypt-*.whl
```

### 방법 2: 소스에서 빌드 및 설치

```bash
cd bindings/python

# 개발 환경 (editable install)
./build.sh

# 또는 Release 모드 (wheel 생성)
./build.sh release
pip install target/wheels/strcrypt-*.whl
```

### 설치 확인

```bash
# 모듈 import 확인
python3 -c "import strcrypt; print(strcrypt.__version__)"

# 환경 변수 설정 (사용 전 필수)
export STRCRYPT_KEY="DEV_ONLY_KEY_NOT_FOR_PRODUCTION!"

# 동작 확인
python3 -c "import strcrypt; print(strcrypt.encrypt('test'))"
```

## 빌드 가이드

### 로컬 개발용 빌드

가장 빠르고 간단한 방법으로, 현재 시스템의 Python 버전용으로 빌드합니다.

```bash
cd bindings/python

# 1. 가상환경 생성 (권장)
python3 -m venv venv
source venv/bin/activate  # Linux/macOS
# venv\Scripts\activate  # Windows

# 2. 빌드 (editable install)
./build.sh

# 3. 테스트
./test.sh
```

**빌드 모드:**
- `./build.sh` - 개발용 (venv 있으면 editable, 없으면 wheel 빌드 후 설치)
- `./build.sh release` - Release 모드 (wheel 파일만 생성)
- `./build.sh -p 3.12` - 호스트의 다른 Python(`python3.12`)으로 빌드. 시험은 `PYTHON=python3.12 ./test.sh`

### 배포용 wheel

배포 wheel 은 `scripts/build_all.sh` 가 만든다. digest 를 고정한 manylinux_2_28 컨테이너와 해시를 고정한
maturin 으로 cp39~cp314(6종) `manylinux_2_28_x86_64` wheel 을, cargo-xwin 컨테이너로 cp39~cp314 `win_amd64` wheel 을 만든다. 각 wheel 은 태그의 CPython 버전(3.9~3.14 중 하나)에만 설치된다.

- glibc 2.28 이상(RHEL·Rocky Linux 8+, Ubuntu 18.04+, Debian 10+) Linux x86_64, Windows 10·11 x64
- 결과물: `target/manylinux_2_28/wheels/`, `target/windows_x64/wheels/`(저장소 루트 기준)

## 사용 방법

### 중요: 환경 변수 설정 (필수)

STRCrypt를 사용하기 전에 **반드시** `STRCRYPT_KEY` 환경 변수를 설정해야 합니다.

```bash
# 32바이트 암호화 키 설정 (필수)
export STRCRYPT_KEY="DEV_ONLY_KEY_NOT_FOR_PRODUCTION!"

# 키 생성 도구 사용
./scripts/keygen.sh
```

환경 변수가 설정되지 않으면 다음과 같은 에러가 발생합니다:
```
ValueError: 암호화 실패: 환경 변수 오류: STRCRYPT_KEY_FILE(권한 0600 키 파일 경로) 또는 STRCRYPT_KEY 환경 변수가 필요합니다. 키 파일 생성(이미 있는 파일은 덮어쓰지 않음): ...
```

### 기본 사용

```python
import strcrypt

# 암호화
plaintext = "안녕하세요, STRCrypt!"
encrypted = strcrypt.encrypt(plaintext)
print(f"암호화: {encrypted}")

# 복호화
decrypted = strcrypt.decrypt(encrypted)
print(f"복호화: {decrypted}")
```

### 바이너리 모드 (제로카피)

Base64 인코딩 없이 바이너리 데이터를 직접 처리:

```python
import strcrypt

# 바이너리 암호화
encrypted_bytes = strcrypt.encrypt_bytes("Hello, World!")
print(f"암호화 크기: {len(encrypted_bytes)} bytes")

# 바이너리 복호화
decrypted = strcrypt.decrypt_bytes(encrypted_bytes)
print(f"복호화: {decrypted}")
```

### 배치 처리

여러 문자열을 한번에 처리 (Cipher 1회 생성 후 재사용):

```python
import strcrypt

# 배치 암호화
messages = ["메시지 1", "메시지 2", "메시지 3"]
encrypted_list = strcrypt.encrypt_batch(messages)

# 배치 복호화
decrypted_list = strcrypt.decrypt_batch(encrypted_list)
print(decrypted_list)
```

### 대량 바이트 배치

바이트 레코드 배열을 호출 1회로 암복호화한다. 레코드마다 AAD 를 주고, 청크 인자로 중간 메모리를 제한한다. 아래 예시는 `doctest` 로 실행 검증한다.

```python
>>> import strcrypt
>>> rows = [b"row-1", b"row-2"]
>>> aads = [b"id=1", b"id=2"]
>>> encrypted = strcrypt.encrypt_batch_bytes_with_aad(rows, "aria256gcm", aads=aads)
>>> [len(c) - len(r) for c, r in zip(encrypted, rows)]
[29, 29]
>>> strcrypt.decrypt_batch_bytes_with_aad(encrypted, aads=aads)
[b'row-1', b'row-2']

```

청크 인자는 키워드 전용이다. 건수 상한과 바이트 상한을 함께 주면 가변 크기 레코드에도 중간 메모리가 한정된다(값은 예시).

```python
>>> chunked = strcrypt.encrypt_batch_bytes_with_aad(
...     rows, "aria256gcm", aads=aads, chunk_size=1000, max_chunk_bytes=16 * 1024 * 1024)
>>> strcrypt.decrypt_batch_bytes_with_aad(chunked, aads=aads) == rows
True

```

`algorithm` 은 필수다. 이름이 비슷한 단건 `encrypt_bytes_with_aad(data, aad)` 는 알고리즘 인자 없이 현재 동작 모드의 기본 알고리즘을 쓰므로, 한 건씩 같은 알고리즘으로 처리하려면 `encrypt_bytes_with_algorithm_and_aad(data, algorithm, aad)` 를 쓴다.

**예외 클래스가 단건 함수와 다르다.** 단건 함수는 키 로드 실패나 동작 모드가 허용하지 않는 알고리즘까지 모두 `ValueError` 로 올리지만, 두 배치 함수는 그런 설정 오류와 프로세스 오류(모듈 오류 상태, 알고리즘 사용 한도 도달 등)를 `RuntimeError` 로 구분한다. 설정 오류를 손상 레코드로 기록하지 않기 위한 구분이다.

레코드 데이터 오류에만 `record_index` 속성이 붙는다. `ValueError` 는 호출 인자 값 오류도 쓰므로, 손상 레코드를 건너뛰는 코드는 먼저 알고리즘 이름·AAD 배열 길이·청크 인자를 호출자 코드로 검사한 뒤 그다음에 난 `ValueError` 중 `record_index` 가 정수인 것만 레코드 오류로 다룬다.

```python
>>> try:
...     strcrypt.encrypt_batch_bytes_with_aad(rows, "aria256gcm", aads=[b"id=1"])
... except ValueError as e:
...     hasattr(e, "record_index")
False

```

Rust 내부 panic 은 `pyo3_runtime.PanicException` 으로 올라오고 이 클래스는 `BaseException` 하위라 `except Exception` 에 잡히지 않는다.

각 청크의 Rust 처리 동안에만 전역 인터프리터 잠금을 놓는다. 두 번째 청크부터 시작 직전에 신호를 확인해 `KeyboardInterrupt` 등을 그대로 올리므로, 청크 인자가 없으면 호출이 끝날 때까지 신호가 처리되지 않는다.

결과·AAD·청크·병렬·보안 규칙의 정본은 루트 [README.md](../../README.md) 의 대량 바이트 배치 절이다.

### 데이터 키(DEK) 컨텍스트

키 보유 주체(고객사·사용자·시스템)마다 데이터 키(DEK)를 따로 두고, 그 키로 레코드를 암복호화한다. DEK 는 마스터 키(`STRCRYPT_KEY_FILE`·`STRCRYPT_KEY`)로 감싼 값(감싼 DEK)으로만 밖에 나오고, 평문 DEK 는 Python 에 나오지 않는다. 감싼 DEK 는 비밀이 아니므로 데이터 옆(DB 등)에 저장한다. 텍스트 컬럼에는 Base64 로 저장한다(늘 `U0RFS` 로 시작한다). 아래 예시는 `doctest` 로 실행 검증한다.

```python
>>> import strcrypt
>>> def field(value):
...     return len(value).to_bytes(4, "big") + value
>>> binding = b"example-app/tenant-dek" + field(b"t7")
>>> wrapped = strcrypt.generate_wrapped_dek(binding, "aria256gcm")  # 주체마다 한 번 만들어 저장한다
>>> info = strcrypt.wrapped_dek_info(wrapped)
>>> info["algorithm"], info["kek_id"] == strcrypt.master_kek_id()
('aria256gcm', True)
>>> rows = [b"row-0", b"row-1"]
>>> aads = [b"id=0", b"id=1"]
>>> with strcrypt.EncryptionContext.from_wrapped_dek(wrapped, binding) as ctx:
...     sealed = ctx.encrypt_batch(rows, aads)
...     ctx.decrypt_batch(sealed, aads)
[b'row-0', b'row-1']

```

**binding(문맥)**: 감싼 DEK 를 만들 때 주는 값이고, 감싼 값에 저장하지 않는다. 풀 때(`from_wrapped_dek`·`rewrap_dek`) 같은 값을 다시 준다. 길이는 1바이트 이상 65,536바이트 이하다. 빈 값은 주체 간 격리를 없애므로 거부한다. 인코딩 규칙은 호출자가 지킨다(라이브러리는 검사하지 않는다).

- 서로 다른 (용도, 주체, ...) 조합이 같은 바이트열이 되지 않도록 일대일 인코딩을 쓴다. 권장 형식은 위 예시처럼 고정 ASCII 용도 라벨 뒤에 각 필드를 4바이트 빅엔디언 길이와 바이트로 잇는 것이다.
- 구분자 문자열을 써야 하면 모든 필드에서 구분자 문자를 거부하거나 이스케이프한다. JSON 을 쓰려면 직렬화 규칙(RFC 8785 JCS 등)을 고정한다.
- 주체 식별자는 바뀌지 않는 내부 ID 를 쓴다. 여러 앱이 마스터 키를 공유할 수 있으므로 앱·용도 라벨을 반드시 넣는다.
- 레코드별 AAD 에도 같은 규칙을 적용한다.

**알고리즘과 동작 모드**: `generate_wrapped_dek` 의 알고리즘은 필수다. DEK 길이는 그 알고리즘의 키 길이이고, DEK 하나는 알고리즘 하나에만 쓴다. 감싸기 알고리즘은 생성할 때의 동작 모드가 정한다(korean-standard 는 ARIA-256-GCM, non-approved 는 AES-256-GCM). korean-standard 에서 만든 값은 non-approved 에서도 풀리지만, non-approved 에서 만든 값은 korean-standard 에서 풀리지 않는다(`RuntimeError`). 감싼 DEK 는 그것을 풀 동작 모드에서 만든다.

**컨텍스트 배치와 전역 배치 함수의 차이**:

- 컨텍스트의 `encrypt_batch`·`decrypt_batch` 는 알고리즘 인자가 없고(컨텍스트의 알고리즘), `aads` 가 필수이며 입력과 길이가 같아야 한다. 원소는 `b""` 여도 된다.
- 전역 `encrypt_batch_bytes_with_aad` 는 건수·스레드 조건이 맞으면 자동으로 병렬 처리한다. 컨텍스트의 `encrypt_batch`·`decrypt_batch` 는 늘 호출 스레드에서 입력 순서대로 처리하고, 병렬은 `encrypt_batch_parallel`·`decrypt_batch_parallel` 이 한다. 병렬 메서드는 입력이 50건 이상이고 실행 풀의 스레드가 2개 이상일 때만 병렬이다. 스레드 수를 줄이려면 순차 메서드를 쓰거나 첫 병렬 호출 전에 `RAYON_NUM_THREADS` 를 설정한다.
- 결과 원소는 단건 암호문과 같은 헤더형이다. 같은 DEK 로 만든 컨텍스트라면 단건·순차·병렬 결과를 서로 복호화할 수 있다. 복호화는 헤더형만 받고, 헤더 없는 레거시 암호문은 받지 않는다.
- 레코드 연산은 호출 전체 동안 전역 인터프리터 잠금을 한 번 놓고, 그동안 신호를 확인하지 않는다. 컨텍스트 배치에는 청크 인자가 없으므로, 메모리 상한·신호(`KeyboardInterrupt`) 응답·다른 호출자의 대기를 줄이려면 호출자가 입력을 잘라 여러 번 부른다. 컨텍스트가 키 스케줄을 들고 있으므로 조각 호출마다 키를 다시 준비하지 않는다.

```python
>>> ctx = strcrypt.EncryptionContext.from_wrapped_dek(wrapped, binding)
>>> many = [b"r%d" % i for i in range(1000)]
>>> ids = [b"id=%d" % i for i in range(1000)]
>>> sealed = []
>>> for start in range(0, len(many), 250):  # 조각 사이에 잠금을 다시 잡고 신호를 처리한다
...     sealed += ctx.encrypt_batch_parallel(many[start:start + 250], ids[start:start + 250])
>>> ctx.decrypt_batch(sealed, ids) == many
True
>>> ctx.close()

```

**원시 키**: 외부 키 관리 서비스(KMS)가 평문 DEK 를 주는 경우 `EncryptionContext.from_raw_key(key, algorithm)` 로 컨텍스트를 만든다. 마스터 키를 읽지 않는다. 키 길이는 알고리즘 키 길이와 정확히 같아야 한다. 다음을 지킨다.

- 라이브러리는 호스트 언어 객체의 키 사본을 소거할 수 없다. 키는 `bytearray` 로 넘기고, 반환 직후 호출자가 0 으로 덮어쓴다. `bytes`·`str` 객체나 그것을 만들며 생긴 사본은 그래도 남는다.
- 신뢰하는 KMS 가 준 키만 넣는다. GCM 계열 암호문은 key-committing 이 아니라, 공격자가 알거나 고른 키가 들어오면 한 암호문이 여러 키로 복호화되게 만들 수 있다.
- 비밀번호에서 파생한 키는 넣지 않는다.
- 같은 키를 마스터 키(`STRCRYPT_KEY`·`STRCRYPT_KEY_FILE`), `wrap_key`·`unwrap_key` 의 KEK, 다른 기존 경로에 함께 쓰지 않는다. 사용량 한도가 따로 세어진다. 같은 키를 다른 알고리즘으로 넣으면 `ValueError` 다.
- 객체마다 다른 DEK 를 넣는 방식이면, 서로 다른 키마다 약 100바이트가 프로세스 수명 동안 남는다.

```python
>>> key = bytearray(b"\x42" * 16)  # 실제로는 KMS 가 준 16바이트 데이터 키
>>> raw_ctx = strcrypt.EncryptionContext.from_raw_key(key, "seed128gcm")
>>> key[:] = bytes(len(key))  # 호출자 사본을 0 으로 덮어쓴다
>>> raw_ctx.decrypt(raw_ctx.encrypt(b"record", b"tenant=7"), b"tenant=7")
b'record'

```

**수명과 스레드**: 여러 스레드가 한 컨텍스트를 동시에 써도 된다. `close()` 는 이후 호출을 막는다. 진행 중인 호출은 끝까지 처리되고, 컨텍스트 안의 키는 마지막 진행 중 호출이 끝날 때 소거된다. `close()` 는 여러 번 불러도 되고 예외를 내지 않는다. `with` 문을 나가면 `close()` 를 부르고, 닫힌 컨텍스트로 `with` 에 들어가면 `ValueError` 다. 닫힌 뒤에도 `algorithm` 은 원래 이름을, `closed` 는 `True` 를 돌려준다. 복제·pickle·키 조회는 지원하지 않는다.

**사용량 한계**: ARIA-256-GCM·SEED-128-GCM·LEA-128-GCM·AES-256-GCM 컨텍스트는 DEK 마다 암호화 호출을 세고, 같은 DEK 로 컨텍스트를 다시 만들어도 카운터는 이어진다. 한 프로세스에서 한 DEK 의 암호화가 2^32-1 회에 닿으면 그 DEK 의 암호화는 `RuntimeError` 이고 복호화는 계속된다. 오류 문구는 재시작을 안내하지만, DEK 컨텍스트는 새 DEK 로 교체한다. 카운터는 프로세스 메모리에만 있어 재시작·여러 프로세스·fork 자식에 걸친 누적은 합쳐지지 않는다. DEK 별 암호화 건수를 모든 프로세스에 걸쳐 누계하거나, 건수·처리량·기간 기준으로 DEK 를 교체한다(기준은 [docs/KEY_MANAGEMENT.md](../../docs/KEY_MANAGEMENT.md)).

**마스터 키 교체**: 옛 마스터 키로 도는 작업 프로세스(앱과 같은 동작 모드)에서 모든 감싼 DEK 를 `rewrap_dek(wrapped, binding, new_master_key_file)` 로 다시 감싸 옛 값과 나란히 저장한다. 레코드 암호문은 DEK 가 그대로라 다시 암호화하지 않는다. 이미 새 키로 감싼 값은 검증만 하고 그대로 돌려주므로 중단되면 다시 돌린다. 새 키로 띄운 일회성 프로세스의 `master_kek_id()` 와 새 값의 `wrapped_dek_info(...)["kek_id"]` 가 같은지 확인한 뒤 재기동한다. KEK 식별자는 소문자 hex 32자이고, 저장해 다른 언어와 비교할 때도 이 형태 그대로 비교한다. 새 키 파일 생성 명령과 전체 절차는 [docs/KEY_MANAGEMENT.md](../../docs/KEY_MANAGEMENT.md) 를 따른다.

```python
# 옛 마스터 키로 도는 작업 프로세스
new_value = strcrypt.rewrap_dek(old_value, binding, "/run/secrets/strcrypt-new.key")
assert strcrypt.wrapped_dek_info(new_value)["kek_id"] == new_kek_id  # 새 키 프로세스의 master_kek_id()
```

### 하드웨어 정보 확인

```python
import strcrypt

info = strcrypt.hardware_info()
print(f"AES-NI 지원: {info['aes_ni']}")
print(f"PCLMULQDQ 지원: {info['pclmulqdq']}")
print(f"플랫폼: {info['platform']}")
```

### 에러 처리

```python
import strcrypt
import sys

try:
    encrypted = strcrypt.encrypt("안녕하세요")
    decrypted = strcrypt.decrypt(encrypted)
    print(f"성공: {decrypted}")
except ValueError as e:
    error_msg = str(e)

    # 환경 변수 오류 확인
    if "STRCRYPT_KEY" in error_msg:
        print("오류: 환경 변수가 설정되지 않았습니다.")
        print("해결: export STRCRYPT_KEY='your-32-byte-key'")
        sys.exit(1)

    # 기타 오류
    print(f"암호화 오류: {e}")
    sys.exit(1)
```

함수 계열마다 예외 분류 규칙이 다르다. 인자 타입 오류는 모든 함수에서 `TypeError` 다.

| 함수 | 인자 값·입력 데이터 오류 | 설정 오류(키 로드 실패, 동작 모드가 허용하지 않는 알고리즘)·프로세스 오류(모듈 오류 상태, 사용량 한도 도달 등) |
|------|-------------------------|-------------------------------------------------|
| 단건 함수(`encrypt`·`decrypt_bytes_with_aad`·`encrypt_file`·`derive_key`·`wrap_key` 등, 아래 두 줄에 없는 함수) | `ValueError` | `ValueError` |
| 대량 바이트 배치(`encrypt_batch_bytes_with_aad`·`decrypt_batch_bytes_with_aad`), `EncryptionContext`, `generate_wrapped_dek`·`rewrap_dek`·`wrapped_dek_info`·`master_kek_id`, `search_index`(도메인 길이) | `ValueError` | `RuntimeError` |

- 둘째 줄 함수의 인자 타입 오류 메시지는 인자 이름과 콜론(`"binding: "` 등)으로 시작한다.
- 배치(대량 바이트 배치 함수와 `EncryptionContext` 의 배치 메서드 4개)의 레코드 데이터 오류에만 `record_index` 속성이 있다. 닫힌 `EncryptionContext` 사용은 `ValueError` 다.
- `rewrap_dek` 의 새 마스터 키 파일 오류(권한·읽기·내용·형식·길이·강도)는 `RuntimeError` 이고, 메시지에 경로를 넣지 않는다.
- Rust 내부 panic 은 `pyo3_runtime.PanicException`(`BaseException` 하위)으로 올라와 `except Exception` 에 잡히지 않는다.

## API 레퍼런스

### 함수

#### `encrypt(text: str) -> str`
문자열을 현재 동작 모드의 레거시 기본 알고리즘(korean-standard: ARIA-256-GCM, non-approved: AES-256-GCM)으로 암호화하고 Base64로 인코딩합니다.

**Parameters:**
- `text` (str): 암호화할 문자열

**Returns:**
- str: Base64로 인코딩된 암호화 데이터

**Raises:**
- ValueError: 암호화 실패 시

#### `decrypt(encrypted_base64: str) -> str`
Base64로 인코딩된 암호화 데이터를 복호화합니다.

**Parameters:**
- `encrypted_base64` (str): Base64로 인코딩된 암호화 데이터

**Returns:**
- str: 복호화된 원본 문자열

**Raises:**
- ValueError: 복호화 실패 시 (잘못된 데이터, 인증 실패 등)

#### `encrypt_bytes(text: str) -> bytes`
문자열을 암호화하고 바이너리 데이터로 반환합니다 (Base64 인코딩 없음).

**Parameters:**
- `text` (str): 암호화할 문자열

**Returns:**
- bytes: 암호화된 바이너리 데이터

**Raises:**
- ValueError: 암호화 실패 시

#### `decrypt_bytes(encrypted_data: bytes) -> str`
바이너리 암호화 데이터를 복호화합니다.

**Parameters:**
- `encrypted_data` (bytes): 암호화된 바이너리 데이터

**Returns:**
- str: 복호화된 원본 문자열

**Raises:**
- ValueError: 복호화 실패 시

#### `encrypt_batch(texts: list[str]) -> list[str]`
여러 문자열을 배치로 암호화합니다 (Cipher 1회 생성 후 재사용).

**Parameters:**
- `texts` (list[str]): 암호화할 문자열 리스트

**Returns:**
- list[str]: Base64로 인코딩된 암호화 데이터 리스트

**Raises:**
- ValueError: 암호화 실패 시

#### `decrypt_batch(encrypted_list: list[str]) -> list[str]`
여러 암호화 데이터를 배치로 복호화합니다 (Cipher 1회 생성 후 재사용).
원소마다 단건 `decrypt()`와 같이 알고리즘 헤더를 자동 감지하고 헤더가 없으면 레거시 형식으로 폴백하므로, 헤더형과 레거시가 섞인 리스트도 한 호출로 복호화합니다(`decrypt_batch_parallel()`도 동일).

**Parameters:**
- `encrypted_list` (list[str]): Base64 암호화 데이터 리스트

**Returns:**
- list[str]: 복호화된 원본 문자열 리스트

**Raises:**
- ValueError: 복호화 실패 시

#### `encrypt_batch_bytes_with_aad(inputs, algorithm, *, aads=None, chunk_size=None, max_chunk_bytes=None) -> list[bytes]`
여러 바이트 레코드를 지정한 알고리즘으로 한 번에 암호화합니다 (레코드별 AAD, 청크 분할).

**Parameters:**
- `inputs` (list[bytes]): 암호화할 레코드. list·tuple 같은 시퀀스만 받고 원소는 `bytes` 만 받습니다
- `algorithm` (str): 알고리즘 이름 (예: `"aria256gcm"`). 필수입니다
- `aads` (list[bytes] | None): 레코드별 추가 인증 데이터. 길이가 `inputs` 와 같아야 합니다
- `chunk_size` (int | None): 청크당 최대 레코드 수 (1 이상)
- `max_chunk_bytes` (int | None): 청크당 레코드 바이트(입력 길이 + AAD 길이) 합의 상한 (1 이상)

**Returns:**
- list[bytes]: 암호문 목록. 입력과 길이·순서가 같고 원소는 단건 암호문과 같은 형식입니다

**Raises:**
- TypeError: 인자 타입 오류
- OverflowError: 청크 인자가 정수 범위를 넘음
- ValueError: 인자 값 오류 또는 레코드 데이터 오류. 레코드 데이터 오류에만 `record_index` 속성이 있습니다
- RuntimeError: 설정 오류(키 로드 실패, 동작 모드가 허용하지 않는 알고리즘)와 프로세스 오류

#### `decrypt_batch_bytes_with_aad(encrypted, *, aads=None, chunk_size=None, max_chunk_bytes=None) -> list[bytes]`
여러 바이트 암호문을 한 번에 복호화합니다. 레코드마다 헤더에서 알고리즘을 감지하므로 알고리즘이 섞인 배열도 한 호출로 처리합니다. 인자·예외 규칙은 `encrypt_batch_bytes_with_aad()` 와 같고 `algorithm` 인자만 없습니다.

#### `class EncryptionContext`
데이터 키(DEK) 하나로 레코드를 암복호화하는 컨텍스트입니다. 직접 생성할 수 없고 두 정적 메서드로 만듭니다. 사용 규칙은 위 "데이터 키(DEK) 컨텍스트" 절을 따릅니다.

| 멤버 | 설명 |
|------|------|
| `from_wrapped_dek(wrapped: bytes, binding: bytes)` | 감싼 DEK 를 현재 마스터 키로 풀어 만든다 |
| `from_raw_key(key: bytes \| bytearray, algorithm: str)` | KMS 가 준 평문 DEK 로 만든다. 키 길이는 알고리즘 키 길이와 정확히 같아야 한다 |
| `algorithm` (속성) | 알고리즘 정규명(예: `"aria256gcm"`). 닫힌 뒤에도 원래 이름 |
| `closed` (속성) | `close()` 를 불렀으면 `True` |
| `encrypt(data: bytes, aad: bytes) -> bytes` / `decrypt(data: bytes, aad: bytes) -> bytes` | 레코드 하나 |
| `encrypt_batch(inputs, aads) -> list[bytes]` / `decrypt_batch(encrypted, aads) -> list[bytes]` | 레코드별 AAD 배치, 늘 순차 |
| `encrypt_batch_parallel(inputs, aads)` / `decrypt_batch_parallel(encrypted, aads)` | 병렬 변형(50건 이상·스레드 2개 이상) |
| `close()`, `with` 문 | 이후 호출 차단. 여러 번 불러도 된다 |

#### `generate_wrapped_dek(binding: bytes, algorithm: str) -> bytes`
새 DEK 를 만들어 현재 마스터 키로 감싼 값(83 또는 99바이트)을 반환합니다. 평문 DEK 는 반환하지 않습니다.

#### `rewrap_dek(wrapped: bytes, binding: bytes, new_master_key_file: str) -> bytes`
감싼 DEK 를 새 마스터 키 파일의 키로 다시 감쌉니다(마스터 키 교체). 새 키 파일은 권한 0600, 줄바꿈 없는 한 줄이어야 하고 키 강도 검사를 통과해야 합니다.

#### `wrapped_dek_info(wrapped: bytes) -> dict[str, str]`
감싼 DEK 를 파싱만 해서 `{"algorithm", "wrap_algorithm", "kek_id"}` 를 반환합니다. `kek_id` 는 소문자 hex 32자입니다. 키가 필요 없고 동작 모드와 무관합니다.

#### `master_kek_id() -> str`
현재 마스터 키의 KEK 식별자(비밀 아님)를 소문자 hex 32자로 반환합니다.

#### `search_index(value: str, domain: str) -> str`
암호화한 열의 같은 값 검색에 쓰는 검색 인덱스를 소문자 hex 64자로 반환합니다. 같은 마스터 키·도메인·값이면 언어와 DB 의 SQL 함수 `strcrypt_search_index` 에 무관하게 같은 값이라, 애플리케이션에서 만든 인덱스로 DB 를 찾거나 DB 에서 만든 인덱스를 애플리케이션이 찾을 수 있다. 도메인은 열을 구분하는 문자열(UTF-8 1~256바이트)이고, 대소문자·공백 같은 표기 차이는 저장과 검색에 같은 규칙으로 맞춘다. 같은 값끼리는 인덱스가 같아 값의 일치와 빈도가 드러나므로 값 종류가 적은 열에는 두지 않는다. 마스터 키를 바꾸면 인덱스를 다시 계산한다. 원리와 운영 규칙은 [RDBMS 통합 README](../../rdbms/README.md) 검색용 열 절에 있다.

```python
row_index = strcrypt.search_index(ssn.replace("-", ""), "customers.ssn")  # 저장할 때와 검색할 때 같은 정규화
```

#### `hardware_info() -> dict`
하드웨어 가속 정보를 반환합니다.

**Returns:**
- dict: 하드웨어 가속 정보
  - `aes_ni` (bool): AES-NI 지원 여부
  - `pclmulqdq` (bool): PCLMULQDQ 지원 여부
  - `platform` (str): 플랫폼 정보

### 추가 노출 함수

위 함수 외에 다음 함수들이 동일하게 노출된다. 시그니처는 [src/lib.rs](src/lib.rs) 참조.

| 분류 | 함수 |
|------|------|
| 알고리즘 지정 | `encrypt_with_algorithm`, `decrypt_with_algorithm`, `encrypt_bytes_with_algorithm`, `decrypt_bytes_with_algorithm`, `encrypt_batch_with_algorithm`, `decrypt_batch_with_algorithm` |
| AAD(추가 인증 데이터) | `encrypt_with_aad`, `decrypt_with_aad`, `encrypt_with_algorithm_and_aad`, `decrypt_with_algorithm_and_aad`, `encrypt_bytes_with_aad`, `decrypt_bytes_with_aad`, `encrypt_bytes_with_algorithm_and_aad` |
| 대량 바이트 배치 | `encrypt_batch_bytes_with_aad`, `decrypt_batch_bytes_with_aad` |
| 병렬 배치 | `encrypt_batch_parallel`, `decrypt_batch_parallel`, `encrypt_batch_parallel_with_algorithm`, `decrypt_batch_parallel_with_algorithm` |
| 파일 암복호화 | `encrypt_file`, `decrypt_file`, `encrypt_file_parallel`, `decrypt_file_parallel` |
| KDF / PBE | `derive_key`, `derive_key_from_password`, `generate_pbe_salt` |
| 키 래핑 | `wrap_key`, `unwrap_key` |
| 유틸 | `init`, `validate_key_strength`, `supported_algorithms`, `version` |
| 데이터 키(DEK) | `EncryptionContext`, `generate_wrapped_dek`, `rewrap_dek`, `wrapped_dek_info`, `master_kek_id` |
| 검색 인덱스 | `search_index` |

파일 암복호화 함수(`encrypt_file`·`decrypt_file`·`encrypt_file_parallel`·`decrypt_file_parallel`)는 결과를 출력 경로와 같은 디렉터리의 임시 파일에 쓴 뒤 rename으로 교체한다. 출력 경로에 이미 있는 심볼릭 링크·FIFO·장치 노드 항목도 새 일반 파일로 바꾸고 링크가 가리키는 파일에는 쓰지 않으며, 결과 파일 권한은 프로세스 umask를 따른다. CLI `encrypt-file`·`decrypt-file`은 일반 파일이 아닌 기존 출력 경로를 거부하고 결과를 권한 0600으로 만든다.

### 모듈 속성

- `__version__`: 버전 정보
- `__author__`: 작성자 정보

타입 스텁 [strcrypt.pyi](strcrypt.pyi) 가 wheel 에 `strcrypt/__init__.pyi` 와 `py.typed` 로 들어가 타입 검사기의 정본이 됩니다. 스텁과 런타임의 일치는 `mypy.stubtest` 로 검사합니다(`./test.sh`).

## 예제

프로젝트에는 다양한 예제가 포함되어 있습니다:

### 기본 사용법

```bash
cd bindings/python
python3 examples/basic_usage.py
```

다음 내용을 포함합니다:
- 기본 암호화/복호화
- 바이너리 모드
- 배치 처리
- 다국어 지원
- 하드웨어 정보 확인
- 에러 처리
- 데이터 키(DEK) 컨텍스트·감싼 DEK·재감싸기

### 성능 벤치마크

```bash
python3 examples/benchmark.py
```

다양한 데이터 크기(1B ~ 10MB)에 대한 성능 측정

## 테스트

### 전체 테스트 실행

```bash
cd bindings/python
./test.sh
```

기능 테스트는 예제를 korean-standard(`STRCRYPT_MODE` 미설정)와 non-approved(`STRCRYPT_MODE=non-approved`) 두 모드로 실행합니다. 예제는 현재 모드가 허용하지 않는 알고리즘 부분을 건너뜁니다. mypy 가 설치되어 있으면 타입 스텁을 `mypy.stubtest` 로 대조하고, 없으면 "건너뜀" 을 출력합니다. 벤치마크는 non-approved 모드로 실행합니다.

### 개별 테스트

```bash
# 기본 사용법만 테스트
./test.sh basic

# 벤치마크만 실행
./test.sh bench
```

## 환경 변수

### STRCRYPT_KEY (필수)

암호화에 사용할 32바이트 키를 설정합니다:

```bash
export STRCRYPT_KEY="DEV_ONLY_KEY_NOT_FOR_PRODUCTION!"
```

키 생성 도구:

```bash
# 프로젝트 루트에서
./scripts/keygen.sh
./scripts/verify_key.sh
```

상세한 키 관리 방법은 [../../docs/KEY_MANAGEMENT.md](../../docs/KEY_MANAGEMENT.md)를 참조하세요.

## 문제 해결

빌드 또는 설치 중 문제가 발생하면 [TROUBLESHOOTING.md](TROUBLESHOOTING.md)를 참조하세요.

주요 문제:
- wheel 파일을 찾을 수 없는 경우
- manylinux 호환성 오류
- Python 버전 불일치
- 메모리 부족 오류
- import 오류

## 성능 최적화

### 빌드 프로파일

`build.sh` 는 dev·release 모두 `maturin --release`(최적화 Level 3, LTO, strip)로 빌드한다. 두 모드의 차이는
설치 여부뿐이다.

### 성능 팁

1. **배치 처리 사용**: 여러 문자열을 처리할 때는 `encrypt_batch()` / `decrypt_batch()` 사용
2. **바이너리 모드**: Base64 인코딩이 필요 없으면 `encrypt_bytes()` / `decrypt_bytes()` 사용
3. **하드웨어 가속 확인**: `hardware_info()`로 AES-NI 활성화 여부 확인

## 프로젝트 구조

```
bindings/python/
├── src/
│   └── lib.rs              # PyO3 바인딩 구현
├── examples/
│   ├── basic_usage.py      # 기본 사용 예제
│   └── benchmark.py        # 성능 벤치마크
├── build.sh                # 로컬 개발용 빌드
├── test.sh                 # 테스트 스크립트
├── pyproject.toml          # 프로젝트 메타데이터
├── Cargo.toml              # Rust 의존성
├── README.md               # 이 문서
└── TROUBLESHOOTING.md      # 문제 해결 가이드
```

## 라이선스

MIT License - Copyright (c) 2024 shpark

## 관련 문서

- [STRCrypt 메인 README](../../README.md)
- [성능 벤치마크](../../docs/PERFORMANCE.md)
- [키 관리 가이드](../../docs/KEY_MANAGEMENT.md)
- [문제 해결 가이드](TROUBLESHOOTING.md)

## 지원

문제가 해결되지 않으면 큐비웨어 홈페이지 문의 양식(https://www.cubiware.co.kr/company/contact/)으로 보고해 주세요.
보안 취약점은 공개 채널에 재현 코드를 올리지 말고 같은 양식으로 보내 주세요.

환경 정보와 함께 보고:
```bash
echo "OS: $(uname -a)"
echo "Docker: $(docker --version)"
echo "Python: $(python3 --version)"
echo "Rust: $(rustc --version)"
```

