본문 바로가기
빌드노트 검색

빌드노트

AES-GCM 복호화가 OperationError로 끝날 때 확인할 입력

AES-GCM 복호화의 OperationError가 나면 키 선택과 IV·AAD·암호문·태그를 호출 직전에서 대조한다. 합성 입력의 정상 복호화와 한 비트 변경 결과를 비교하고, 인증 실패 뒤 문자열 변환이나 평문 처리를 계속하지 않는 방법을 설명한다.

Web Crypto의 AES-GCM에서 await subtle.decrypt(...)가 OperationError로 끝난다면, 문자열 디코더를 바꾸기 전에 그 호출에 도착한 입력부터 확인한다. 인증에 실패한 AES-GCM 복호화는 평문을 반환하지 않는다. 다만 이 예외명만으로 암호문·IV·AAD 중 무엇이 달라졌는지, 악의적인 변조였는지까지 알 수는 없다.

암호화 때의 입력과 복호화 직전의 값을 대조한다

암호화 호출에 넘긴 키·IV·AAD·태그 길이를 기준으로 복호화 호출을 확인한다. 암호문은 encrypt()가 반환한 바이트와 decrypt()에 넘길 바이트를 비교한다. 실제 키나 민감한 내용을 로그에 덤프하지 말고, 합성 입력으로 자기 코드의 전달 경로를 재현해 차이가 생기는 지점을 찾는다.

  • 키 선택: 저장한 키 식별자가 해당 암호문을 만든 키를 가리키는지 확인한다. 식별자 문자열만 같다고 충분한 것은 아니며, 실제로 어떤 키를 불러오는지 봐야 한다. 복호화 함수에서 새 키를 생성해도 기존 암호문을 복원할 수는 없다.
  • IV: 초기화 벡터인 iv에 암호화 때 사용한 바이트를 전달한다. 복호화할 때 getRandomValues()로 새 IV를 만드는 코드가 있다면 이 부분부터 고친다.
  • AAD: additionalData는 암호화되지는 않지만 함께 인증되는 추가 데이터다. 암호화 때 넣었다면 복호화에도 같은 바이트를 전달한다. JSON을 다시 만들었다면 공백이나 필드 순서 차이도 확인한다. 선택 인자라는 말이 암호화에 쓴 비어 있지 않은 AAD를 복호화에서 생략해도 된다는 뜻은 아니다. MDN의 AesGcmParams 설명 (새 탭에서 열림)
  • 암호문과 태그: Web Crypto의 AES-GCM 암호화 결과에는 암호문 뒤에 인증 태그가 붙어 있다. W3C의 AES-GCM 연산 정의 (새 탭에서 열림)에 따라 이 바이트열 전체를 복호화에 전달한다. 태그를 잘랐거나, Base64에서 복원한 바이트 대신 Base64 문자열 자체를 UTF-8로 인코딩해 넘기지는 않았는지 본다. 길이가 같아도 내용은 다를 수 있다.

tagLength도 암호화 때와 맞아야 한다. 단위는 비트다. 다른 라이브러리가 태그를 별도 필드로 준다면 그 라이브러리와 사용 중인 프로토콜의 형식을 먼저 확인한다.

새 암호화의 IV 고유성과 기존 암호문의 복호화에 필요한 IV 일치는 함께 지켜야 한다. 같은 키로 새 메시지를 암호화할 때는 이전 암호화에 사용하지 않은 IV가 필요하다. 이미 만들어진 암호문을 복호화할 때는 그 암호화의 IV를 사용한다. Node.js 22.23.2의 IV 설명 (새 탭에서 열림)

복호화가 성공한 다음에 문자열로 바꾼다

다음은 기존 비동기 함수에서 복호화 성공과 문자열 변환을 잇는 부분이다. subtle은 사용 중인 Web Crypto의 SubtleCrypto 객체이고, selectedKey는 해당 암호문을 만든 키 재료를 가진 복호화용 CryptoKey다. receivedIv, receivedAad, receivedCiphertext는 위에서 대조한 Uint8Array이며, 암호화 때 tagLength: 128을 사용한 경우다. 키 생성과 수신 처리를 포함한 독립 실행 예제가 아니므로, 이 이름들을 자기 코드의 실제 입력에 연결한다.

const plain = await subtle.decrypt(
  { name: 'AES-GCM', iv: receivedIv, additionalData: receivedAad,
    tagLength: 128 },
  selectedKey, receivedCiphertext);
const text = new TextDecoder().decode(plain);

decrypt()가 거절되면 마지막 줄에 도달하지 않는다. 호출자에게 실패를 전달하거나, 예외를 잡는 곳에서 실패 결과를 반환해 해당 처리를 끝낸다. 빈 문자열·수신 암호문·이전 요청의 평문을 대신 넣고 표시나 파싱을 계속하지 않는다. 반대로 복호화는 성공했는데 문자열이 깨진 경우라면 반환된 평문의 인코딩을 조사한다.

같은 memo에서 한 조건씩 바꾼 결과

위 발췌 코드에 합성 입력을 연결해 2026년 10월 23일 Windows의 Node.js 22.23.2에서 실행했다. 메모리에만 만든 새 비추출 AES-256-GCM 키와 새 무작위 12바이트 IV로 memo를 한 번 암호화했다. AAD는 UTF-8의 kind=note, 태그 길이는 128비트로 고정했다. 키를 내보내거나 출력하지 않았다.

  • 원본 입력: 문자열 memo를 반환했다.
  • 암호문 첫 바이트의 한 비트만 바꾼 복사본: OperationError였다.
  • AAD 첫 바이트의 한 비트만 바꾼 복사본: OperationError였다.
  • IV 첫 바이트의 한 비트만 바꾼 복사본: OperationError였다.
  • 원본 입력으로 다시 복호화: 다시 memo를 반환했다.

세 변경 모두 입력 길이는 유지했고, 거절된 경우 문자열 디코딩 호출은 0회였다. 여기서 원인을 구분할 수 있는 것은 바꾼 조건을 알고 있기 때문이다. 실제 오류에서 같은 예외명이 나왔다는 사실만으로 AAD 오류나 공격자의 변조를 특정할 수는 없다. 별도 대조에서는 태그의 한 비트 변경과 허용되지 않는 tagLength: 40도 같은 예외명으로 거절됐다. MDN의 decrypt 예외 설명 (새 탭에서 열림)도 잘못된 크기의 파라미터와 암호문 복호화 실패를 OperationError의 예로 든다.

자기 코드에서도 합성 입력의 정상 복호화부터 확보한다. 그것부터 실패한다면 변형 결과로 원인을 좁히기 전에 키 선택과 암호화 때의 입력을 복원해야 한다. 정상 입력은 성공하고 전달 경로를 거친 입력만 실패한다면 두 지점 사이에서 바뀐 바이트나 키 선택을 추적한다. 이 판단은 위 비교에서 얻은 진단 방법이며 실제 전송 장애를 재현한 결과는 아니다.

직접 실행한 범위는 Windows의 Node.js 22.23.2 Web Crypto와 위의 짧은 합성 입력이다. 브라우저, 실제 네트워크·저장·Base64 왕복이나 JSON 재직렬화, 장기간 사용하는 키의 IV 고유성 관리와 다른 라이브러리와의 상호 운용은 검증하지 않았다. 이 비교는 입력 일치와 인증 실패 후 처리 중단을 확인한 것이며, 자체 암호 프로토콜을 구현하는 지침으로 사용하지 않는다.

← 전체 글 보기