환자 번호나 직원 번호를 '대조는 가능하지만, 유출되어도 읽을 수 없는' 형태로 보관하는 방법: 해싱만으로는 지킬 수 없는 이유와 3가지 대책
요약
개인 식별 번호(환자/직원 번호 등)를 안전하게 보관하는 방법을 다룹니다. 단순 해싱은 후보군이 적을 경우 무차별 대입 공격에 취약하므로, HMAC와 같은 키 기반 해시를 사용하여 데이터 유출 시에도 원본 복구를 막아야 합니다. 또한, 표시용 값은 매번 다른 암호화 방식을 사용해 동일성이 노출되지 않도록 설계해야 합니다.
핵심 포인트
- 단순 해싱은 후보군이 적으면 무차별 대입 공격에 취약합니다.
- 데이터 대조에는 '열쇠(Key)'가 필요한 HMAC를 사용해야 안전합니다.
- 표시용 값은 매번 다른 암호화 방식(IV)을 사용하여 동일성을 숨겨야 합니다.
- 대조 규칙 설계 전, 실물 데이터의 표기 차이를 반드시 통일하는 과정이 필요합니다.
※이 기사는 Zenn의 글에서 전재된 내용입니다.
"개인 정보는 해시화해서 저장했기 때문에 안전합니다".
이 설명은 값의 종류가 적은 데이터에는 통하지 않습니다. 6자리 환자 번호나 직원 번호, 생년월일 등은 모든 후보를 해시하여 비교하면 원상 복구할 수 있기 때문입니다.
이 글에서는 자릿수가 적은 개인 정보를 '대조는 가능하지만, 유출되어도 읽을 수 없는' 형태로 보관하기 위한 설계를 정리합니다. 다루는 사례는 제가 사내용으로 만들고 있는 업무 시스템(스프레드시트 + Google Apps Script와 Windows 상주 툴)입니다.
시험해 보겠습니다. 6자리 번호 804721을 SHA-256에 한 값만 유출되었다고 가정합니다.
const crypto = require('crypto');
const target = crypto.createHash('sha256').update('804721').digest('hex');
console.time('6자리를 전부 시도');
...
찾음: 804721
6자리를 전부 시도: 397.13ms
제 노트북(Apple M1)에서 0.4초가 걸렸습니다. 특별한 도구는 아무것도 사용하지 않았습니다.
SHA-256이 약해서가 아닙니다. 후보가 100만 가지밖에 없기 때문이 문제입니다. 생년월일이라면, 100년치여도 약 36,500가지 밖에 없습니다.
| 데이터 | 후보 수 | 전부 시도 시간 예상 |
|---|---|---|
| 6자리 번호 | 100만 | 1초 미만 |
| ... |
'솔트(랜덤 값)를 추가하면?'이라고 생각할 수도 있습니다. 솔트가 해시와 같은 장소에 저장되어 있다면, 상대방은 솔트를 더해서 똑같이 100만 번 시도하기만 하면 됩니다. 건당 수고로움은 변하지 않습니다.
대조(예: '이 번호의 사람이 목록에 있는지')하는 데 필요한 것은, 같은 번호에서 반드시 같은 값이 나오는 것입니다. 이것은 그대로 남겨두되, 열쇠를 모르는 사람에게는 계산할 수 없도록 합니다. 그것이 HMAC(키 기반 해시)입니다.
// Google Apps Script의 예
function matchKey_(rawNo) {
// 표기 차이를 맞춘 후 변환합니다 (예: 맨 앞 0을 제거)
...
열쇠(MATCH_SECRET)
는 데이터와 다른 장소에 둡니다. 데이터(시트)만 유출되어도, 열쇠가 없으면 100만 가지를 시도할 수 없습니다.
실제로 어려웠던 점이 여기입니다. 같은 환자 번호라도,
- 직접 입력하는 경우: 6자리
- 바코드 내용물은 7자리이고 맨 앞에
0이 붙음 - 종이에 인쇄된 번호는
0없음
처럼, 장소에 따라 표기가 일치하지 않았습니다. 해시는 한 글자만 달라도 완전히 다른 값이 됩니다. 표기를 맞추지 않고 변환하면, 본인인데도 일치하지 않습니다.
대조 규칙을 만들기 전에, 실물(바코드 내용물・인쇄・입력 화면)을 비교해 보세요. 통일하는 방법을 정한 후에 변환합니다.
HMAC는 원상 복구할 수 없습니다. 화면에 이름이나 번호를 표시하고 싶다면, 원상 복구가 가능한 암호화가 별도로 필요합니다.
여기서, 대조용과 같은 '같은 값이면 같은 결과' 방식을 사용해서는 안 됩니다. 암호문이 같으면 같은 사람이라는 것을 알아버릴 수 있기 때문입니다. 표시용은 매번 랜덤한 값(IV)을 섞어서, 같은 이름이라도 매번 다른 암호문이 되는 방식(AES-GCM 등)을 사용합니다.
// 브라우저 표준 기능 (Web Crypto API)의 예
async function encryptForDisplay(dataKey, text) {
const iv = crypto.getRandomValues(new Uint8Array(12)); // 매번 다른 값
...
즉, 하나의 항목에 역할이 다른 두 가지 값을 갖게 합니다.
| 용도 | 방식 | 성질 |
|---|---|---|
| 대조용 | HMAC (키 기반 해시) | 같은 입력 → 같은 값. 원상 복구 불가 |
| 표시용 | AES-GCM 등 | 같은 입력이라도 매번 다른 값. 열쇠가 있으면 복구 가능 |
번호 804721을 각각의 방식으로 2번씩 적용한 실제 출력입니다 (열쇠는 설명용 가상의 것입니다. 긴 값은 중간에 잘랐습니다).
⚠️ [IMG:N] 형식 토큰은 이미지 placeholder 입니다. 번역하지 말고 원래 위치에 그대로 유지하세요.
| 방식 | 1회차 | 2회차 | 읽을 수 있는 것 |
|---|---|---|---|
| SHA-256 (키 없음) | d491867edc02… | d491867edc02… | 매번 동일. 누구나 같은 계산이 가능 → 모두 시도하면 복원 가능 |
| HMAC (키 A) | YTlhRdb7T664… | YTlhRdb7T664… | 매번 동일 → 대조에 사용 가능. 키 A가 없으면 계산 불가 |
| HMAC (키 B) | g0V_2F0ppzF3… | — | 키가 다르면, 같은 번호라도 완전히 다른 값 |
| AES-GCM | iIAHqcmW…ezwG6r… | ZNxyOHbM…4AQM2s… | 매번 다름 → 같은 사람인지도 알 수 없음. 키만 있으면 복원 가능 |
참고로, 바로 옆 번호인 804722의 SHA-256은 65113e69508a…입니다. 글자 하나 차이로도 전혀 닮지 않은 값이 됩니다. 이전 절에서 '표기를 통일한 후 변환하는 것'이 중요한 이유가 바로 이것입니다.
본인에게 자신의 신청 내용을 보여주고 싶은 상황이 있었습니다. 로그인 시스템은 없고, 본인이 알고 있는 것은 생년월일 정도뿐입니다.
생년월일을 기반으로 키를 만들 경우, 후보는 36,500가지밖에 없습니다. 그래서 두 가지를 추가했습니다.
- 계산을 일부러 느리하게 하기: PBKDF2와 같이 반복 횟수를 지정할 수 있는 방식을 사용합니다. 60만 번 반복하면 1회 계산에 약 72ms (M1에서 실측)가 걸립니다. 36,500가지 후보를 모두 시도하면 약 44분이 걸립니다.
- 서버만 가진 비밀 값 혼합: 시트만 손에 넣은 상대방은 애초에 계산을 시작할 수 없습니다.
이 방식이라면 조회 시 '그 생년월일로 이름을 복원했는지'를 통해 본인 확인이 가능합니다. 생년월일 자체를 저장할 필요가 없어집니다.
지금까지의 대책은 키와 데이터가 다른 장소에 분리되어 있다는 전제가 깔려 있습니다. 제 구성에서는 이름을 복원하는 재료를 3군데로 나누어 보관하고 있습니다.
| 재료 | 보관 위치 |
|---|---|
| 암호문・대조용 값 | 스프레드시트 |
| ... | |
| 시트를 복사하거나 실수로 공유하더라도, 외부 도구(AI 연결 등)가 시트를 읽어도 보이는 것은 읽을 수 없는 문자열뿐입니다. |
솔직하게 말씀드립니다.
-
시트와 '비밀 값'이 모두 유출되면, 생년월일 전체 대입은 성립합니다. 느리게 했기 때문에 1인당 약 44분이 걸리지만, 시간을 벌고 있을 뿐 막지는 못했습니다. 이 설계가 강력한 것은 '둘 중 하나만 유출된 경우'입니다. - 화면에 표시하는 순간, 그 화면을 보는 사람에게는 읽힙니다. 암호화는 저장 장소의 방어이지, 화면의 방어가 아닙니다. - 암호 부품은 직접 만든 것이 아닙니다. HMAC・AES-GCM・PBKDF2는 GAS와 브라우저 표준 기능만으로 만들었습니다. 직접 만든 암호만큼 위험한 것은 없습니다.
-
자릿수가 적은 개인 정보는, 해싱해도 모두 시도하면 복원 가능합니다 (6자리 기준 0.4초) - 대조용은 HMAC, 표시용은 매번 다른 암호문. 하나의 항목에 두 가지 값을 갖게 합니다. - 생년월일 같은 약한 비밀에서 키를 만들 경우, 느리게 하기 + 서버의 비밀을 혼합합니다. - 가장 효과적인 것은 암호의 강도보다, 키를 어디에·누구에게 맡기느냐입니다. - 지킬 수 없는 범위(둘 다 유출된 경우)도 말씀드립니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기