본문으로 건너뛰기

암호화와 변조 검출

「바이너리 형식」으로 돌아가기


파일 암호화

flags의 bit0이 서면 keyCheck부터 파일 끝까지가 암호문입니다. 헤더의 나머지 — 시그니처·버전·flags·cipher·nonce·mac — 는 평문으로 남습니다. 필드의 자리는 암호화 여부와 무관하게 같습니다.

  • 암호는 ChaCha20(RFC 8439, 256비트 키, 블록 카운터 0)입니다. 모든 언어에 외부 의존성 없이 들어가고, 하드웨어 가속이 없는 기기에서도 빠르며, 스트림 XOR이라 길이가 변하지 않습니다byteLength 합계 검증을 비롯한 구조 검사가 평문 기준 그대로 성립합니다.
  • nonce는 난수가 아니라 평문의 SHA-256 앞 12바이트입니다. 난수 nonce는 같은 입력에서 다른 파일을 만들어 결정적 출력과 골든 트리를 깨뜨립니다. 같은 키에서 내용이 다르면 nonce도 다르므로 keystream 재사용은 없습니다.
  • keyCheck가 「키가 다르다」와 「파일이 손상됐다」를 구분합니다. 복호한 4바이트가 54 43 42 00이 아니면 그 자리에서 멈춥니다. 파일 앞의 시그니처와 같은 값이지만 역할이 다릅니다 — 시그니처는 도구가 파일 종류를 알아보는 것이고, 이쪽은 키가 맞는지 보는 것입니다.
  • layer 순서는 인코딩 → (압축, 예약) → 암호화 → MAC입니다. 압축은 반복이 있어야 일하므로 암호문 위에서는 무의미하고, MAC은 저장되는 바이트에 대해 계산하므로 가장 바깥입니다.

암호화가 하지 않는 것 — 스트림 XOR의 가단성

암호화는 변조를 막지 않습니다. ChaCha20은 keystream XOR이므로 암호문의 비트를 뒤집으면 평문의 같은 자리 비트가 뒤집힙니다 — 키가 없어도 그렇습니다. 어느 오프셋이 어느 값인지 알아낸 공격자는 그 자리를 원하는 대로 밀 수 있고, 복호는 성공하며, keyCheck도 맞고, 구조 검사도 통과합니다.

그래서 「값을 바꿔 넣어도 그대로 로드된다」를 실제로 없애는 것은 아래의 MAC입니다.

변조 검출 — MAC

mac 16바이트가 0이 아니면, 그것은 파일 자신의 바이트에서 계산된 인증 태그입니다.

  • HMAC-SHA-256의 앞 16바이트입니다. 덮는 범위는 mac 필드 자신을 뺀 파일 전부 — [0, 22)[38, 끝)입니다.
  • 암호화 뒤에 계산합니다(encrypt-then-MAC). 그래서 리더가 복호 전에 검증할 수 있고, nonce·flags·version을 바꾸는 것도 함께 검출됩니다.
  • 키는 암호화 키와 별개입니다. 암호화와 MAC은 각각 켤 수 있고, 4개 조합이 전부 유효합니다.
  • 파일 바이트의 함수이므로 같은 입력은 같은 인증 태그를 냅니다 — 결정적 출력이 유지됩니다.

검출되는 변조와 검출되지 않는 변조

구조 검증이 검출하는 것은 파일의 구조가 어긋난 경우뿐입니다. 고정폭 원소는 어떤 비트 패턴도 유효한 값이므로, 값 하나를 바꾸는 변조는 구조를 어긋나게 하지 않습니다.

변조구조 검증만MAC이 있으면
f32·i32의 4바이트를 다른 값으로통과합니다 — 바뀐 값이 그대로 읽힙니다거부
bool 1바이트를 0↔1통과합니다거부
암호문의 비트 뒤집기(키 없이)통과합니다 — 평문의 같은 비트가 뒤집힙니다거부
길이 접두어·런 길이 변경거부거부
파일 절단·바이트 삽입거부거부

첫 세 줄이 이 기능의 존재 이유이고, 검증 게이트에서 ①과 ②를 같은 테스트로 확인합니다 — MAC이 없으면 통과한다는 것까지 단정하지 않으면 근거가 기록되지 않기 때문입니다.

MAC 키도 클라이언트에 실립니다. 바이너리에서 키를 꺼낼 수 있는 공격자는 태그를 다시 계산할 수 있습니다. 이 층이 바꾸는 것은 변조의 난이도입니다 — 헥스 에디터로 파일을 고치는 수준에서, 클라이언트를 분석해 키를 꺼내야 하는 수준으로 올라갑니다.

리더가 하는 일

리더는 바이트를 받으면 먼저 open을 부릅니다. 암호화도 MAC도 없는 파일은 그 호출에서 그대로 돌아옵니다 — 그래서 이 호출은 프로젝트가 키를 쓰든 안 쓰든 로드 경로에 들어갑니다.

순서는 시그니처 확인, MAC 검증, 복호입니다.

복호는 제자리에서 일어나고, 돌아오는 것은 같은 배열에 대한 창이지 사본이 아닙니다.

헤더의 자리가 고정이므로 창은 파일 전체이고, 리더는 소비한 필드(flags의 bit0, cipher, nonce)를 평문 파일이 갖는 값으로 되돌립니다.

같은 버퍼에 open을 두 번 호출해도 결과가 같습니다.

파일의 mac리더의 MAC 키결과
0없음검사하지 않고 읽습니다 — MAC을 쓰지 않는 프로젝트
0있음거부. 16바이트를 0으로 덮는 것으로 검사가 없어지면 이 기능은 아무것도 지키지 않습니다
비영있음검증하고, 어긋나면 거부
비영없음검사하지 않고 읽습니다 — 검증할 수단이 없고, 구버전 클라이언트가 새 데이터를 못 읽게 되는 편이 나쁩니다

그러므로 켜는 순서가 있습니다 — 데이터가 먼저, 클라이언트의 키가 나중입니다. 반대로 하면 아직 MAC이 없는 데이터를 클라이언트가 거부합니다.

리더 옵션 VerifyMac(언어별 표기는 다릅니다)을 거짓으로 두면 검사를 건너뜁니다. 기본은 검사입니다. 이 스위치가 클라이언트 안의 불 변수라는 것은 그대로 적어 둡니다 — 뒤집을 수 있는 공격자는 이미 키도 꺼낼 수 있습니다.

  • 키 없이(또는 32바이트가 아닌 키로) 암호화된 파일을 열면 그 사실을 출력하고 멈춥니다.
  • 틀린 키로 열면 keyCheck가 나오지 않으므로 「이 파일이 쓰인 키가 아니다」를 출력하고 멈춥니다. 값을 하나도 읽기 전입니다. MAC이 통과해도 이 검사는 따로 합니다 — 두 키는 다른 키이므로, MAC 통과는 「변조되지 않았다」이지 「복호 키가 맞다」가 아닙니다.
  • open을 부르지 않고 바이트를 그대로 넘기면, 헤더 검사가 flags의 bit0을 보고 복호되지 않았다고 보고합니다.

이 두 layer가 하는 일과 하지 않는 일

대칭키도 MAC 키도 파일을 읽는 클라이언트 안에 실립니다.

바이너리에서 키를 꺼낼 수 있는 공격자에게 이것은 장벽이 아니고, 그런 공격자를 막는 형식은 존재하지 않습니다.

두 층이 없애는 것은 둘입니다. 「데이터 파일을 에디터로 열면 다 보인다」가 암호화로, 「값을 바꿔 넣어도 그대로 로드된다」가 MAC으로 없어집니다.

그러므로 지켜야 할 것은 키를 클라이언트에서 숨기는 것이 아니라, 키가 저장소와 빌드 로그와 이슈 트래커에 남지 않는 것입니다.

키를 만드는 방법, recipe가 키의 위치만 받는 이유, 서버와 클라이언트에서 각각 무엇을 지킬 수 있는지는 TCB v104 §4MAC과 파일 시그니처에 있습니다.

켜는 방법은 내보내기에 있습니다.

키 회전도 비대칭 서명도 없습니다. 키가 샜다고 판단되면 할 일은 하나입니다 — 새 키를 만들고, 데이터를 다시 내보내고, 클라이언트를 다시 배포합니다.

매니페스트 해시와의 관계

업데이터가 검사하는 매니페스트의 파일 해시는 전송 중 손상을 검출합니다. 키가 없는 해시이고 매니페스트 자신도 함께 내려받는 파일이므로, 파일을 고친 사람이 해시도 고칠 수 있습니다. MAC은 파일이 어디서 왔든 그 파일에 대해 성립합니다. 둘은 대체 관계가 아닙니다.