
# 증명 가능한 보안 언어 스테가노그래피 회전 범위 코딩을 통해 LLM 생성 텍스트에 비밀 메시지를 은닉하는 증명 가능한 보안 언어 스테가노그래피. 임베드, 추출, 왕복 검증 스크립트를 포함합니다.
Rotation Range-Coding (RRC) 스테가노그래피 — 대규모 언어 모델이 생성한 자연어 텍스트에 비밀 메시지를 삽입하는 효율적이고 증명 가능하게 안전한 언어적 스테가노그래피 기법.
논문: Efficient Provably Secure Linguistic Steganography via Range Coding (ACL 2026)
| 단계 | 설명 |
|---|---|
| 삽입 (알고리즘 3) | 비밀 메시지를 십진수 값으로 변환하고, LM의 확률 분포와 PRNG가 생성한 오프셋에 따라 축소되는 구간 내에서 반복적으로 회전시킨다. 각 회전 단계마다 하나의 토큰이 생성된다. |
| 추출 (알고리즘 4) | 스테고텍스트에 대해 LM을 다시 실행하여 구간 경계를 복원한 뒤, 역회전하여 원래의 십진수 값으로 되돌리고 이진화한다. |
회전 메커니즘은 기본 범위 코딩 스테가노그래피의 두 가지 문제를 해결한다:
.
├── rrc_core.py # 알고리즘 3과 4, 모든 진입점에서 공유
├── RRC_embed.py # 삽입 CLI
├── RRC_extract.py # 추출 CLI (복구 정확도 보고)
├── test_roundtrip.py # 여러 메시지 길이에 걸친 왕복 테스트
├── fastest_embed.py # 삽입 속도 벤치마크
├── 0.Prompts.tsv # 1000개의 프롬프트 (C4 시퀀스의 처음 ~10 단어)
└── requirements.txt
인코더와 디코더는 루프의 별도 복사본을 보유하는 대신 rrc_core.py를 공유한다. 양측은 비트 단위로 일치해야 하며, 독립적으로 유지 관리되는 두 복사본 간의 차이는 검사 시에는 결코 드러나지 않고 오직 왕복 실패로만 나타난다.
pip install -r requirements.txt
python test_roundtrip.py # GPT-2, 32/128/512 bits
python test_roundtrip.py --model meta-llama/Llama-2-7b-hf
python RRC_embed.py \
--language_model meta-llama/Llama-2-7b-hf \
--bit_length 128 --key 42 --top_k -1
python RRC_extract.py \
--language_model meta-llama/Llama-2-7b-hf \
--bit_length 128 --key 42 \
--input_file 1.RC_decimal_Llama-2-7b-hf_bit128.tsv
인코더는 삽입한 비밀과 이를 운반한 토큰 id를 기록하므로, 추출은 복구율을 보고한다. 두 가지 수신자 모델이 평가된다:
ids — 수신자에게 토큰 시퀀스가 주어진다. 이는 코딩 기법을 분리한다. 100% 미만이면 수치적 또는 논리적 결함이다.text — 수신자에게 디토큰화된 문자열이 주어지고 이를 다시 토큰화한다. 이는 텍스트 채널이 요구하는 방식이다. 재토큰화가 항상 송신자의 분절을 재현하지는 않으므로 이는 하한이다. 두 측정값 사이의 격차는 코딩 기법이 아니라 토큰화 모호성을 나타낸다. Llama-2-7b에서 128비트 기준: ids는 100%, text는 ~91%.중요: 송신자와 수신자는 언어 모델, 키, 비트 길이, top-k 설정을 공유해야 하며, 동일한 하드웨어와 라이브러리 버전에서 실행해야 한다. 수치적 요구 사항을 참조하라.
RRC_embed.py와 RRC_extract.py가 공유:
| 인수 | 기본값 | 설명 |
|---|---|---|
--language_model | meta-llama/Llama-3.1-8B | HuggingFace 모델 id |
--bit_length | 128 | 비밀 메시지 길이 (비트) |
--top_k | -1 | Top-k 절단; -1 = 전체 어휘 |
--key | 42 | 대칭 키 K (PRNG 시드) |
--precision_margin | 40 | 2**bit_length에 필요한 것 이상의 십진수 자릿수 |
--max_ctx_len | 3900 | KV-캐시 윈도우 |
--device | auto | cuda 또는 cpu |
RRC_embed.py는 또한 --prompts, --output, --limit, --seed, --max_tokens, --resume, --no_verify, 그리고 프롬프트 집합을 여러 프로세스에 분할하기 위한 --part / --part_max를 받는다 (GPU당 하나씩 --part 0,1,2와 함께 --part_max 3).
RRC_extract.py는 또한 --input_file, --mode {ids,text,both}, --limit, --verbose를 받는다.
RRC는 폭 2**l의 구간을 폭 ~1로 좁히므로, 복구된 값은 2**l배의 상대 오차에 민감하다: 128비트에서 체인 어디에서든 1e-16의 상대적 섭동은 0.5의 허용 오차에 대해 결과를 ~1e22만큼 이동시킨다. 추출은 점진적으로 저하되지 않는다 — 메시지를 반환하거나 아니면 무관한 비트를 반환한다. Llama-2-7b에서 측정:
| 설정 | 확률의 상대적 섭동 | 추출 |
|---|---|---|
| 동일한 batch-1 디코드 반복 | 0 (비트 단위 동일) | 작동 |
| batch 2 / 8 / 16에서 동일한 행 | ~1e-2 | 실패 |
| 프롬프트 좌측 패딩 | ~1e-2 | 실패 |
| 증분 디코딩 대신 한 번의 teacher-forced 패스 | ~4e-3 | 실패 |
두 가지 결과가 구현을 형성한다. 배칭이 불가능하므로 시퀀스는 한 번에 하나씩 처리된다. 그리고 수신자는 모든 토큰을 미리 보유하고 있음에도 불구하고, 스테고텍스트를 단일 순방향 패스로 채점하는 대신 송신자의 증분 디코딩을 단계별로 재현한다. 두 지름길 모두 ~9배 빠르게 측정되며 어느 것도 확률을 보존하지 않는다.
로짓 이후의 모든 것은 float64이며 CPU와 GPU에서 비트 단위로 동일한 경계를 생성하므로, 순방향 패스가 스테고텍스트를 하드웨어에 묶는 유일한 단계이다.
정밀도는 메시지 길이로부터 ceil(bit_length * log10(2)) + margin으로 도출되며 다른 무엇보다 먼저 설정된다. 이는 메시지에 따라 확장된다: 2**256은 78자리가 필요하고 2**8192는 2466자리가 필요하다.
그 임계값 미만의 정밀도는 깔끔하게 실패하지 않는다. 비밀 값의 하위 자릿수가 반올림되어 사라지고, 구간이 몇 개의 토큰 내에 붕괴하며 종료 테스트가 조기에 발동하므로, 실행이 완료되고 산술적으로 도달 불가능한 수치를 보고할 수 있다. 메시지 길이에 관계없이 정밀도를 50자리로 고정하면:
| bits | 50자리에서의 활용률 | 완료된 샘플 | 도출된 정밀도 사용 시 |
|---|---|---|---|
| 256 | 157.05% | 8/20 | 100.17% |
| 512 | 299.67% | 9/20 | 102.48% |
| 1024 | 660.91% | 7/10 | 102.58% |
| 2048 | 1046.40% | 1/5 | 98.18% |
| 4096 | 2483.92% | 3/3, 오류 미발생 | 100.87% |
| 8192 | 4766.49% | 1/2 | 99.07% |
--precision_margin은 도출된 최소값 위의 여유분을 조정한다; 최소값 자체는 강제된다.
알고리즘 3은 (L + R) / 2 - d_s가 (-0.5, 0.5]에 들어갈 때 멈춘다. 이 테스트는 구간이 1보다 좁을 때 발동할 수 있으며, 그때 윈도우 [mid - 0.5, mid + 0.5)는 이를 운반하는 구간보다 넓다. 역회전은 구간 폭에 대한 등거리 변환이므로, 절단을 가로지르는 윈도우는 수신자를 d_s로부터 전체 구간만큼 떨어뜨린다.
따라서 송신자는 중단을 확정하기 전에 수신자의 역회전을 재현하고, 재현이 비밀을 반환하지 않으면 또 다른 토큰을 방출한다. 재현은 순수 Decimal 작업이므로 (순방향 패스의 ~20 ms에 대해 ~0.05 ms) 가드는 측정 가능한 비용이 들지 않는다. --no_verify는 이를 비활성화한다.
Llama-2-7b에서 측정:
| bits | 가드 없이 복구됨 | 가드 사용 시 |
|---|---|---|
| 128 | 291/300 (97.0%) | 300/300 |
| 512 | 64/65 (98.5%) | 40/40 |
| 1024 | 20/20 | 20/20 |
| 2048 | 10/10 | 10/10 |
128비트에서 가드는 0.095%의 추가 생성 토큰을 소모했다. 1024와 2048비트의 샘플은 비율을 한정하기에 너무 적으므로 — 3% 비율에서 0/20이 그렇지 않은 경우보다 더 유력한 결과이다 — 더 긴 메시지가 다르게 작동한다는 증거가 되지 않는다. 가드 사용 시 370/370이 복구되었다.
가드 없이는 송신자가 차이를 구별할 수 없다: 성공을 보고하고 무관한 비트로 디코딩되는 스테고텍스트를 방출한다. 비율은 추첨에 따라 달라진다; 한 99-프롬프트 실행에서는 아무것도 나타나지 않았는데, 이는 3% 비율에서 약 5%의 확률로 발생하며, 재실행 시 정확히 재현된다. 동일한 샘플이 45에서 200자리까지 모든 작동 정밀도에서 실패하므로, 이 동작은 산술이 아니라 종료 테스트에서 비롯된다.
가드는 이를 비활성화한 상태로 삽입하고 실제 추출기를 실행하여 정답과 대조 검증되었다: 가드가 표시하는 샘플은 정확히 수신자가 실패하는 샘플이며, 거짓 양성도 없고 누락도 없다.
LM 순방향 패스는 Llama-2-7b에서 생성 단계의 95–97%를 차지하며, 7B fp16 모델의 batch-1 디코딩은 메모리 대역폭 루프라인에 위치한다 (측정된 ~18.7 ms에 대해 토큰당 ~13.5 ms). 따라서 삽입 속도는 모델의 디코딩 속도를 따르며, 코딩 자체는 이에 거의 영향을 미치지 않는다.
더 작은 모델에서는 순방향 패스가 더 이상 지배적이지 않고 단계별 CPU 작업이 가시화된다. GPT-2에서 128비트, 공유 머신에서 셀당 100개 메시지로 종단 간 측정:
| torch CPU 스레드 | bits/s |
|---|---|
| 1 | 1320, 1205 |
| 16 | 1232, 756 |
| 20 (torch 기본값) | 352 |
처리량은 코딩 경로의 어떤 것이 아니라 CPU 스레드 경합에 의해 좌우되며, 멀티스레드 수치는 실행마다 재현되지 않는다. 따라서 모든 진입점은 torch.set_num_threads(1)을 호출한다: 여기서 각 CPU 연산은 단일 어휘 크기의 패스로, 스레드 풀에서 이득을 얻기에는 너무 작고 풀이 다른 작업과 경합할 때 크게 손실되기에 충분히 크다. 이는 예측 가능성을 얻는 것이지 유휴 머신에서의 속도 향상이 아니다.
나머지 단계별 선택은 개별적으로 저렴하며 한 스레드에서 종단 간 처리량을 측정 가능하게 변화시키지 않는다; 비용이 들지 않기 때문에 선택된다:
min(config.vocab_size, len(tokenizer.vocab))로 슬라이스된다. 이는 임베딩 행렬을 재할당하지 않고 모델 객체를 그대로 두는데, 이는 동일한 모델이 여러 실행에 걸쳐 공유될 때 중요하다.128비트 메시지, 전체 어휘, temperature 1.0, RTX 6000 Ada, CPU 스레드 하나, 그 외에는 유휴 GPU. 모델당 100개 메시지 (Llama-2-7b는 60개), 모두 왕복되고 검사됨:
| model | embed | extract | tokens/msg | utilisation |
|---|---|---|---|---|
| GPT-2 | 1386 bits/s | 1385 bits/s | 22.3 | 99.4% |
| OPT-1.3b | 754 bits/s | 753 bits/s | 30.4 | 98.7% |
| Llama-2-7b | 183 bits/s | 183 bits/s | 45.4 | 98.9% |
추출은 삽입 속도로 실행되는데, 둘 다 토큰당 하나의 순방향 패스를 수행하고 역회전 체인은 그 옆에서 무시할 수준이기 때문이다.
처리량은 GPU의 다른 작업에 둔감하다 — 완전히 로드된 카드에서도 동일한 수치가 몇 퍼센트 이내로 나온다 — 왜냐하면 워크로드는 장치를 포화시키는 어떤 것이 아니라 지연에 묶인 단일 토큰 순방향 패스의 체인이기 때문이다. ~6%의 실행 간 변동은 추출된 프롬프트에서 비롯된다: bits/s는 bit_length / time이고 시간은 메시지가 필요로 하는 토큰에 따라 확장되므로, 더 낮은 엔트로피의 프롬프트 샘플은 더 느리게 읽힌다.
Python의 Decimal %는 절단 나눗셈을 사용하며 음수 나머지를 반환할 수 있다 (Decimal('-19672') % Decimal('65536')은 45864가 아니라 -19672를 준다). 역회전은 진정한 모듈로를 요구하므로, decimal_mod는 내림 나눗셈 의미론을 사용한다:
def decimal_mod(a, m):
return a - m * (a / m).to_integral_value(rounding=ROUND_FLOOR)