2021-01-13 17:19:11 +03:00

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)
}
}
}