WalletKit 암호화
채널 암호화
ChannelCrypto
페어링·세션 채널의 SHA-256, ChaCha20-Poly1305, HKDF-SHA256 연산을 제공합니다.
- Kotlin (Android · JVM)
- Swift (iOS)
public object ChannelCrypto {
public fun sha256(data: ByteArray): ByteArray
public fun seal(
key: ByteArray,
plaintext: ByteArray,
associatedData: ByteArray? = null,
): ByteArray
public fun open(
key: ByteArray,
ciphertext: ByteArray,
associatedData: ByteArray? = null,
): ByteArray
public fun hkdfSha256(
ikm: ByteArray,
salt: ByteArray,
info: ByteArray,
sizeBits: Int = 256,
): ByteArray
}
class ChannelCrypto {
class var shared: ChannelCrypto { get }
func sha256(data: KotlinByteArray) -> KotlinByteArray
func seal(
key: KotlinByteArray,
plaintext: KotlinByteArray,
associatedData: KotlinByteArray?
) -> KotlinByteArray
func open(
key: KotlinByteArray,
ciphertext: KotlinByteArray,
associatedData: KotlinByteArray?
) -> KotlinByteArray
func hkdfSha256(
ikm: KotlinByteArray,
salt: KotlinByteArray,
info: KotlinByteArray,
sizeBits: Int32
) -> KotlinByteArray
}
| 함수 | 반환 | 오류 |
|---|---|---|
sha256 | 32바이트 해시 | 제공자 오류 가능 |
seal | IV가 포함된 인증 암호문 | 키 길이·제공자 오류 |
open | 검증된 평문 | 잘못된 키, AAD 또는 변조 시 복호화 오류 |
hkdfSha256 | sizeBits / 8 바이트 | 잘못된 출력 크기·제공자 오류 |
- Kotlin (Android · JVM)
- Swift (iOS)
import io.lambda256.scopeconnect.walletkit.crypto.ChannelCrypto
val key = ByteArray(32) { it.toByte() }
val plaintext = "scope-session".encodeToByteArray()
val sealed = ChannelCrypto.seal(key, plaintext)
val opened = ChannelCrypto.open(key, sealed)
check(opened.contentEquals(plaintext))
import WalletKit
let key = KotlinByteArray(size: 32)
for i in 0..<32 { key.set(index: Int32(i), value: Int8(i)) }
let plaintext = HexKt.hexToBytes("73636f70652d73657373696f6e") // "scope-session"
let sealed = ChannelCrypto.shared.seal(key: key, plaintext: plaintext, associatedData: nil)
let opened = ChannelCrypto.shared.open(key: key, ciphertext: sealed, associatedData: nil)
assert(opened.toHex() == plaintext.toHex())
X25519
양쪽이 임시 키 쌍을 만들고 동일한 공유 비밀을 계산합니다.
- Kotlin (Android · JVM)
- Swift (iOS)
public object X25519 {
public class KeyPair {
public val publicKeyRaw: ByteArray
}
public fun generateKeyPair(): KeyPair
public fun sharedSecret(
self: KeyPair,
peerPublicKeyRaw: ByteArray,
): ByteArray
}
class X25519 {
class var shared: X25519 { get }
func generateKeyPair() -> X25519.KeyPair
// Kotlin 파라미터 이름이 self 이므로 Swift 레이블도 self: 입니다.
func sharedSecret(
self keyPair: X25519.KeyPair,
peerPublicKeyRaw: KotlinByteArray
) -> KotlinByteArray
class KeyPair {
var publicKeyRaw: KotlinByteArray { get }
}
}
peerPublicKeyRaw는 32바이트 RAW X25519 공개키여야 합니다. 반환된 공유 비밀을 직접 암호 키로 쓰지 말고 ChannelCrypto.hkdfSha256()로 파생합니다. 세션 키 합의가 이 조합(X25519 ECDH → HKDF-SHA256)으로 이루어집니다.
- Kotlin (Android · JVM)
- Swift (iOS)
import io.lambda256.scopeconnect.walletkit.crypto.X25519
val alice = X25519.generateKeyPair()
val bob = X25519.generateKeyPair()
val a = X25519.sharedSecret(alice, bob.publicKeyRaw)
val b = X25519.sharedSecret(bob, alice.publicKeyRaw)
check(a.contentEquals(b))
import WalletKit
let alice = X25519.shared.generateKeyPair()
let bob = X25519.shared.generateKeyPair()
let a = X25519.shared.sharedSecret(self: alice, peerPublicKeyRaw: bob.publicKeyRaw)
let b = X25519.shared.sharedSecret(self: bob, peerPublicKeyRaw: alice.publicKeyRaw)
assert(a.toHex() == b.toHex())
Secp256k1Signer와 EthSignature
32바이트 서명 해시(digest)에 서명해 Ethereum 형식의 복구 가능한 서명을 만듭니다.
- Kotlin (Android · JVM)
- Swift (iOS)
public class EthSignature(
public val r: ByteArray,
public val s: ByteArray,
public val v: Int,
) {
public fun toBytes(): ByteArray
}
public object Secp256k1Signer {
public fun signRecoverable(
digest: ByteArray,
privateKey: ByteArray,
): EthSignature
public fun addressOf(privateKey: ByteArray): String
public fun recoverAddress(
digest: ByteArray,
signature: EthSignature,
): String
}
class EthSignature {
init(r: KotlinByteArray, s: KotlinByteArray, v: Int32)
var r: KotlinByteArray { get }
var s: KotlinByteArray { get }
var v: Int32 { get }
func toBytes() -> KotlinByteArray
}
class Secp256k1Signer {
class var shared: Secp256k1Signer { get }
func signRecoverable(
digest: KotlinByteArray,
privateKey: KotlinByteArray
) -> EthSignature
func addressOf(privateKey: KotlinByteArray) -> String
func recoverAddress(
digest: KotlinByteArray,
signature: EthSignature
) -> String
}
반환: r과 s는 각각 32바이트이며 v는 27 또는 28입니다. 주소 함수는 0x 접두사 없는 소문자 40자리 hex를 반환합니다.
오류: 서명 해시 또는 개인키 길이가 32바이트가 아니거나 복구 ID를 결정할 수 없으면 IllegalArgumentException을 던집니다.
- Kotlin (Android · JVM)
- Swift (iOS)
import io.lambda256.scopeconnect.walletkit.crypto.ChannelCrypto
import io.lambda256.scopeconnect.walletkit.crypto.Secp256k1Signer
val privateKey = ByteArray(32).also { it[31] = 1 }
val digest = ChannelCrypto.sha256("approve".encodeToByteArray())
val signature = Secp256k1Signer.signRecoverable(digest, privateKey)
check(
Secp256k1Signer.recoverAddress(digest, signature) ==
Secp256k1Signer.addressOf(privateKey),
)
import WalletKit
let privateKey = KotlinByteArray(size: 32)
privateKey.set(index: 31, value: 1)
let digest = ChannelCrypto.shared.sha256(data: HexKt.hexToBytes("617070726f7665")) // "approve"
let signature = Secp256k1Signer.shared.signRecoverable(digest: digest, privateKey: privateKey)
assert(
Secp256k1Signer.shared.recoverAddress(digest: digest, signature: signature)
== Secp256k1Signer.shared.addressOf(privateKey: privateKey)
)
개인키는 샘플 상수로 저장하지 않습니다. 위 키는 로컬 연산 검증용이며 실제 지갑에서는 자체 보안 구조와 사용자 인증 정책을 적용합니다. 이 함수를 호출하면 개인키 바이트가 지갑 앱 프로세스 메모리에서 처리됩니다.
Kotlin object는 Swift에서 .shared로 접근합니다 — Secp256k1Signer.shared.signRecoverable(digest:privateKey:). Swift 쪽 호출 예시는 서명하기의 Swift 탭에 있습니다.
해시와 16진수 변환
- Kotlin (Android · JVM)
- Swift (iOS)
public fun keccak256(data: ByteArray): ByteArray
public fun ByteArray.toHex(): String
public fun String.hexToBytes(): ByteArray
// 최상위 함수는 파일 이름을 딴 클래스의 정적 멤버로 노출됩니다.
KeccakKt.keccak256(data: KotlinByteArray) -> KotlinByteArray
HexKt.hexToBytes(_ receiver: String) -> KotlinByteArray
// ByteArray 확장 함수는 KotlinByteArray 의 메서드로 노출됩니다.
bytes.toHex() -> String
keccak256은 EVM에서 쓰는 legacy Keccak-256이며 NIST SHA3-256과 다릅니다. hexToBytes()는 선택적 0x 접두사를 허용하며 길이가 홀수이거나 문자가 잘못되면 IllegalArgumentException을 던집니다.
- Kotlin (Android · JVM)
- Swift (iOS)
import io.lambda256.scopeconnect.walletkit.crypto.hexToBytes
import io.lambda256.scopeconnect.walletkit.crypto.toHex
val bytes = "0x0102ff".hexToBytes()
check(bytes.toHex() == "0102ff")
import WalletKit
let bytes = HexKt.hexToBytes("0x0102ff")
assert(bytes.toHex() == "0102ff")
walletKitCryptoProvider
- Kotlin (Android · JVM)
- Swift (iOS)
public expect val walletKitCryptoProvider: CryptographyProvider
// expect 프로퍼티는 actual 을 선언한 플랫폼 소스 파일명을 딴
// 클래스의 정적 멤버로 노출됩니다 (iOS: CryptoProvider.apple.kt).
CryptoProvider_appleKt.walletKitCryptoProvider
WalletKit이 플랫폼별로 구성한 공개 암호 제공자입니다. iOS는 CryptoKit, JVM은 JDK 제공자를 쓰고, Android는 JDK 제공자가 X25519·Ed25519를 지원하지 않아 BouncyCastle을 포함합니다. 애플리케이션이 같은 제공자를 직접 사용해야 할 때 선택합니다.
EIP-712 인코딩
Eip712
구조화 데이터의 기본 타입 값과 도메인 분리자를 인코딩하고, 최종 서명 해시(digest)를 계산합니다.
- Kotlin (Android · JVM)
- Swift (iOS)
public object Eip712 {
public fun typeHash(encodeType: String): ByteArray
public fun uint256(value: Long): ByteArray
public fun uint256(decimal: String): ByteArray
public fun address(addressHex: String): ByteArray
public fun bytes32(hex: String): ByteArray
public fun string(value: String): ByteArray
public fun domainSeparator(
name: String,
version: String,
chainId: Long,
verifyingContract: String,
): ByteArray
public fun digest(
domainSeparator: ByteArray,
hashStruct: ByteArray,
): ByteArray
}
class Eip712 {
class var shared: Eip712 { get }
func typeHash(encodeType: String) -> KotlinByteArray
// 오버로드는 파라미터 레이블로 구분됩니다.
func uint256(value: Int64) -> KotlinByteArray
func uint256(decimal: String) -> KotlinByteArray
func address(addressHex: String) -> KotlinByteArray
func bytes32(hex: String) -> KotlinByteArray
func string(value: String) -> KotlinByteArray
func domainSeparator(
name: String,
version: String,
chainId: Int64,
verifyingContract: String
) -> KotlinByteArray
func digest(
domainSeparator: KotlinByteArray,
hashStruct: KotlinByteArray
) -> KotlinByteArray
}
| 함수 | 제약 | 반환 |
|---|---|---|
uint256(Long) | 0 이상 | 32바이트 big-endian |
uint256(String) | 비어 있지 않은 10진수, 256비트 이하 | 32바이트 big-endian |
address | 20바이트 hex | 12바이트 왼쪽 패딩을 더한 값 |
bytes32 | 정확히 32바이트 hex | 32바이트 |
domainSeparator | 일반 도메인 필드 | 32바이트 hash |
digest | 두 입력 모두 32바이트 | 32바이트 서명 해시 |
제약을 어기면 IllegalArgumentException을 던집니다.
- Kotlin (Android · JVM)
- Swift (iOS)
import io.lambda256.scopeconnect.walletkit.crypto.keccak256
import io.lambda256.scopeconnect.walletkit.eip.Eip712
val domain = Eip712.domainSeparator(
name = "Example",
version = "1",
chainId = 11155111,
verifyingContract = "0x0000000000000000000000000000000000000001",
)
val messageHash = keccak256(
Eip712.typeHash("Message(string contents)") +
Eip712.string("hello"),
)
val digest = Eip712.digest(domain, messageHash)
check(digest.size == 32)
import WalletKit
let domain = Eip712.shared.domainSeparator(
name: "Example",
version: "1",
chainId: 11155111,
verifyingContract: "0x0000000000000000000000000000000000000001"
)
// KotlinByteArray 는 + 연결이 안 되므로 hex 로 이어 붙여 다시 디코딩합니다.
let typeHash = Eip712.shared.typeHash(encodeType: "Message(string contents)")
let contents = Eip712.shared.string(value: "hello")
let messageHash = KeccakKt.keccak256(
data: HexKt.hexToBytes(typeHash.toHex() + contents.toHex())
)
let digest = Eip712.shared.digest(domainSeparator: domain, hashStruct: messageHash)
assert(digest.size == 32)