아빠가 정한 대비책
다른 브라우져들처럼 자체 비밀번호 저장소를 만들어줘. 한번 기억한 id/pass 쌍은 다시 입력 안 해도 되게. 1password 연동 안 되면 이걸로 fallback.
2026-10-04, 1Password를 보류한 다음 날 나온 말이야. 핵심 단어는 fallback이야. 1Password가 언젠가 붙든 말든, 매일 겪는 번거로움은 없어져야 했어.
목록과 비밀
저장소는 로그인마다 둘로 쪼개. Voyager 폴더의 logins.json이 목록이야. 로그인마다 id, 출처, 사용자 이름, HTTP 인증용 영역, 날짜 둘이 적히고, 아빠가 묻지 말라고 한 사이트들도 여기 있어. 비밀번호는 절대 거기 없어. 비밀번호는 키체인으로 가. 로그인 하나에 일반 비밀번호 항목 하나씩, com.cwk.voyager.logins 서비스 아래 로그인의 무작위 UUID를 계정 삼아 들어가. 읽히는 건 Mac이 잠금 해제돼 있을 때뿐이야. 잠긴 키체인은 따로 오류를 내서 "지금은 못 읽어"가 "저장한 적 없어"로 둔갑하지 않아.
보관소는 구현이 둘인 프로토콜이야. 설치된 앱에선 키체인, 테스트와 검증 빌드에선 메모리. 그래서 어떤 확인도 아빠 키체인에 흔적을 안 남겨. theIndexOnDiskHoldsNoPassword 테스트는 파일을 다시 읽어서 직접 봐. 저장은 일부러 순서를 지켜(첫째 코드 블록). 보관소가 먼저고 목록은 나중이야. 키체인이 거절하면 행이 안 써지니까, 목록엔 채울 수 없는 로그인이 절대 안 올라가.
로그인을 지켜보는 건 딱 한 번
작은 페이지 스크립트 loginWatcher가 모든 페이지의 메인 프레임에서 돌아. 도는 곳은 Voyager 자기 콘텐츠 월드라서 페이지 스크립트는 못 닿아. 이 스크립트는 부모한테 두 가지를 알려. 하나는 페이지에 로그인 폼이 있는지야. 이건 바뀔 때마다 한 번 알려. 다른 하나는 아빠가 폼을 보내는 순간(폼 제출, 비밀번호 칸에서 Enter, 버튼 클릭)의 이름과 비밀번호야. 두 단계 로그인이면 마지막으로 친 이름 같은 칸을 10분 동안 기억해. 페이지 안에 낀 다른 사이트 프레임은 그 페이지 로그인이 아니니까 메인 프레임만 쳐.
Voyager에서 비밀번호 칸 값을 읽는 스크립트는 이거 하나고, 그것도 제출하는 순간뿐이야. 피파가 페이지를 읽는 스크립트는 비밀번호 칸을 라벨로만 부르고 안의 값은 절대 안 읽어. pippasReadingStillLeavesThePasswordOut이 그걸 지켜. 집 규칙은 다음에 이 코드를 만질 사람한테 하나 더 적어 둬. login-submit 메시지는 절대 로그로 남기지 마.
제안과 채우기
제출이 오면 뭘 제안할지는 부모가 정해. 묻지 말라고 한 사이트거나 같은 이름이 같은 비밀번호로 이미 있으면 아무것도 안 해. 아는 이름에 새 비밀번호면 Update(업데이트)를 띄워. 나머지는 Save(저장)이고, 옆에 Never for This Site(이 사이트는 묻지 않기)가 붙어. 시크릿 창은 제안도 저장도 안 하고, Settings에서 제안을 끌 수도 있어.
채우기도 부모가 정해. 페이지가 혼자 채우는 일은 없어. 로그인 폼이 뜨고, 그 사이트에 저장된 로그인이 딱 하나고, 이 주소에서 아직 채운 적이 없으면 Voyager가 채워. 여러 개면 주소창에 열쇠가 뜨고 아빠가 골라. 같은 사이트란 스킴과 포트가 같고, 호스트가 앞의 www.만 빼고 같다는 뜻이야. 그래서 https로 저장한 로그인은 평문 http 페이지엔 절대 안 나가. HTTP 인증은 영역별로 기억해서 시트 없이 답하는데, 서버가 한 번 거절했으면 다시 물어. Safari나 Chrome처럼, Settings → Passwords도 Touch ID나 Mac 비밀번호를 거친 뒤에야 비밀번호를 보여 줘.
OS의 동의를 받아 딱 한 번 가져오기
아빠의 다음 질문은 Comet에 이미 저장된 비밀번호도 가져올 수 있냐는 거였어. 돼, 한 번만. 대신 고리마다 주인이 있는 동의 사슬을 거쳐야 해.
- 시작은 아빠가 Settings나 Bookmarks 메뉴에서 해. 검증 실행이면 뭘 읽기도 전에 거절해.
- Voyager는 일어날 일을 일어나기 전에 말해(둘째 코드 블록).
- 다른 브라우저의 저장 비밀번호를 지키는 열쇠는 그 브라우저 자기 키체인 항목이야. 그러니 Voyager가 그걸 써도 되냐고 아빠한테 묻는 건 Voyager가 아니라 macOS야.
- Voyager는 메인 스레드 밖에서 그 프로필 로그인 데이터베이스의 사본을 한 번 읽어. 비밀번호는 하나하나 메모리에서 풀리고, 같은 저장소를 거쳐 키체인으로만 가.
- Voyager에 같은 사이트, 같은 이름의 로그인이 이미 있으면 그대로 두고, 묻지 말라고 한 사이트는 저장 안 해. 다른 브라우저 쪽은 하나도 안 바뀌어. 결과는 숫자로 보고해. 새로 들어온 것, 이미 있던 것, 저장 안 하는 사이트, 못 푼 것, 못 넣은 것.
가져오기는 북마크와 방문 기록 파일 말고 로그인 데이터베이스 둘(Login Data, Login Data For Account)을 열어. 파일 머리 주석엔 아직 '전부 세 파일'이라고 적혀 있긴 해. 쿠키, 자동 완성, 환경 설정은 절대 안 열고, 네트워크에도 안 닿아. 이 레슨은 동의의 모양만 보여 주고 암호 처리 부분은 뺐어. 테스트 벡터는 이 코드가 아니라 Python hashlib과 openssl에서 가져와서, 코드가 자기 답안을 자기가 채점하지 않아.
패스키, 그리고 아빠가 거절한 자격
2026-10-04에 재 봤어. Apple의 웹 브라우저 자격이 없어도 WebKit은 웹 뷰에 PublicKeyCredential을 내줘. 근데 isUserVerifyingPlatformAuthenticatorAvailable()가 false라서 어떤 페이지도 Touch ID 패스키를 못 받아. iPhone과 iPad에선 그 자격을 받으려면 공개 TestFlight 링크와 외부 베타 앱 심사를 거쳐야 해. 아빠 답은 이랬어.
아니, 근데 이거 다 디폴트 브라우져 자격 얻는데 필요한 깐깐한 심사 아니야? 디폴트로 안 쓰면 그만이잖아? 이걸 외부 베타 테스트 심사까지 받는다고? 그건 하면 안 돼.
그 대가는 지금도 사실로 적혀 있어. Voyager에선 cwkPippa 패스키 로그인 버튼이 안 보이고, 가족 PIN으로 들어가(그 PIN은 Solo Auth Quest가 가르쳐). 같은 결정의 배포 쪽 얘기는 트랙 9에 있어.