시크릿과 암호화
dotweave가 내장 age 구현으로 시크릿 artifact를 암호화하는 방식과 keys.txt를 잃었을 때 벌어지는 일을 설명해요.
시크릿 artifact란
secret 모드로 추적하는 항목은 저장소에 평문으로 들어가지 않아요. push는 파일 내용을 암호화해서 profiles/<profile>/<repoPath>.dotweave.secret에 저장해요. 이 artifact는 ASCII armor로 감싸여 있어서 -----BEGIN AGE ENCRYPTED FILE----- 줄로 시작하고, git이 바이너리 취급 없이 전송하고 diff하고 병합할 수 있는 평범한 텍스트로 남아요.
dotweave는 age 구현을 직접 품고 있어요. packages/cli/lib/src/crypto/age/에 X25519 recipient stanza, ChaCha20-Poly1305 페이로드 암호화, bech32 키 문자열, ASCII armor 코덱이 모두 들어 있어요. 외부 프로세스를 실행하지 않으니 secret 모드를 쓰려고 age 바이너리를 따로 설치할 필요도 없어요.
암호화는 일반 파일의 바이트를 대상으로 해요. 심링크에는 암호화할 내용 대신 링크 대상만 담겨 있으니, secret 경로는 심링크가 아닌 일반 파일이어야 해요.
암호화되는 건 내용뿐이에요. 파일 이름과 디렉터리 이름, 저장소 경로는 저장소를 읽을 수 있는 사람에게 그대로 보여요. .ssh/config.dotweave.secret은 SSH 설정을 추적한다는 사실을 여전히 드러내요. 경로 자체가 민감하다면 repoPath를 재정의해서 아무것도 알려 주지 않는 이름으로 저장하세요.
identity와 recipient
일을 나눠 맡는 두 가지 키 자료가 있고, 서로 다른 자리에 일부러 떨어져 있어요.
identity는 개인 키예요. <dotweave-home>/keys.txt에 한 줄에 하나씩 AGE-SECRET-KEY-... 문자열로 들어가고, 빈 줄과 #으로 시작하는 주석 줄도 허용돼요. 이 파일은 sync 디렉터리 밖에 있어서 git이 볼 일이 없고, dotweave는 이 파일을 포함하는 경로를 추적하려고 하면 TARGET_OVERLAPS_IDENTITY로 거부해요.
recipient는 공개 키이고, manifest.jsonc의 age.recipients에 최소 한 개 이상 저장돼요. dotweave init은 identity에서 recipient를 도출해 그 자리에 기록하기 때문에, 새로 만든 설정도 바로 암호화하고 복호화할 수 있어요.
두 쪽은 비대칭으로 쓰여요. push는 manifest에 적힌 모든 recipient를 대상으로 암호화해요. pull은 keys.txt의 identity를 전부 읽어서 그중 맞는 키로 복호화하니, 쓸 수 있는 키 하나만 있으면 충분해요. 여러 recipient를 대상으로 암호화한 파일은 그중 어느 하나로도 독립적으로 열 수 있어서, 여러 기기가 개인 키를 공유하지 않고도 한 저장소를 함께 쓸 수 있어요.
push는 쓰기 전에 저장된 artifact를 복호화해서 평문을 비교해요. 내용이 바뀌지 않은 시크릿은 새 암호문을 만들지 않아서 git 히스토리가 깔끔하게 유지돼요. 이 비교는 recipient 목록을 보지 않으니, age.recipients만 고쳐도 내용이 그대로인 artifact는 다시 쓰이지 않아요.
다른 기기에 자체 키 쌍을 주려면 이렇게 하세요.
새 기기에서 저장소 인자 없이
dotweave init을 실행하세요.keys.txt가 생성되고, 도출된age1...recipient가 로컬manifest.jsonc에 기록돼요. 그 recipient 값을 복사하고,keys.txt사본은 dotweave 홈 디렉터리 밖에 따로 보관하세요.이미 복호화할 수 있는 기기에서 공유 저장소의
manifest.jsonc의age.recipients에 새 recipient를 추가하고, 다시 암호화하려는.dotweave.secretartifact를 삭제한 뒤dotweave push를 실행하고 결과를 git으로 커밋하세요.다시 새 기기로 돌아와 공유 저장소를 연결하면서 그 기기의 키를 넘기세요.
dotweave init <your-repo-url> --force --key-file ~/dotweave.agekey
파일을 시크릿으로 추적하기
secret은 항목별 동기화 모드라서, 경로를 추적할 때 지정해요.
dotweave track ~/.ssh/config --mode secret
dotweave push
같은 대상을 다시 추적하면 넘긴 필드만 갱신돼요. 그래서 dotweave track ~/.ssh/config --mode normal로 항목을 secret 모드에서 빼내도 repoPath, permission, profiles는 그대로 남아요. 중첩된 항목이 상위 디렉터리에서 모드를 물려받는 방식은 동기화 모드에서 설명해요.
두 번째 기기에서는 시크릿을 읽기 전에 identity가 먼저 도착해야 해요. 파일로 넘기세요.
dotweave init <your-repo-url> --key-file ~/dotweave.agekey
dotweave pull
대화형 터미널이라면 dotweave init <your-repo-url>을 실행하고 프롬프트에 키를 붙여 넣어도 돼요. 키가 없으면 recipient가 이미 등록된 저장소를 연결하는 작업은 INIT_AGE_IDENTITY_REQUIRED로 실패해요.
keys.txt 백업하기
dotweave는 keys.txt를 동기화하지 않아요. 저장소에 저장되지도 않고, 기기 사이로 옮겨 주는 명령도 없어요. 비밀번호 관리자나 암호화된 볼륨, 신뢰하는 경로의 scp처럼 직접 옮기는 방법을 쓰세요.
키 사본을 전부 잃으면 되돌릴 방법이 없어요. artifact는 git에 남아 있지만 dotweave도 age도 다시 열 수 없어요. 남은 건 identity가 살아 있는 기기의 평문뿐이에요. 그래서 백업은 dotweave가 관리하지 않는 곳에 두어야 해요.
dotweave init --force는 sync 디렉터리와 설정 파일과 함께 기존 identity 파일까지 삭제해요. keys.txt의 유일한 사본을 가진 기기에서 실행하면 키가 사라지고, 그 키만을 대상으로 암호화된 시크릿 artifact에도 접근할 수 없게 돼요. 먼저 키를 다른 곳에 저장하거나, 같은 명령에서 --key-file로 키를 다시 넘기세요.
dotweave doctor는 해석된 경로에 identity 파일이 있는지 확인해요. pull이 필요해지기 전에 keys.txt가 없거나 엉뚱한 곳에 있는 상황을 미리 잡아낼 수 있어요.
복호화가 실패할 때
시크릿 관련 오류는 대개 네 가지 중 하나이고, 메시지가 어느 쪽인지 알려 줘요.
Failed to decrypt a secret artifact.는 keys.txt의 identity가 artifact의 recipient 중 어느 것과도 맞지 않거나, 저장된 데이터가 손상되었다는 뜻이에요. 옮겨 온 키가 age.recipients에 등록된 recipient의 짝인지 확인하세요.
No age identities were found in the configured identity file.는 keys.txt가 있긴 하지만 빈 줄과 # 주석만 담고 있다는 뜻이에요. 개인 키를 추가하거나 dotweave init으로 새로 생성하세요.
Secret sync path is stored as a plain artifact in the repository.는 manifest는 secret이라고 하는데 저장된 파일이 암호화되어 있지 않다는 뜻이에요. artifact를 쓴 뒤에 모드를 바꾼 경우가 대부분이에요. 반대쪽인 Plain sync path is stored as a secret artifact in the repository.는 manifest는 normal인데 암호화된 artifact가 그대로 남아 있다는 뜻이에요. 둘 다 해당 항목을 읽을 수 있는 기기에서 push하거나 오래된 artifact를 지우면 정리돼요.
구현과 참고
age v1 구현은 packages/cli/lib/src/crypto/age/, 암호화와 복호화 진입점은 lib/src/lib/crypto.dart, identity 경로 해석은 lib/src/config/identity_file.dart에 있어요. artifact 비교와 쓰기는 lib/src/services/repo_artifacts.dart에서 일어나요.
age.recipients 필드를 나머지 설정과 함께 보려면 manifest.jsonc를, --key-file과 --force가 기존 설정에 무엇을 하는지는 dotweave init을 확인하세요.