mirror of
https://github.com/status-im/react-native-status-keycard.git
synced 2025-02-28 12:00:36 +00:00
373 lines
16 KiB
Swift
373 lines
16 KiB
Swift
import Foundation
|
|
import Keycard
|
|
import os.log
|
|
|
|
enum SmartCardError: Error {
|
|
case invalidBase64
|
|
}
|
|
|
|
enum DerivationPath: String {
|
|
case masterPath = "m"
|
|
case rootPath = "m/44'/60'/0'/0"
|
|
case walletPath = "m/44'/60'/0'/0/0"
|
|
case whisperPath = "m/43'/60'/1581'/0'/0"
|
|
case encryptionPath = "m/43'/60'/1581'/1'/0"
|
|
}
|
|
|
|
class SmartCard {
|
|
func initialize(channel: CardChannel, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let puk = self.randomPUK()
|
|
let pairingPassword = self.randomPairingPassword();
|
|
|
|
let cmdSet = KeycardCommandSet(cardChannel: channel)
|
|
try cmdSet.select().checkOK()
|
|
try cmdSet.initialize(pin: pin, puk: puk, pairingPassword: pairingPassword).checkOK();
|
|
|
|
resolve(["pin": pin, "puk": puk, "password": pairingPassword])
|
|
}
|
|
|
|
func pair(channel: CardChannel, pairingPassword: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = KeycardCommandSet(cardChannel: channel)
|
|
let info = try ApplicationInfo(cmdSet.select().checkOK().data)
|
|
|
|
logAppInfo(info)
|
|
|
|
try cmdSet.autoPair(password: pairingPassword)
|
|
|
|
resolve(Data(cmdSet.pairing!.bytes).base64EncodedString())
|
|
}
|
|
|
|
func generateMnemonic(channel: CardChannel, pairingBase64: String, words: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try securedCommandSet(channel: channel, pairingBase64: pairingBase64)
|
|
|
|
let mnemonic = try Mnemonic(rawData: cmdSet.generateMnemonic(length: GenerateMnemonicP1.length12Words).checkOK().data)
|
|
mnemonic.wordList = words.components(separatedBy: .newlines)
|
|
|
|
resolve(mnemonic.toMnemonicPhrase())
|
|
}
|
|
|
|
func generateAndLoadKey(channel: CardChannel, mnemonic: String, pairingBase64: String, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
let seed = Mnemonic.toBinarySeed(mnemonicPhrase: mnemonic)
|
|
let keyPair = BIP32KeyPair(fromSeed: seed)
|
|
|
|
try cmdSet.loadKey(keyPair: keyPair).checkOK()
|
|
os_log("keypair loaded to card");
|
|
|
|
let rootKeyPair = try exportKey(cmdSet: cmdSet, path: .rootPath, makeCurrent: false, publicOnly: true)
|
|
let whisperKeyPair = try exportKey(cmdSet: cmdSet, path: .whisperPath, makeCurrent: false, publicOnly: false)
|
|
let encryptionKeyPair = try exportKey(cmdSet: cmdSet, path: .encryptionPath, makeCurrent: false, publicOnly: false)
|
|
let walletKeyPair = try exportKey(cmdSet: cmdSet, path: .walletPath, makeCurrent: false, publicOnly: true)
|
|
|
|
let info = try ApplicationInfo(cmdSet.select().checkOK().data)
|
|
|
|
resolve([
|
|
"address": bytesToHex(keyPair.toEthereumAddress()),
|
|
"public-key": bytesToHex(keyPair.publicKey),
|
|
"wallet-root-address": bytesToHex(rootKeyPair.toEthereumAddress()),
|
|
"wallet-root-public-key": bytesToHex(rootKeyPair.publicKey),
|
|
"wallet-address": bytesToHex(walletKeyPair.toEthereumAddress()),
|
|
"wallet-public-key": bytesToHex(walletKeyPair.publicKey),
|
|
"whisper-address": bytesToHex(whisperKeyPair.toEthereumAddress()),
|
|
"whisper-public-key": bytesToHex(whisperKeyPair.publicKey),
|
|
"whisper-private-key": bytesToHex(whisperKeyPair.privateKey!),
|
|
"encryption-public-key": bytesToHex(encryptionKeyPair.publicKey),
|
|
"instance-uid": bytesToHex(info.instanceUID),
|
|
"key-uid": bytesToHex(info.keyUID)
|
|
])
|
|
}
|
|
|
|
func saveMnemonic(channel: CardChannel, mnemonic: String, pairingBase64: String, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
let seed = Mnemonic.toBinarySeed(mnemonicPhrase: mnemonic)
|
|
try cmdSet.loadKey(seed: seed).checkOK()
|
|
os_log("seed loaded to card");
|
|
resolve(true)
|
|
}
|
|
|
|
func getApplicationInfo(channel: CardChannel, pairingBase64: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = KeycardCommandSet(cardChannel: channel)
|
|
let info = try ApplicationInfo(cmdSet.select().checkOK().data)
|
|
|
|
os_log("Card initialized? %@", String(info.initializedCard))
|
|
var cardInfo = [String: Any]()
|
|
cardInfo["initialized?"] = info.initializedCard
|
|
|
|
if (info.initializedCard) {
|
|
logAppInfo(info)
|
|
var isPaired = false
|
|
|
|
if (!pairingBase64.isEmpty) {
|
|
do {
|
|
try openSecureChannel(cmdSet: cmdSet, pairingBase64: pairingBase64)
|
|
isPaired = true
|
|
} catch let error as CardError {
|
|
os_log("autoOpenSecureChannel failed: %@", String(describing: error));
|
|
} catch let error as StatusWord {
|
|
os_log("autoOpenSecureChannel failed: %@", String(describing: error));
|
|
}
|
|
|
|
if (isPaired) {
|
|
let status = try ApplicationStatus(cmdSet.getStatus(info: GetStatusP1.application.rawValue).checkOK().data);
|
|
os_log("PIN retry counter: %d", status.pinRetryCount)
|
|
os_log("PUK retry counter: %d", status.pukRetryCount)
|
|
|
|
cardInfo["pin-retry-counter"] = status.pinRetryCount
|
|
cardInfo["puk-retry-counter"] = status.pukRetryCount
|
|
}
|
|
}
|
|
|
|
cardInfo["paired?"] = isPaired
|
|
}
|
|
|
|
cardInfo["has-master-key?"] = info.hasMasterKey
|
|
cardInfo["instance-uid"] = bytesToHex(info.instanceUID)
|
|
cardInfo["key-uid"] = bytesToHex(info.keyUID)
|
|
cardInfo["secure-channel-pub-key"] = bytesToHex(info.secureChannelPubKey)
|
|
cardInfo["app-version"] = info.appVersionString
|
|
cardInfo["free-pairing-slots"] = info.freePairingSlots
|
|
|
|
resolve(cardInfo)
|
|
}
|
|
|
|
func deriveKey(channel: CardChannel, path: String, pairingBase64: String, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
let currentPath = try KeyPath(data: cmdSet.getStatus(info: GetStatusP1.keyPath.rawValue).checkOK().data);
|
|
os_log("Current key path: %@", currentPath.description)
|
|
|
|
if (currentPath.description != path) {
|
|
try cmdSet.deriveKey(path: path).checkOK()
|
|
os_log("Derived %@", path)
|
|
}
|
|
|
|
resolve(true)
|
|
}
|
|
|
|
func exportKey(channel: CardChannel, pairingBase64: String, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
let key = try cmdSet.exportCurrentKey(publicOnly: true).checkOK().data
|
|
resolve(bytesToHex(key))
|
|
}
|
|
|
|
func exportKeyWithPath(channel: CardChannel, pairingBase64: String, pin: String, path: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
let key = try BIP32KeyPair(fromTLV: cmdSet.exportKey(path: path, makeCurrent: false, publicOnly: true).checkOK().data).publicKey;
|
|
|
|
resolve(bytesToHex(key))
|
|
}
|
|
|
|
func getKeys(channel: CardChannel, pairingBase64: String, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
|
|
let encryptionKeyPair = try exportKey(cmdSet: cmdSet, path: .encryptionPath, makeCurrent: false, publicOnly: false)
|
|
let masterPair = try exportKey(cmdSet: cmdSet, path: .masterPath, makeCurrent: false, publicOnly: true)
|
|
let rootKeyPair = try exportKey(cmdSet: cmdSet, path: .rootPath, makeCurrent: false, publicOnly: true)
|
|
let whisperKeyPair = try exportKey(cmdSet: cmdSet, path: .whisperPath, makeCurrent: false, publicOnly: false)
|
|
let walletKeyPair = try exportKey(cmdSet: cmdSet, path: .walletPath, makeCurrent: false, publicOnly: true)
|
|
|
|
let info = try ApplicationInfo(cmdSet.select().checkOK().data)
|
|
|
|
resolve([
|
|
"address": bytesToHex(masterPair.toEthereumAddress()),
|
|
"public-key": bytesToHex(masterPair.publicKey),
|
|
"wallet-root-address": bytesToHex(rootKeyPair.toEthereumAddress()),
|
|
"wallet-root-public-key": bytesToHex(rootKeyPair.publicKey),
|
|
"wallet-address": bytesToHex(walletKeyPair.toEthereumAddress()),
|
|
"wallet-public-key": bytesToHex(walletKeyPair.publicKey),
|
|
"whisper-address": bytesToHex(whisperKeyPair.toEthereumAddress()),
|
|
"whisper-public-key": bytesToHex(whisperKeyPair.publicKey),
|
|
"whisper-private-key": bytesToHex(whisperKeyPair.privateKey!),
|
|
"encryption-public-key": bytesToHex(encryptionKeyPair.publicKey),
|
|
"instance-uid": bytesToHex(info.instanceUID),
|
|
"key-uid": bytesToHex(info.keyUID)
|
|
])
|
|
}
|
|
|
|
func sign(channel: CardChannel, pairingBase64: String, pin: String, message: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
let sig = try processSignature(message) { return try cmdSet.sign(hash: $0) }
|
|
resolve(sig)
|
|
}
|
|
|
|
func signWithPath(channel: CardChannel, pairingBase64: String, pin: String, path: String, message: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
let sig = try processSignature(message) {
|
|
if (cmdSet.info!.appVersion < 0x0202) {
|
|
let currentPath = try KeyPath(data: cmdSet.getStatus(info: GetStatusP1.keyPath.rawValue).checkOK().data);
|
|
|
|
if (currentPath.description != path) {
|
|
try cmdSet.deriveKey(path: path).checkOK()
|
|
}
|
|
|
|
return try cmdSet.sign(hash: $0)
|
|
} else {
|
|
return try cmdSet.sign(hash: $0, path: path, makeCurrent: false)
|
|
}
|
|
}
|
|
|
|
resolve(sig)
|
|
}
|
|
|
|
func signPinless(channel: CardChannel, message: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = CashCommandSet(cardChannel: channel)
|
|
try cmdSet.select().checkOK()
|
|
|
|
let sig = try processSignature(message) { return try cmdSet.sign(data: $0) }
|
|
resolve(sig)
|
|
}
|
|
|
|
func verifyPin(channel: CardChannel, pairingBase64: String, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
let status = try ApplicationStatus(cmdSet.getStatus(info: GetStatusP1.application.rawValue).checkOK().data);
|
|
resolve(status.pinRetryCount)
|
|
}
|
|
|
|
func changePin(channel: CardChannel, pairingBase64: String, currentPin: String, newPin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: currentPin)
|
|
try cmdSet.changePIN(pin: newPin).checkOK()
|
|
os_log("pin changed")
|
|
resolve(true)
|
|
}
|
|
|
|
func unblockPin(channel: CardChannel, pairingBase64: String, puk: String, newPin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try securedCommandSet(channel: channel, pairingBase64: pairingBase64)
|
|
try cmdSet.unblockPIN(puk: puk, newPIN: newPin).checkAuthOK()
|
|
os_log("pin unblocked")
|
|
resolve(true)
|
|
}
|
|
|
|
func unpair(channel: CardChannel, pairingBase64: String, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
|
|
try cmdSet.autoUnpair()
|
|
os_log("card unpaired")
|
|
|
|
resolve(true)
|
|
}
|
|
|
|
func removeKey(channel: CardChannel, pairingBase64: String, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
try cmdSet.removeKey().checkOK()
|
|
os_log("key removed")
|
|
|
|
resolve(true)
|
|
}
|
|
|
|
func removeKeyWithUnpair(channel: CardChannel, pairingBase64: String, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
|
|
let cmdSet = try authenticatedCommandSet(channel: channel, pairingBase64: pairingBase64, pin: pin)
|
|
try cmdSet.removeKey().checkOK()
|
|
os_log("key removed")
|
|
|
|
try cmdSet.autoUnpair()
|
|
os_log("card unpaired")
|
|
|
|
resolve(true)
|
|
}
|
|
|
|
func randomPUK() -> String {
|
|
return String(format: "%012ld", Int64.random(in: 0..<999999999999))
|
|
}
|
|
|
|
func randomPairingPassword() -> String {
|
|
let digits = "23456789"
|
|
let letters = "abcdefghijkmnopqrstuvwxyz"
|
|
return String((0..<5).map{ i in ((i % 2) == 0) ? letters.randomElement()! : digits.randomElement()! })
|
|
}
|
|
|
|
func exportKey(cmdSet: KeycardCommandSet, path: DerivationPath, makeCurrent: Bool, publicOnly: Bool) throws -> BIP32KeyPair {
|
|
let tlvRoot = try cmdSet.exportKey(path: path.rawValue, makeCurrent: makeCurrent, publicOnly: publicOnly).checkOK().data
|
|
os_log("Derived %@", path.rawValue)
|
|
return try BIP32KeyPair(fromTLV: tlvRoot)
|
|
}
|
|
|
|
func authenticatedCommandSet(channel: CardChannel, pairingBase64: String, pin: String) throws -> KeycardCommandSet {
|
|
let cmdSet = try securedCommandSet(channel: channel, pairingBase64: pairingBase64)
|
|
try cmdSet.verifyPIN(pin: pin).checkAuthOK()
|
|
os_log("pin verified")
|
|
|
|
return cmdSet;
|
|
}
|
|
|
|
func securedCommandSet(channel: CardChannel, pairingBase64: String) throws -> KeycardCommandSet {
|
|
let cmdSet = KeycardCommandSet(cardChannel: channel)
|
|
try cmdSet.select().checkOK()
|
|
try openSecureChannel(cmdSet: cmdSet, pairingBase64: pairingBase64)
|
|
|
|
return cmdSet
|
|
}
|
|
|
|
func openSecureChannel(cmdSet: KeycardCommandSet, pairingBase64: String) throws -> Void {
|
|
cmdSet.pairing = try base64ToPairing(pairingBase64)
|
|
|
|
try cmdSet.autoOpenSecureChannel()
|
|
os_log("secure channel opened")
|
|
}
|
|
|
|
func processSignature(_ message: String, sign: ([UInt8]) throws -> APDUResponse) throws -> String {
|
|
let hash = hexToBytes(message)
|
|
let signature = try RecoverableSignature(hash: hash, data: sign(hash).checkOK().data)
|
|
logSignature(hash, signature)
|
|
return formatSignature(signature)
|
|
}
|
|
|
|
func base64ToPairing(_ base64: String) throws -> Pairing {
|
|
if let data = Data(base64Encoded: base64) {
|
|
return Pairing(pairingData: [UInt8](data))
|
|
} else {
|
|
throw SmartCardError.invalidBase64
|
|
}
|
|
}
|
|
|
|
func logAppInfo(_ info: ApplicationInfo) -> Void {
|
|
os_log("Instance UID: %@", bytesToHex(info.instanceUID))
|
|
os_log("Key UID: %@", bytesToHex(info.keyUID))
|
|
os_log("Secure channel public key: %@", bytesToHex(info.secureChannelPubKey))
|
|
os_log("Application version: %@", info.appVersionString)
|
|
os_log("Free pairing slots: %d", info.freePairingSlots)
|
|
}
|
|
|
|
func logSignature(_ hash: [UInt8], _ signature: RecoverableSignature) -> Void {
|
|
os_log("Signed hash: %@", bytesToHex(hash))
|
|
os_log("Recovery ID: %d", signature.recId)
|
|
os_log("R: %@", bytesToHex(signature.r))
|
|
os_log("S: %@", bytesToHex(signature.s))
|
|
}
|
|
|
|
func formatSignature(_ signature: RecoverableSignature) -> String {
|
|
var out = Data(signature.r)
|
|
out.append(contentsOf: signature.s)
|
|
out.append(contentsOf: [signature.recId])
|
|
let sig = dataToHex(out)
|
|
|
|
os_log("Signature: %@", sig)
|
|
return sig
|
|
}
|
|
|
|
func dataToHex(_ data: Data) -> String {
|
|
return data.map { String(format: "%02hhx", $0) }.joined()
|
|
}
|
|
|
|
func bytesToHex(_ bytes: [UInt8]) -> String {
|
|
return bytes.map { String(format: "%02hhx", $0) }.joined()
|
|
}
|
|
|
|
func hexToBytes(_ hex: String) -> [UInt8] {
|
|
let h = hex.starts(with: "0x") ? String(hex.dropFirst(2)) : hex
|
|
|
|
var last = h.first
|
|
return h.dropFirst().compactMap {
|
|
guard
|
|
let lastHexDigitValue = last?.hexDigitValue,
|
|
let hexDigitValue = $0.hexDigitValue
|
|
else {
|
|
last = $0
|
|
return nil
|
|
}
|
|
defer {
|
|
last = nil
|
|
}
|
|
return UInt8(lastHexDigitValue * 16 + hexDigitValue)
|
|
}
|
|
}
|
|
}
|