본문으로 건너뛰기

시드 생성과 난수

지갑의 강도는 그 지갑을 만든 난수의 품질에 달려 있습니다. BitTool Signer는 모든 난수를 운영 체제의 암호학적으로 안전한 난수 생성기에서 가져오며, JavaScript에 내장된 의사 난수 함수는 절대 사용하지 않습니다.

Android

이 페이지는 iPhone 앱을 기준으로 설명합니다. Android용 BitTool Signer가 난수를 얻는 곳은 Android에 정리되어 있습니다.

엔트로피 소스​

새 복구 문구는 Apple의 SecRandomCopyBytes(kSecRandomDefault, …)에서 얻은 무작위 바이트로 시작합니다. 이는 iOS가 하드웨어 엔트로피로 시드를 공급하는 시스템 CSPRNG입니다. 호출은 네이티브 Swift 코드에서 이루어집니다.

BitToolSigner/Core/VaultCrypto.swift
enum SecureRandom {
static func bytes(count: Int) throws -> Data {
guard (1...65_536).contains(count) else { throw VaultError.randomFailure }
var data = Data(count: count)
let status = data.withUnsafeMutableBytes { buffer in
guard let address = buffer.baseAddress else { return errSecAllocate }
return SecRandomCopyBytes(kSecRandomDefault, count, address)
}
guard status == errSecSuccess else { throw VaultError.randomFailure }
return data
}

static func entropyHex(wordCount: Int = 24) throws -> String {
guard [12, 15, 18, 21, 24].contains(wordCount) else { throw VaultError.invalidSecret }
return try bytes(count: wordCount / 3 * 4).map { String(format: "%02x", $0) }.joined()
}
}

생성기가 오류를 보고하면 지갑 생성은 오류와 함께 중단됩니다. 더 약한 소스로 대체하는 경로는 없습니다.

엔트로피에서 단어로​

앱은 24단어(기본값, 256비트 엔트로피) 또는 12단어(128비트)를 제공합니다. 바이트는 표준 BIP39 알고리즘으로 단어로 변환됩니다.

  1. 엔트로피를 준비합니다. 24단어는 32바이트, 12단어는 16바이트입니다.
  2. SHA-256(entropy)의 앞 entropy bits ÷ 32비트를 체크섬으로 덧붙입니다(24단어는 8비트, 12단어는 4비트).
  3. 결과를 11비트 단위로 나눕니다. 각 그룹은 2,048개 단어로 된 영어 단어 목록의 인덱스입니다.

변환은 내장 엔진(@scure/bip39)에서 실행되며, 엔진은 네이티브 코드로부터 엔트로피를 hex로 받습니다.

// engine/src/keys.js
export function mnemonic(entropy) {
const b = hexToBytes(entropy, 32);
check([16, 20, 24, 28, 32].includes(b.length), 'ENTROPY', 'Entropy must contain 128–256 bits in 32-bit steps.');
return { mnemonic: entropyToMnemonic(b, wordlist), wordCount: b.length * 3 / 4 };
}

새 지갑은 모든 지갑이 지원하는 영어 단어 목록을 항상 사용합니다. 나머지 아홉 개 BIP39 언어의 문구는 가져오기로 사용할 수 있습니다.

예를 들어 0으로 채운 32바이트(절대 사용하지 마세요)는 다음과 같습니다.

엔트로피 0x00…00 (32바이트) → 24단어
abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art

문구에서 시드로​

64바이트 BIP39 시드는 JavaScript가 아니라 CommonCrypto를 사용해 네이티브로 계산합니다. iOS에서 엔진은 JIT 없이 실행되므로, 그 환경에서 PBKDF2를 돌리면 1초 가까이 걸리기 때문입니다.

seed = PBKDF2-HMAC-SHA512(
password = NFKD(phrase words joined by single spaces),
salt = NFKD("mnemonic" + passphrase),
iterations = 2048,
length = 64 bytes)

엔진은 파생과 서명에 필요한 64바이트 시드만(hex로) 받으며, 문구 자체는 절대 받지 않습니다. 네이티브 버퍼는 사용 후 memset_s로 덮어씁니다. BIP39 패스프레이즈 페이지에서 패스프레이즈가 있을 때와 없을 때의 테스트 문구 시드를 확인할 수 있습니다.

그 밖의 모든 난수 값​

값소스
복구 문구 엔트로피SecRandomCopyBytes, 16 또는 32바이트
볼트 솔트(PBKDF2)SecRandomCopyBytes, 32바이트, 암호화할 때마다 새로 생성
기기 키SecRandomCopyBytes, 32바이트, 지갑마다 하나
AES-GCM nonceCryptoKit, 96비트, 암호화할 때마다 새로 생성
엔진 내부의 crypto.getRandomValues()엔진 로드 전에 SecRandomCopyBytes로 연결
엔진 내부의 Math.random()SecRandomCopyBytes 기반 함수로 교체(일부 라이브러리가 내부 ID 생성에 호출)
Taproot(BIP340) 보조 난수서명마다 새 무작위 32바이트
ECDSA nonce(Bitcoin, EVM, TRON)결정적, RFC 6979
Ed25519(Solana)설계상 결정적(RFC 8032)

엔진의 crypto 객체는 어떤 라이브러리 코드보다 먼저 설치되고 동결(freeze)되므로, 의존성이 이를 바꿔치기할 수 없습니다.

Installed by SigningEngine.swift before the bundle is evaluated
Object.defineProperty(globalThis, 'crypto', {value: Object.freeze({
getRandomValues(array) { /* fills the array from native SecRandomCopyBytes */ }
}), writable: false, configurable: false});
Object.defineProperty(Math, 'random', {value: /* also SecRandomCopyBytes */, writable: false, configurable: false});
주사위를 쓰지 않는 이유

일부 서명 기기는 사용자가 직접 만든 엔트로피(주사위나 동전 던지기)를 섞을 수 있게 합니다. BitTool Signer는 그렇게 하지 않습니다. 사람이 만든 엔트로피는 잘못 만들기 쉽고, iOS의 난수 생성기는 충분히 검증되었기 때문입니다. 직접 만든 엔트로피를 쓰고 싶은 사용자는 다른 곳에서 문구를 생성한 뒤 가져오면 됩니다.

Android​

값출처
복구 구문 엔트로피, 금고 솔트, AES-GCM 논스java.security.SecureRandom: Linux 커널이 시드를 주는 BoringSSL 생성기(Conscrypt 경유). 오류가 나면 작업이 중단되며, 대체 출처는 없습니다.
기기 키Android Keystore 안에서 생성되며 절대 내보내지 않음(기기에 있으면 StrongBox)
엔진 안의 crypto.getRandomValues()와 Math.random()엔진이 로드되기 전에 설치되고 고정되며, 커널이 시드를 주는 Android C 라이브러리의 arc4random_buf를 사용
서명iPhone과 같은 엔진: ECDSA는 RFC 6979, Ed25519는 RFC 8032, Taproot는 새로운 보조 난수

구문에서 시드를 계산하는 과정도 같은 BIP39 PBKDF2-HMAC-SHA512이며, 앱이 엔진 밖에서 계산하고 엔진은 역시 시드만 받습니다.

보안 검토 결과​

버전 1.0.0에 대한 두 보안 검토 모두 이 경로를 추적해 다음을 확인했습니다. 엔트로피는 SecRandomCopyBytes에서만 나오고, 오류 상태가 반환되면 지갑 생성이 중단되며, 시간 기반 대체 경로나 그 밖의 약한 대체 경로는 없고, 엔진의 crypto 객체와 Math.random은 어떤 라이브러리 코드보다 먼저 설치되어 교체할 수 없습니다. GPT-6 Astra 보고서는 타당한 단서를 덧붙입니다. 이것은 iOS와 기기를 신뢰할 수 있다는 가정 아래에서 암호학적으로 안전한 생성기이며, 인증된 물리적 난수 스트림은 아닙니다. 또한 다른 곳에서 가져온 문구의 무작위성은 그 문구를 생성한 지갑에 달려 있습니다.

테스트 방식​

네이티브 테스트 모음은 엔트로피 함수의 경계값, Python으로 독립적으로 생성한 PBKDF2 벡터, 그리고 실제 JavaScriptCore 브리지를 거친 공개 BIP39 벡터를 검사합니다. 엔진 테스트는 문구에서 파생한 시드와 네이티브 시드에서 같은 키가 나오는지 확인합니다.