서로 믿어야 하는 프로그램 둘
Chromium 브라우저에서 1Password는 프로그램 둘이야. 브라우저 안의 확장, 그리고 옆에 있는 데스크톱 앱. 둘은 네이티브 메시징으로 만나고, 데스크톱 앱의 헬퍼는 뭘 답하기 전에 브라우저부터 검증해. 모르는 브라우저를 위해선 데스크톱 앱에 Add browser가 있어. 아빠가 1Password를 쓰니까, 벽돌 하나 쌓기 전에 돌린 Voyager 엔진 게이트의 첫 케이스 W1이 바로 이거였어.
이 레슨은 그 얘기의 브라우저 쪽을 디버깅 사례로 풀어. 이야기는 일이 실제로 멈춘 자리에서 끝나. 그리고 그 자리는 고친 자리가 아니야.
통과한 것, 벽 하나씩
| 벽 | 막고 있던 것 | 치운 것 |
|---|---|---|
| 없는 Chrome 이름 | 백그라운드가 WebKit에 없는 첫 이름에서 멈췄어 | 호환 레이어(레슨 2) |
| 서명 | 헬퍼가 가족 서명 빌드의 서명을 못 읽었어. 1Password 코어는 Apple 체인 아래 번들 id와 팀 id가 든 hardened runtime 서명을 봐 | 유료 팀의 Apple Development 아이덴티티와 hardened runtime으로 서명한 설치 번들. 아빠가 Add browser로 Voyager를 넣었고, 1Password가 연결된 브라우저로 올렸어. 그 결과는 트랙 2의 게이트 레슨에, 서명 자체는 트랙 9에 있어 |
| 프로세스 그룹 | 브리지가 띄운 호스트가 전부 1분마다 거절당했어 | 브라우저 자기 그룹 안에서 posix_spawn(레슨 3) |
| 직접 부모 | 중간에 낀 중계가 둘째 규칙에 걸렸어 | 중간 없이 앱 자신이 부모야(레슨 3) |
| 확장 페이지 | chrome-extension:// 주소가 버려지고, 스킴이 취소되고, 컨텍스트 고유 설정 없인 페이지가 거절됐어 | pageURL(for:)와 그 설정으로 만든 페이지(레슨 3) |
| 프래그먼트 | 확장의 앱 페이지가 깜깜한 탭으로 열렸어 | 프래그먼트를 지키고 회귀 테스트로 묶었어(아래) |
넷째 줄을 치우고 나자 헬퍼 로그엔 Voyager를 검증한다는 줄과 파트너 승인을 기다린다는 줄이 찍혔어. 헬퍼가 기다리는 건, 확장 자기 백그라운드가 앱 페이지의 요청을 받고 나서 보내는 메시지야. 그래서 마지막 두 줄이 중요했지.
깜깜한 페이지와 프래그먼트
앱 페이지는 처음에 "1Password"라는 제목의 깜깜한 탭으로 떴어. 초기 기록은 그걸 확장 프런트엔드 앱이 마운트에 실패한 걸로 읽고 프레임워크 이름까지 붙였어. 나중에 설치된 패키지를 들여다보니 둘 다 틀렸어. 앱 진입점은 해시 라우터를 쓰는 React야. /엔 라우트가 없고, 환영 라우트는 #/page/welcome이야. Voyager의 pageURL(for:)가 프래그먼트를 빼고 페이지 URL을 만들고 있었으니 라우터는 갈 데가 없었던 거야. 그걸 확인하던 프로브도 프래그먼트 없이 맨 app/app.html을 열고 있었어. 계측기가 찾던 버그를 자기도 갖고 있었던 셈이지.
이제 매핑은 프래그먼트를 인코딩 그대로 옮겨(첫째 코드 블록). anExtensionPageKeepsItsFragmentAndRendersItsHashRoute가 그걸 붙잡아. 없는 프래그먼트, 빈 프래그먼트, 인코딩된 프래그먼트, 프래그먼트 안 라우트 자기 쿼리, 그리고 해시 라우트가 도착해야만 그려지는 픽스처 페이지까지(둘째 코드 블록). 집 규칙은 이걸 한 문장으로 남겨. 깜깜한 페이지 하나만으론 그 JavaScript가 마운트에 실패했다고 단정할 수 없다.
죽은 툴바 버튼도 같은 식으로 풀렸어. 페어링 전이면 1Password 백그라운드가 올라오고 몇 초 뒤 자기 팝업을 스스로 지워. 그러면 클릭은 백그라운드의 클릭 이벤트 몫이 되고, 그건 페어링이 끝나기 전엔 아무것도 안 보여 줘. 죽은 버튼은 페어링 안 된 상태가 겉으로 드러난 거였어.
통과 못 한 것
확장을 통한 인증은 끝내 안 붙었어. 팝업도, 로그인 채우기도 Voyager에서 돌아가는 걸 본 적이 없어. 그리고 페어링 안 된 확장은 1분마다 네이티브 포트를 새로 열어서, 기다리는 헬퍼가 Mac에 쌓여.
범위를 미리 정한 진단이 한 번 더 있었어. 공개 API만 쓰는 숨은 프로브에, 새로 격리한 컨트롤러와 데이터 저장소를 썼어. 데스크톱 브리지도, 진짜 탭도, 이미지 캡처도 안 썼고, 자격 증명이나 보관함이나 프로토콜 본문은 하나도 안 읽었어. 가설 하나는 지웠어. WebKit은 확장 컨텍스트마다 출처를 만들어 주는데 그게 프로세스마다 바뀌었어. 그래도 그 아래 저장한 데이터는 다음 프로세스에서 살아 있었어. 그러니 출처가 바뀐다는 것만으론 그 Mac에서 로그인 상태가 사라진 걸 설명 못 해. 공개 패키지는 빈 프로필에서 환영 라우트도 잘 그렸고. 둘 다 원인은 아니야. 기록은 자기 말로 이렇게 적어. 실제 근본 원인도, 정확한 수정 패치도 밝혀지지 않았다.
멈추는 것도 진단의 일부야
아빠가 정한 멈춤 조건이 거기서 일을 끝냈어. 이번 시도는 여기까지, 1Password는 맨 뒤로 보류, 가설 세우고 고치는 반복도 더는 안 해. Home, 계정, 시크릿 창, 피파는 1Password 없이 계속 가. 기록은 다음 시도에도 울타리를 쳐 둬. 다음 시도는 콜백, 액션 상태, 완료 메타데이터만 보거나, 아빠가 같이하는 확인만 할 수 있어. 그리고 거기 적힌 어떤 것에서도 새 설치, 재시작, 신뢰 부여, 자격 증명 사용 허락을 끌어내면 안 돼. 그 뒤로 1Password를 건드린 Voyager 커밋은 없어.
그래서 문서 둘이 사실 둘을 말해. 계획과 게이트는 1Password가 Voyager를 연결된 브라우저로 올렸다고 하고, 진단은 연동이 안 풀렸다고 해. 둘은 같은 일의 서로 다른 절반을 말하는 거야. 이 일에서 나온 결정 하나는 남았어. cwkPippa 페이지에서 1Password 콘텐츠 스크립트가 navigator.credentials를 가로채서, 페어링이 열려 있는 동안 패스키를 저장할 수 없었거든. 지금 모든 확장을 가족 호스트에서 떼어 놓는 이유가 그거야(트랙 6). 다음 날 아빠가 대비책을 정했고, 마지막 레슨이 그 얘기야.