주민번호 복호화를 붙인다 — 이 DB 는 실제로 암호화되어 있었다
문서(docs/RESNUM-DECRYPT.md)의 1→2→3 을 실행. 태그 7종 → 11종. ## 이 DB 가 암호화 병원이다 진단이 읽은 설정: 전체=Y 주민번호=Y. 앞 커밋에서 PAT_주민번호 를 뺀 판단이 맞았다 — PatResNum 을 그대로 썼으면 <b>암호문이 종이에 찍혔다</b>. ## 앱은 키를 갖지 않는다 복호화는 오라클 패키지가 한다. SELECT SUBSTR(:e,1,7) || CRYPTO_AES256.DEC_AES(SUBSTR(:e,8,256)) FROM DUAL 바인드 변수이고 접근 통제는 DB 권한이다. SCHILDREN 갈래(UDF_GETDECRESNUM_damo)도 함께 옮겼다. 앞 7자리가 평문이라 SUBSTR(...,1,7) 을 이어 붙이는 것을 빠뜨리면 앞자리가 사라진다. ## 설정을 못 읽으면 값을 만들지 않는다 "암호화 아님"으로 가정하면 PatResNum 을 평문처럼 써서 암호문을 찍는다. PatientIdentity 는 예외 시 빈 문자열을 돌려주고 사유만 로그에 남긴다 — 값은 남기지 않는다. 주민번호를 만드는 자리를 한 곳으로 좁혀 뒀다(만드는 경로가 여럿이면 어디서 새는지 추적할 수 없다). ## 순수 계산은 표로 고정했다 (테스트 13건) 보정 ①(기관기호, SRCH·JINJU): 앞 6자리를 PatBthDay 로 갈아 끼운다. 원본은 If Not DateValidCheck(Substring(1,6)) 으로 감싸 있지만 <b>그 조건은 항상 참이다</b> — DateValidCheck 는 길이가 8·12·14·17 이 아니면 무조건 False 인데 6자리를 넘긴다. 여기서 "고쳐서" 진짜 날짜 검사를 넣으면 레거시와 다른 번호가 나온다(이 DB 가 SRCH 다). 이식은 동작을 옮기는 일이지 바로잡는 일이 아니다 — 그 사실을 테스트로 못 박았다. 보정 ②(외국인): 7번째 자리 0/9 를 PatBthDay 세기로 6/8·5/7 로. 국적코드가 있어야 걸린다. 성별: 7번째 자리 mod 2. PAT_성별 은 CoiCalCod 가 "24" 로 시작하면 PatSexTyp 을 먼저 쓴다. ## 추측하면 걸린다 M_DtsMst 컬럼을 DtsTabCod·DtlCod 로 추측해 ORA-00904 를 맞았다. 실제는 DtsTblCod·DtsDtlCod·DtsCod 다(dtCommonLib.vb:1136). ## 확인한 것과 못 한 것을 갈라 적는다 진단은 HIS 사용자 문맥 없이 도므로 병원코드가 비고, SRCH 기관기호 보정 갈래는 이 경로로 확인되지 않는다. 통과로 세지 않고 [미확인] 줄로 적는다 — 그 갈래는 순수 함수 테스트가 덮는다. 진단 리포트에 <b>값을 쓰지 않는다</b> — 길이와 "전부 숫자인가"만 적는다. 리포트는 파일로 남는다. ## 아직 안 한 것 나이(AgeCheck)는 병원별 규칙과 의료급여 전산관리번호 규약이 붙어 별도 작업이다. 개인정보 접근 로그(C_PdrInf)에 쓸지는 운영 DB 쓰기라 승인 전까지 하지 않는다. ## 게이트 - dotnet test 336/336 (주민번호 판정 13건 신규) - --db-patient ①~㉑ 전건 통과 (⑲ 설정 · ⑳ 13자리 · ㉑ 성별) - --edit-smoke 실패 0 (이름 검사가 파생 태그 4종까지 본다) - --dialog-shots FAIL 0 - --db-render P062 md5 8d683835f5d81e7bb41c79071d6bf954 불변 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
34c53e3d69
commit
4bdd583adb
@@ -2106,6 +2106,43 @@ public static class DbSmoke
|
||||
$"Ok={blind.Ok} {blind.Rows.Count}행 — 치환이 실제로 기여하지 않았다는 뜻이다");
|
||||
lines.Add($" 대조군 SQL: {blind.Sql.Replace("\r", " ").Replace("\n", " ")}");
|
||||
lines.Add($" 치환 못 한 변수 {blind.Unknown.Count}개");
|
||||
|
||||
// ── 주민번호 복호화 ──
|
||||
// 값은 <b>절대 리포트에 쓰지 않는다</b> — 길이와 형식만 적는다.
|
||||
// 이 앱이 다루는 것 중 가장 민감한 값이고, 리포트는 파일로 남는다.
|
||||
var personal = new PersonalDataStore(conn);
|
||||
var flags = personal.Flags();
|
||||
var hospital = Services.UserSession.Current.HspCod;
|
||||
lines.Add($" 암호화 설정: 전체={(flags.Enabled ? "Y" : "N")}"
|
||||
+ $" 주민번호={(flags.ResidentNumber ? "Y" : "N")}"
|
||||
+ $" / 병원코드='{hospital}'");
|
||||
Check("⑲ 암호화 설정을 예외 없이 읽는다", true);
|
||||
// 병원코드가 비면 SRCH·JINJU 기관기호 보정이 걸리지 않는다 —
|
||||
// 그 갈래는 이 진단으로 확인되지 않는다. 대신 <b>복호화 자체</b>는 확인한다.
|
||||
// 그것이 이 단계에서 실패하면 나머지가 전부 무의미하고, DB 권한·패키지 문제라
|
||||
// 코드로는 알 수 없다. 확인한 것과 못 한 것을 갈라서 적는다.
|
||||
if (hospital.Length == 0)
|
||||
{
|
||||
lines.Add(" [미확인] 병원코드가 없어 SRCH·JINJU 기관기호 보정은"
|
||||
+ " 이 진단으로 확인되지 않았다(순수 함수 테스트가 그 갈래를 덮는다)");
|
||||
}
|
||||
{
|
||||
var raw = flags.ResidentNumberEncrypted
|
||||
? personal.Decrypt(PatientContext.Value(ctx.PatInf, "PatResEnc"), hospital)
|
||||
: PatientContext.Value(ctx.PatInf, "PatResNum");
|
||||
var normalized = SheetMe.Core.Catalog.ResidentNumber.Normalize(
|
||||
raw,
|
||||
PatientContext.Value(ctx.PatInf, "PatBthDay"),
|
||||
PatientContext.Value(ctx.PatInf, "PatNatCod"),
|
||||
hospital);
|
||||
var sex = SheetMe.Core.Catalog.ResidentNumber.SexOf(normalized);
|
||||
lines.Add($" 주민번호: 원본 {raw.Length}자 → 보정 {normalized.Length}자,"
|
||||
+ $" 전부 숫자={normalized.All(char.IsAsciiDigit)} (값은 기록하지 않는다)");
|
||||
Check("⑳ 주민번호가 13자리 숫자로 만들어진다",
|
||||
normalized.Length == 13 && normalized.All(char.IsAsciiDigit),
|
||||
$"{normalized.Length}자 — 복호화 실패나 컬럼 부재를 의심할 것");
|
||||
Check("㉑ 그 번호에서 성별이 나온다", sex is "M" or "F", $"'{sex}'");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user