비밀번호를 변경했을 때 암호화된 데이터는 어떻게 될까? 전체 재작업 없이 '키를 감싸는' 구조
요약
기존의 직접 암호화 방식은 비밀번호 변경 시 전체 데이터를 재암호화해야 하는 운영상의 위험이 있습니다. 이 글에서는 데이터 자체를 건드리지 않고 키만 관리하는 'Envelope Encryption' 방식을 소개합니다. 이는 데이터 키와 이를 감싸는 키(Wrapping Key)로 역할을 분리하여 보안성과 시스템의 안정성을 높입니다.
핵심 포인트
- 직접 암호화 방식은 비밀번호 변경 시 전체 데이터를 재작성해야 하는 위험이 있습니다.
- 문제는 계산 속도가 아닌, 데이터 전체를 다시 작성하는 행위 자체에 있습니다.
- 해결책은 무작위 '데이터 키'와 이를 감싸는 '감싸는 키'로 역할을 분리하는 것입니다.
- 비밀번호 변경 시 암호화된 데이터(암호문)는 그대로 두고, 오직 '감싼 키'만 업데이트합니다.
※이 글은 Zenn의 게시글을 전재한 것입니다.
개인 정보를 암호화하여 저장하는 시스템을 만들 때, 가장 먼저 떠올리는 방식은 '관리자 비밀번호로 키를 생성하고, 그 키로 이름을 직접 암호화'하는 형태입니다.
이 방식은 작동합니다. 하지만 실제 운영에 들어가면 다음과 같은 상황에서 어려움을 겪게 됩니다.
- 관리자가 교대되어 비밀번호를 변경해야 할 때
- 관리자를 한 명 추가하고 싶지만, 동일한 비밀번호를 공유하고 싶지 않을 때
- 같은 데이터를 다른 PC에서도 읽을 수 있게 해야 할 때
이 글에서는 이러한 변경 사항들을 데이터 자체에 손대지 않고 처리할 수 있는 '데이터 키를 감싸는' 설계(Envelope Encryption이라고도 불립니다)를, 제가 사내용으로 만들고 있는 업무 시스템(스프레드시트 + Google Apps Script와 Windows 상주 툴)을 예로 들어 정리합니다.
이 내용은 이전 글인 '환자 번호나 직원 번호를 「대조는 가능하지만, 유출되어도 읽을 수 없는」 형태로 보유하기'에서 마지막에 '다른 글에서 작성하겠다'고 했던 부분입니다.
비밀번호로부터 만든 키로 이름을 직접 암호화하면, 비밀번호와 암호문이 1 대 1로 연결됩니다.
| 하고 싶은 일 | 비밀번호로 직접 암호화했을 경우 |
|---|---|
| 비밀번호 변경 | 전원분을 이전 키로 열고, 새 키로 재암호화하여 시트에 다시 작성해야 함 |
| ... | |
| 여기서 오해하기 쉬운 부분이 있는데, 문제가 되는 것은 계산 속도가 아닙니다. 직접 테스트해보면 1,000건을 재암호화하는 데 걸린 시간은 22ms(Apple M1・Node.js)에 불과했습니다. |
문제가 되는 것은 전체 데이터를 다시 작성해야 하는 그 행위 자체입니다. 도중에 멈추게 되면, 이전 키의 행과 새 키의 행이 뒤섞이게 됩니다. 재작성하는 도중에 누군가 신청을 하면, 해당 행이 어느 키로 암호화된 것인지 알 수 없게 됩니다. 비밀번호를 바꿀 때마다 이런 위험한 작업이 발생합니다.
해결책은 키를 2단계로 나누는 것입니다.
- 데이터 키 (Data Key): 무작위로 만든 키입니다. 이름의 암호화에는 이것만 사용합니다. 한 번 만들면 변경하지 않습니다 -
- 감싸는 키 (Wrapping Key): 관리자 비밀번호로부터 만드는 키입니다. 데이터 키를 암호화(감싸는)하는 용도로만 사용합니다
저장하는 것은 '감싼 데이터 키'(수십 바이트)뿐입니다. 비밀번호도, 데이터 키도 원형 그대로 어디에도 저장하지 않습니다.
이렇게 하면 앞서의 표가 다음과 같이 변경됩니다.
| 하고 싶은 일 | 데이터 키를 감싸는 경우 |
|---|---|
| 비밀번호 변경 | 이전 비밀번호로 감싼 것을 열고, 새 비밀번호로 다시 감쌉니다. 이름의 암호문은 건드리지 않습니다 |
| 관리자 추가 | 동일한 데이터 키를 그 사람의 비밀번호로 한 번 더 감싸줍니다 |
| 관리자 퇴사 | 그 사람의 감싼 것만 삭제합니다(※아래에 한계점 있음) |
| 다른 단말기에서 읽기 | 해당 단말기 전용 보호로 한 번 더 감쌉니다 |
변경되는 것은 오직 '감싸는' 한 줄뿐입니다.
암호화 구성 요소는 직접 만들지 않습니다. 브라우저(와 Node.js)에 표준으로 내장된 Web Crypto API만으로 작성할 수 있습니다. 아래는 설명용으로 작성한 최소 예시입니다.
const { subtle } = globalThis.crypto;
const enc = new TextEncoder();
async function fromPassword(password, salt) {
...
1회의 계산을 통해 역할이 다른 두 가지 값을 분리해냅니다.
- 감싸는 키: 브라우저 내부에서만 사용하며 서버로 전송하지 않습니다 -
- 확인용 값: 서버로 전송하여 '비밀번호가 맞는지' 판별하는 데 사용합니다. 서버가 가진 것은 그 해시값뿐입니다
서버는 '비밀번호가 일치하는지'는 알 수 있지만, '감싼 것을 열 키'는 가지고 있지 않습니다. 따라서 서버 계정이 탈취되더라도 그것만으로는 이름을 읽을 수 없습니다.
async function setup(password) {
const salt = crypto.getRandomValues(new Uint8Array(16));
const dataKey = await subtle.generateKey({ name: 'AES-GCM', length: 256 }, true, ['encrypt', 'decrypt']);
...
평소에는 extractable = false로 열 수 있습니다. 이렇게 하면 열린 데이터 키는 브라우저 내에서 사용할 수는 있지만, 내부 내용을 추출할 수는 없는 상태가 됩니다. 화면 내에서 작동하는 스크립트에 오류가 발생하더라도, 데이터 키 자체를 외부로 출력할 수는 없습니다.
async function changePassword(oldPw, newPw, salt, wrapped) {
const dataKey = await open(oldPw, salt, wrapped, true); // 이 경우에만 추출 가능한 형태로 열 수 있음
const newSalt = crypto.getRandomValues(new Uint8Array(16));
...
직접 실행한 결과입니다.
재포장(ms) 289 wrapped bytes 40
새 비밀번호로 복호화: 야마다 타로
기존 비밀번호: OperationError
...
- 재포장은 약 0.3초가 소요되었습니다. 대부분은 의도적으로 지연시킨 부분(60만 회) 때문입니다.
- 새로운 비밀번호로는 변경 전에 암호화된 이름이 그대로 읽힙니다. — 이전 비밀번호로는 열 수 없습니다 (AES-KW는 포장이 손상되지 않았는지 확인하기 때문에, 잘못된 키를 사용하면 오류가 발생합니다).
- 평소 방식(extractable = false)으로 연 데이터 키는 추출하려고 시도해도 거부됩니다.
이러한 사고방식을 Windows의 상주 도구에서도 사용하고 있습니다. 이 도구는 병원 내 공유 폴더에 있는 대상자 데이터를 정해진 몇 대의 컴퓨터에서 매일 읽어들이는 시스템입니다.
매일 아침 비밀번호를 입력받는 것은 현실적이지 않습니다. 그래서 하나의 데이터 키를 두 가지 방식으로 포장(wrap)하여 사용하고 있습니다.
| 포장 방식 | 사용 시나리오 |
|---|---|
| 기기별 OS 표준 보호 (해당 기기에서만 열 수 있음) | 매일의 작동. 아무것도 입력하지 않음 |
| 관리자 비밀번호 | 기기를 추가하거나 교체할 때만 |
새로운 기기를 추가할 때는, 관리자 비밀번호로 포장을 풀고, 그 기기의 보호 방식으로 다시 한 번 포장하여 저장합니다. 다른 기기나 데이터에는 접근할 수 없습니다.
여기서도 부품을 직접 만든 것은 아닙니다. 오래된 실행 환경에는 AES-GCM과 같은 '암호화와 변조 감지가 통합된 방식'이 없었기 때문에, 표준 암호화 방식과 표준 변조 감지 방식을 결합하여 사용하고 있습니다.
비밀번호 확인 시에는 실패 횟수 제한(몇 번 틀리면 일정 시간 잠금)을 적용합니다. 여기서 한 번 리뷰를 통해 지적받은 부분이 있었습니다.
처음에는, 횟수 제한을 로그인 화면 처리에만 두고 있었습니다. 그런데 Google Apps Script의 Web App에서는, 화면에서 호출 가능한 함수는 화면 과정을 거치지 않고 직접 호출할 수도 있습니다. 즉, 로그인 화면 단계를 건너뛰고 다른 관리용 함수에 잘못된 비밀번호를 몇 번이고 보낼 수 있게 됩니다.
수정 방법은 횟수 제한을 모든 관리용 처리가 반드시 통과하는 공통의 인증 함수로 옮기는 것이었습니다.
횟수 제한은 '진입 화면'이 아니라, '지켜야 할 처리가 반드시 거치는 부품'에 두어야 합니다.
이러한 지적을 해준 것은 설계와 리뷰를 담당한 AI였습니다. 구현을 맡은 AI와 분리되어 있었기 때문에, 작성자 본인도 인지하기 어려운 허점이 발견되었습니다.
퇴사한 관리자의 포장을 삭제하더라도, 그 사람이 데이터 키를 기억하고 있다면 읽을 수 있습니다. 포장을 삭제하는 것은 '앞으로 그 사람의 비밀번호로는 열 수 없게 만드는 것'일 뿐이지, '이미 알고 있는 키를 잊게 하는 것'이 아닙니다. 정말로 접근을 차단하려면, 데이터 키 자체를 새로 만들고 모든 데이터를 재암호화해야 합니다 (이때만은, 재작성을 피할 수 없습니다).
포장이 하나밖에 없으면, 비밀번호를 잊어버린 순간 모두의 데이터가 읽을 수 없게 됩니다. 관리자용 포장은 두 개 이상 보유하는 것이 안전합니다(예: 예비 관리자 비밀번호를 종이에 적어 봉인하여 보관). - 화면에 표시된 이름은 그 화면을 보고 있는 사람에게는 읽힐 수 있습니다. 암호화는 저장 장소의 보호입니다.
-
비밀번호로 데이터를 직접 암호화하면, 비밀번호 변경이나 관리자 교체 시마다 **전체 항목 재작업(全件の書き直し)**이 발생한다. 문제는 속도가 아니라 재작업의 위험성이다. 무작위 **데이터 키 (Data Key)**를 하나 만들고, 이 키를 비밀번호에서 만든 키로 감싸서(wrap) 저장한다. 변경은 감싸기만 다시 하면 된다. 감싸기는 여러 방식으로 할 수 있다. 관리자별, 단말기별로 감싸면 매일의 노력 없이 추가나 교체에 대응할 수 있다.
-
비밀번호로부터 '감싸는 키(Wrapping Key)'와 '확인용 값'을 분리하여 만들고, 서버에는 감싸는 키를 전달하지 않는다.
-
횟수 제한은 지키고 싶은 처리가 반드시 통과하는 공통 부품에 배치한다.
-
frontmatter에
published: true를 작성했기 때문에,zenn_publish.js(150행)가 승인 전에 commit되었고 → User가 Gemini 기사를 push했을 때 함께 공개되었다. - 사용자 판단 = 이대로 공개 (2026-10-14). 공개 버전은 vault 최신 버전과 본문 차이분 0을 확인했다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기