본문으로 건너뛰기

키 저장과 인증

BitTool Signer는 각 지갑의 복구 문구(설정된 경우 BIP39 패스프레이즈 포함)를 지갑별 암호화 파일에 저장합니다. 파일을 열려면 서로 독립적인 키 두 개가 필요합니다. 하나는 지갑 비밀번호에서 파생한 키이고, 다른 하나는 iOS가 해당 기기에만 보관하는 키입니다.

Android

이 페이지는 iPhone 앱을 기준으로 설명합니다. Android용 BitTool Signer는 같은 두 계층을 Android 자체의 키 저장소로 사용합니다. Android를 참고하세요.

볼트​

각 지갑은 앱 전용 디렉터리 Library/Application Support/BitToolSigner/ 안의 Vaults/<UUID>.bsvault 파일입니다. 생성 과정은 다음과 같습니다.

  1. SecRandomCopyBytes로 새 32바이트 솔트를 생성합니다.
  2. 256비트 비밀번호 키를 파생합니다: PBKDF2-HMAC-SHA256, 600,000회 반복(CommonCrypto).
  3. 비밀 정보(문구 + 패스프레이즈, JSON 형식)를 비밀번호 키로 AES-256-GCM 암호화합니다: 새 96비트 nonce, 128비트 태그.
  4. 그 암호문을 무작위 256비트 기기 키로 AES-256-GCM 암호화를 한 번 더 수행합니다.
  5. 기기 키를 kSecAttrAccessibleWhenPasscodeSetThisDeviceOnly 속성으로 iCloud 동기화 없이 Keychain에 저장합니다. iPhone에 암호가 설정되어 있지 않으면 지갑 생성이 실패하며, 더 약한 대체 방식은 없습니다.
  6. 완전한 파일 보호, 소유자 전용 권한, 백업 제외를 적용해 파일을 원자적으로 기록합니다.
vault file
├── header (not secret, but authenticated)
│ format version 2 · KDF parameters · salt · wallet ID · name · created at
└── AES-GCM(device key,
AES-GCM(PBKDF2-SHA256(password, salt, 600 000),
{ mnemonic, passphrase }))

두 AES-GCM 계층 모두 저장된 헤더 바이트를 도메인 레이블(com.bittool.signer.vault.v2/<layer>) 접두사와 함께 연관 데이터(associated data)로 인증합니다. 헤더의 바이트를 하나라도 수정하거나, 다시 인코딩하거나, 순서를 바꾸면 복호화가 실패합니다.

지갑 비밀번호는 로컬 볼트를 암호화합니다. BIP39 패스프레이즈가 아니며 주소에 아무 영향도 주지 않습니다. 규칙: 8–64자, 대문자, 소문자, 숫자, 기호 중 세 종류 이상, 제어 문자 불가. 입력한 그대로 사용합니다(공백 제거나 정규화 없음).

볼트는 백업이 아닙니다

기기 키는 iPhone 밖으로 나가지 않고 백업에서도 제외되므로, 볼트 파일을 복사해도 다른 곳에서는 열 수 없습니다. iPhone을 잃어버리거나, 앱을 삭제하거나, 기기 암호를 해제하면 볼트를 읽을 수 없게 될 수 있습니다. 종이에 적어 둔 복구 문구(와 패스프레이즈)만이 유일한 백업입니다.

왜 Argon2id가 아니라 PBKDF2인가요?​

Argon2id는 메모리 하드(memory-hard) 함수이므로 더 강력한 비밀번호 KDF가 될 수 있습니다. 이번 버전은 네이티브 암호화 의존성을 추가하지 않기 위해 CommonCrypto의 PBKDF2를 사용합니다(HMAC-SHA256 기준 현재 OWASP 권장값인 600,000회 반복). KDF 파라미터는 버전이 있는 헤더에 들어 있으므로, 향후 마이그레이션 시 새 파일 구조가 필요하지 않습니다.

Opus 5 보안 검토는 이 선택을 약점이 아니라 정보 등급(BTS-OP5-03)으로 평가했습니다. 기기 키로 암호화한 두 번째 계층이 있으므로, 볼트 파일만 복사한 사람은 비밀번호 탐색을 아예 시작할 수 없기 때문입니다. 이 검토는 기기 키까지 침해되는 경우에 대비한 심층 방어(defence in depth)로서, 향후 볼트 형식에 Argon2id를 사용할 것을 제안합니다.

비밀번호 시도​

실패한 비밀번호 시도는 지갑별로 암호화되고 인증된 .attempts 파일에 기록됩니다. 횟수는 느린 키 파생을 시작하기 전에 저장되므로, 앱을 강제 종료해도 초기화되지 않습니다.

실패 횟수다음 시도까지 대기 시간
1–4없음
52초
64초
nmin(2n−4초, 5분)

올바른 비밀번호를 입력하면 카운터가 초기화됩니다. 앱은 잘못된 비밀번호 때문에 지갑을 삭제하지 않습니다. 이것은 하드웨어 카운터가 아니라 애플리케이션 수준의 통제이므로, 앱 파일을 이전 상태로 되돌릴 수 있는 사람은 카운터를 초기화할 수 있습니다. 실제 암호학적 보호는 KDF가 담당합니다.

비밀번호 없이 쓰는 공개 데이터​

주소, 페어링 QR 코드, 활동 내역은 공개 키만 필요하므로 비밀번호 없이 볼 수 있습니다. 지갑을 생성하거나 가져올 때 엔진은 공개 인덱스를 한 번 파생해 볼트 밖에 저장합니다.

  • Bitcoin 계정 노드 6개(BIP84, BIP86, BIP49, BIP44, BIP48 native 및 nested 멀티시그)
  • EVM과 TRON 계정 노드, 그리고 TRON 주소 0–99
  • Solana 공개 키 3개(파생 경로 참고)

공개 인덱스 파일에는 별도의 무작위 Keychain 키로 계산한 HMAC-SHA256이 붙어 있습니다. 검증에 실패하면(누군가 파일을 수정했거나 다른 지갑의 파일인 경우) 앱은 파일이 없는 것으로 간주하고, 다시 만들기 위해 비밀번호를 한 번 요청합니다. 따라서 앱 파일에 쓰기 권한이 있는 공격자라도 자신의 주소로 바꿔치기할 수 없습니다.

승인​

  • "잠금 해제" 상태나 전역 세션은 없습니다. 모든 서명 작업마다 해당 지갑의 비밀번호를 요청하며, 복구 문구 표시, 비밀번호 변경, 지갑 삭제도 마찬가지입니다.
  • App Lock(선택 사항, Face ID)은 앱 접근만 보호합니다. 서명을 승인하는 데는 절대 쓰이지 않습니다. App Lock을 켜면 앱은 콜드 실행 시, 그리고 백그라운드에서 120초가 지나면 잠기며, 이 시간은 iPhone이 잠자기 상태일 때도 계속 흐르는 시계로 측정합니다.
  • 앱이 백그라운드로 전환되면 입력된 비밀번호, 위험 확인, 표시 중인 복구 문구를 즉시 폐기합니다. 앱 전환기와 화면 녹화 중에는 개인정보 보호 화면이 내용을 가립니다.

메모리 관리​

BIP39 시드는 네이티브 코드에서 계산하며, 임시 버퍼는 memset_s로 덮어씁니다. 비밀 정보나 신뢰할 수 없는 입력을 다루는 각 작업은 새 JavaScriptCore 컨텍스트에서 실행되고, 작업이 끝나면 폐기됩니다. 이렇게 하면 비밀 정보가 메모리에 머무는 시간이 줄어들지만, Swift와 JavaScript 문자열 모두 지워진다고 보장할 수 없으므로 앱은 완전한 제로화(zeroization)를 주장하지 않습니다.

Android​

금고는 같은 두 계층과 같은 인증 헤더를 사용합니다. 다른 점은 기기 키가 있는 곳과 플랫폼이 앱을 보호하는 방식입니다.

  • 비밀번호 계층. PBKDF2-HMAC-SHA256(600,000회)을 네이티브 코드로 계산하고(프로세서에 ARMv8 SHA-256 명령이 있으면 사용), 이어서 암호화할 때마다 새 12바이트 논스와 새 32바이트 솔트로 AES-256-GCM을 적용합니다.
  • 기기 계층. 지갑마다 Android Keystore 안에서 생성되어 절대 내보낼 수 없는 AES-GCM 키입니다(기기에 있으면 StrongBox). 금고는 화면 잠금을 요구합니다.
  • 파일. 앱 전용 저장 공간에 원자적으로 기록되며, 클라우드 백업과 기기 간 이전에서 제외됩니다.
  • 비밀번호 시도. iPhone과 마찬가지로 느린 키 유도 전에 지갑별로 집계되며, 카운터가 손상되면 모든 시도를 거부합니다.
  • 승인. 서명할 때마다 지갑 비밀번호가 필요합니다. 앱 잠금(선택, 지문 또는 얼굴)은 서명을 승인하지 않습니다. 모든 창은 스크린샷, 화면 녹화, 최근 앱 화면에 나타나지 않고, Android 12 이상에서는 다른 앱의 오버레이를 숨기며, 다른 창이 가리고 있는 동안 서명 버튼과 위험 확인은 탭을 받지 않습니다.
  • 메모리. 비밀 정보는 사용 후 닫히고 지워지는 보관 객체에 담기며, 작업이 취소된 경우에도 마찬가지입니다. 비밀 정보를 다루는 모든 작업은 새 QuickJS 런타임에서 실행됩니다. 복구 구문은 앱 자체 키보드로 입력되어 어떤 입력기도 볼 수 없습니다. JVM은 모든 사본이 지워진다고 보장할 수 없으므로, 여기서도 앱은 완전한 제로화를 주장하지 않습니다.

Opus 5.5 검토는 이 계층에 관해 낮음 등급 발견 사항 두 건을 제시합니다. Android 8~11에서 Keystore 오류가 나면 공개 지갑 데이터를 인증하는 키가 다시 만들어질 수 있다는 점(BTS-13)과, 보조 키 두 개가 화면 잠금 여부를 확인하지 않는다는 점(BTS-14)입니다. 보안 검토를 참고하세요.