Skip to content

Commit 735bed7

Browse files
authored
Keycard 3.1.0 support (#54)
* full keycard 3.1 support (android) * handle empty card name * full keycard 3.1 support (ios) * send keyCardOnDisconnected on NFC Error 102 * temporarely keep the wallet keys in exported data
1 parent 26c476b commit 735bed7

File tree

5 files changed

+171
-41
lines changed

5 files changed

+171
-41
lines changed

android/src/main/java/im/status/ethereum/keycard/RNStatusKeycardModule.java

+29
Original file line numberDiff line numberDiff line change
@@ -448,6 +448,35 @@ public void run() {
448448
}).start();
449449
}
450450

451+
@ReactMethod
452+
public void getCardName(final Promise promise) {
453+
new Thread(new Runnable() {
454+
public void run() {
455+
try {
456+
promise.resolve(smartCard.getCardName());
457+
} catch (IOException | APDUException e) {
458+
Log.d(TAG, e.getMessage());
459+
promise.reject(e);
460+
}
461+
}
462+
}).start();
463+
}
464+
465+
@ReactMethod
466+
public void setCardName(final String pin, final String name, final Promise promise) {
467+
new Thread(new Runnable() {
468+
public void run() {
469+
try {
470+
smartCard.setCardName(pin, name);
471+
promise.resolve(true);
472+
} catch (IOException | APDUException e) {
473+
Log.d(TAG, e.getMessage());
474+
promise.reject(e);
475+
}
476+
}
477+
}).start();
478+
}
479+
451480
@ReactMethod
452481
public void unpairAndDelete(final String pin, final Promise promise) {
453482
promise.reject("E_KEYCARD", "Not implemented (unused)");

android/src/main/java/im/status/ethereum/keycard/SmartCard.java

+60-20
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
import im.status.keycard.applet.ApplicationStatus;
3636
import im.status.keycard.applet.BIP32KeyPair;
3737
import im.status.keycard.applet.Mnemonic;
38+
import im.status.keycard.applet.Metadata;
3839
import im.status.keycard.applet.CashCommandSet;
3940
import im.status.keycard.applet.KeycardCommandSet;
4041
import im.status.keycard.applet.Pairing;
@@ -285,8 +286,11 @@ public WritableMap getApplicationInfo() throws IOException, APDUException {
285286

286287
if (info.isInitializedCard()) {
287288
String instanceUID = Hex.toHexString(info.getInstanceUID());
289+
String cardName = getCardNameOrDefault(cmdSet);
290+
cardInfo.putString("card-name", cardName);
288291

289292
Log.i(TAG, "Instance UID: " + instanceUID);
293+
Log.i(TAG, "Card name: " + cardName);
290294
Log.i(TAG, "Key UID: " + Hex.toHexString(info.getKeyUID()));
291295
Log.i(TAG, "Secure channel public key: " + Hex.toHexString(info.getSecureChannelPubKey()));
292296
Log.i(TAG, "Application version: " + info.getAppVersionString());
@@ -437,31 +441,37 @@ public WritableMap getKeys(final String pin) throws IOException, APDUException {
437441

438442
public WritableMap importKeys(final String pin) throws IOException, APDUException {
439443
KeycardCommandSet cmdSet = authenticatedCommandSet(pin);
444+
ApplicationInfo info = cmdSet.getApplicationInfo();
445+
446+
byte p2 = (info.getAppVersion() < 0x0310) ? KeycardCommandSet.EXPORT_KEY_P2_PUBLIC_ONLY : KeycardCommandSet.EXPORT_KEY_P2_EXTENDED_PUBLIC;
440447

441448
byte[] tlvEncryption = cmdSet.exportKey(ENCRYPTION_PATH, false, false).checkOK().getData();
442449
BIP32KeyPair encryptionKeyPair = BIP32KeyPair.fromTLV(tlvEncryption);
443450

444451
byte[] tlvMaster = cmdSet.exportKey(MASTER_PATH, false, true).checkOK().getData();
445452
BIP32KeyPair masterPair = BIP32KeyPair.fromTLV(tlvMaster);
446453

447-
byte[] tlvRoot = cmdSet.exportKey(ROOT_PATH, false, true).checkOK().getData();
448-
BIP32KeyPair keyPair = BIP32KeyPair.fromTLV(tlvRoot);
454+
byte[] tlvRoot = cmdSet.exportKey(ROOT_PATH, false, p2).checkOK().getData();
455+
BIP32KeyPair rootKeyPair = BIP32KeyPair.fromTLV(tlvRoot);
449456

450457
byte[] tlvWhisper = cmdSet.exportKey(WHISPER_PATH, false, false).checkOK().getData();
451458
BIP32KeyPair whisperKeyPair = BIP32KeyPair.fromTLV(tlvWhisper);
452459

453-
byte[] tlvWallet = cmdSet.exportKey(WALLET_PATH, false, true).checkOK().getData();
454-
BIP32KeyPair walletKeyPair = BIP32KeyPair.fromTLV(tlvWallet);
455-
456-
ApplicationInfo info = cmdSet.getApplicationInfo();
457-
458460
WritableMap data = Arguments.createMap();
459461
data.putString("address", Hex.toHexString(masterPair.toEthereumAddress()));
460462
data.putString("public-key", Hex.toHexString(masterPair.getPublicKey()));
461-
data.putString("wallet-root-address", Hex.toHexString(keyPair.toEthereumAddress()));
462-
data.putString("wallet-root-public-key", Hex.toHexString(keyPair.getPublicKey()));
463-
data.putString("wallet-address", Hex.toHexString(walletKeyPair.toEthereumAddress()));
464-
data.putString("wallet-public-key", Hex.toHexString(walletKeyPair.getPublicKey()));
463+
data.putString("wallet-root-address", Hex.toHexString(rootKeyPair.toEthereumAddress()));
464+
data.putString("wallet-root-public-key", Hex.toHexString(rootKeyPair.getPublicKey()));
465+
466+
if (rootKeyPair.isExtended()) {
467+
data.putString("wallet-root-chain-code", Hex.toHexString(rootKeyPair.getChainCode()));
468+
} //else { (for now we return both keys, because xpub support is not yet available)
469+
byte[] tlvWallet = cmdSet.exportKey(WALLET_PATH, false, true).checkOK().getData();
470+
BIP32KeyPair walletKeyPair = BIP32KeyPair.fromTLV(tlvWallet);
471+
data.putString("wallet-address", Hex.toHexString(walletKeyPair.toEthereumAddress()));
472+
data.putString("wallet-public-key", Hex.toHexString(walletKeyPair.getPublicKey()));
473+
//}
474+
465475
data.putString("whisper-address", Hex.toHexString(whisperKeyPair.toEthereumAddress()));
466476
data.putString("whisper-public-key", Hex.toHexString(whisperKeyPair.getPublicKey()));
467477
data.putString("whisper-private-key", Hex.toHexString(whisperKeyPair.getPrivateKey()));
@@ -474,14 +484,15 @@ public WritableMap importKeys(final String pin) throws IOException, APDUExceptio
474484

475485
public WritableMap generateAndLoadKey(final String mnemonic, final String pin) throws IOException, APDUException {
476486
KeycardCommandSet cmdSet = authenticatedCommandSet(pin);
487+
byte p2 = (cmdSet.getApplicationInfo().getAppVersion() < 0x0310) ? KeycardCommandSet.EXPORT_KEY_P2_PUBLIC_ONLY : KeycardCommandSet.EXPORT_KEY_P2_EXTENDED_PUBLIC;
477488

478489
byte[] seed = Mnemonic.toBinarySeed(mnemonic, "");
479490
BIP32KeyPair keyPair = BIP32KeyPair.fromBinarySeed(seed);
480491

481492
cmdSet.loadKey(keyPair).checkOK();
482493
log("keypair loaded to card");
483494

484-
byte[] tlvRoot = cmdSet.exportKey(ROOT_PATH, false, true).checkOK().getData();
495+
byte[] tlvRoot = cmdSet.exportKey(ROOT_PATH, false, p2).checkOK().getData();
485496
Log.i(TAG, "Derived " + ROOT_PATH);
486497
BIP32KeyPair rootKeyPair = BIP32KeyPair.fromTLV(tlvRoot);
487498

@@ -493,23 +504,28 @@ public WritableMap generateAndLoadKey(final String mnemonic, final String pin) t
493504
Log.i(TAG, "Derived " + ENCRYPTION_PATH);
494505
BIP32KeyPair encryptionKeyPair = BIP32KeyPair.fromTLV(tlvEncryption);
495506

496-
byte[] tlvWallet = cmdSet.exportKey(WALLET_PATH, false, true).checkOK().getData();
497-
Log.i(TAG, "Derived " + WALLET_PATH);
498-
BIP32KeyPair walletKeyPair = BIP32KeyPair.fromTLV(tlvWallet);
499-
500-
ApplicationInfo info = new ApplicationInfo(cmdSet.select().checkOK().getData());
501-
502507
WritableMap data = Arguments.createMap();
503508
data.putString("address", Hex.toHexString(keyPair.toEthereumAddress()));
504509
data.putString("public-key", Hex.toHexString(keyPair.getPublicKey()));
505510
data.putString("wallet-root-address", Hex.toHexString(rootKeyPair.toEthereumAddress()));
506511
data.putString("wallet-root-public-key", Hex.toHexString(rootKeyPair.getPublicKey()));
507-
data.putString("wallet-address", Hex.toHexString(walletKeyPair.toEthereumAddress()));
508-
data.putString("wallet-public-key", Hex.toHexString(walletKeyPair.getPublicKey()));
512+
513+
if (rootKeyPair.isExtended()) {
514+
data.putString("wallet-root-chain-code", Hex.toHexString(rootKeyPair.getChainCode()));
515+
} //else { (see note above)
516+
byte[] tlvWallet = cmdSet.exportKey(WALLET_PATH, false, true).checkOK().getData();
517+
BIP32KeyPair walletKeyPair = BIP32KeyPair.fromTLV(tlvWallet);
518+
data.putString("wallet-address", Hex.toHexString(walletKeyPair.toEthereumAddress()));
519+
data.putString("wallet-public-key", Hex.toHexString(walletKeyPair.getPublicKey()));
520+
//}
521+
509522
data.putString("whisper-address", Hex.toHexString(whisperKeyPair.toEthereumAddress()));
510523
data.putString("whisper-public-key", Hex.toHexString(whisperKeyPair.getPublicKey()));
511524
data.putString("whisper-private-key", Hex.toHexString(whisperKeyPair.getPrivateKey()));
512525
data.putString("encryption-public-key", Hex.toHexString(encryptionKeyPair.getPublicKey()));
526+
527+
ApplicationInfo info = new ApplicationInfo(cmdSet.select().checkOK().getData());
528+
513529
data.putString("instance-uid", Hex.toHexString(info.getInstanceUID()));
514530
data.putString("key-uid", Hex.toHexString(info.getKeyUID()));
515531

@@ -662,6 +678,19 @@ public String signPinless(final String message) throws IOException, APDUExceptio
662678
return sig;
663679
}
664680

681+
public String getCardName() throws IOException, APDUException {
682+
KeycardCommandSet cmdSet = new KeycardCommandSet(this.cardChannel);
683+
cmdSet.select().checkOK();
684+
return getCardNameOrDefault(cmdSet);
685+
}
686+
687+
public void setCardName(final String pin, final String name) throws IOException, APDUException {
688+
KeycardCommandSet cmdSet = authenticatedCommandSet(pin);
689+
690+
Metadata m = new Metadata(name);
691+
cmdSet.storeData(m.toByteArray(), KeycardCommandSet.STORE_DATA_P1_PUBLIC).checkOK();
692+
}
693+
665694
public WritableMap verifyCard(final String challenge) throws IOException, APDUException {
666695
KeycardCommandSet cmdSet = new KeycardCommandSet(this.cardChannel);
667696
cmdSet.select().checkOK();
@@ -714,6 +743,17 @@ private KeycardCommandSet securedCommandSet() throws IOException, APDUException
714743
return cmdSet;
715744
}
716745

746+
private String getCardNameOrDefault(KeycardCommandSet cmdSet) throws IOException, APDUException {
747+
byte[] data = cmdSet.getData(KeycardCommandSet.STORE_DATA_P1_PUBLIC).checkOK().getData();
748+
749+
try {
750+
Metadata m = Metadata.fromData(data);
751+
return m.getCardName();
752+
} catch(Exception e) {
753+
return "";
754+
}
755+
}
756+
717757
private void openSecureChannel(KeycardCommandSet cmdSet) throws IOException, APDUException {
718758
String instanceUID = Hex.toHexString(cmdSet.getApplicationInfo().getInstanceUID());
719759
String pairingBase64 = pairings.get(instanceUID);

ios/SmartCard.swift

+69-20
Original file line numberDiff line numberDiff line change
@@ -54,33 +54,42 @@ class SmartCard {
5454

5555
func generateAndLoadKey(channel: CardChannel, mnemonic: String, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
5656
let cmdSet = try authenticatedCommandSet(channel: channel, pin: pin)
57+
let exportOption = cmdSet.info!.appVersion < 0x0310 ? KeycardCommandSet.ExportOption.publicOnly : .extendedPublic
58+
5759
let seed = Mnemonic.toBinarySeed(mnemonicPhrase: mnemonic)
5860
let keyPair = BIP32KeyPair(fromSeed: seed)
5961

6062
try cmdSet.loadKey(keyPair: keyPair).checkOK()
6163
os_log("keypair loaded to card")
6264

63-
let rootKeyPair = try exportKey(cmdSet: cmdSet, path: .rootPath, makeCurrent: false, publicOnly: true)
65+
let rootKeyPair = try exportKey(cmdSet: cmdSet, path: .rootPath, makeCurrent: false, exportOption: exportOption)
6466
let whisperKeyPair = try exportKey(cmdSet: cmdSet, path: .whisperPath, makeCurrent: false, publicOnly: false)
6567
let encryptionKeyPair = try exportKey(cmdSet: cmdSet, path: .encryptionPath, makeCurrent: false, publicOnly: false)
66-
let walletKeyPair = try exportKey(cmdSet: cmdSet, path: .walletPath, makeCurrent: false, publicOnly: true)
67-
68-
let info = try ApplicationInfo(cmdSet.select().checkOK().data)
6968

70-
resolve([
69+
var keys = [
7170
"address": bytesToHex(keyPair.toEthereumAddress()),
7271
"public-key": bytesToHex(keyPair.publicKey),
7372
"wallet-root-address": bytesToHex(rootKeyPair.toEthereumAddress()),
7473
"wallet-root-public-key": bytesToHex(rootKeyPair.publicKey),
75-
"wallet-address": bytesToHex(walletKeyPair.toEthereumAddress()),
76-
"wallet-public-key": bytesToHex(walletKeyPair.publicKey),
7774
"whisper-address": bytesToHex(whisperKeyPair.toEthereumAddress()),
7875
"whisper-public-key": bytesToHex(whisperKeyPair.publicKey),
7976
"whisper-private-key": bytesToHex(whisperKeyPair.privateKey!),
80-
"encryption-public-key": bytesToHex(encryptionKeyPair.publicKey),
81-
"instance-uid": bytesToHex(info.instanceUID),
82-
"key-uid": bytesToHex(info.keyUID)
83-
])
77+
"encryption-public-key": bytesToHex(encryptionKeyPair.publicKey)
78+
]
79+
80+
if rootKeyPair.isExtended {
81+
keys["wallet-root-chain-code"] = bytesToHex(rootKeyPair.chainCode!)
82+
} //else { (for now we return both keys, because xpub support is not yet available)
83+
let walletKeyPair = try exportKey(cmdSet: cmdSet, path: .walletPath, makeCurrent: false, publicOnly: true)
84+
keys["wallet-address"] = bytesToHex(walletKeyPair.toEthereumAddress())
85+
keys["wallet-public-key"] = bytesToHex(walletKeyPair.publicKey)
86+
//}
87+
88+
let info = try ApplicationInfo(cmdSet.select().checkOK().data)
89+
keys["instance-uid"] = bytesToHex(info.instanceUID)
90+
keys["key-uid"] = bytesToHex(info.keyUID)
91+
92+
resolve(keys)
8493
}
8594

8695
func saveMnemonic(channel: CardChannel, mnemonic: String, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
@@ -186,6 +195,9 @@ class SmartCard {
186195

187196
if (info.initializedCard) {
188197
logAppInfo(info)
198+
let cardName = try cardNameOrDefault(cmdSet: cmdSet)
199+
cardInfo["card-name"] = cardName
200+
189201
var isPaired = false
190202
var isAuthentic = false
191203
let instanceUID = bytesToHex(info.instanceUID)
@@ -262,29 +274,36 @@ class SmartCard {
262274

263275
func importKeys(channel: CardChannel, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
264276
let cmdSet = try authenticatedCommandSet(channel: channel, pin: pin)
277+
let info = cmdSet.info!
278+
let exportOption = info.appVersion < 0x0310 ? KeycardCommandSet.ExportOption.publicOnly : .extendedPublic
265279

266280
let encryptionKeyPair = try exportKey(cmdSet: cmdSet, path: .encryptionPath, makeCurrent: false, publicOnly: false)
267281
let masterPair = try exportKey(cmdSet: cmdSet, path: .masterPath, makeCurrent: false, publicOnly: true)
268-
let rootKeyPair = try exportKey(cmdSet: cmdSet, path: .rootPath, makeCurrent: false, publicOnly: true)
282+
let rootKeyPair = try exportKey(cmdSet: cmdSet, path: .rootPath, makeCurrent: false, exportOption: exportOption)
269283
let whisperKeyPair = try exportKey(cmdSet: cmdSet, path: .whisperPath, makeCurrent: false, publicOnly: false)
270-
let walletKeyPair = try exportKey(cmdSet: cmdSet, path: .walletPath, makeCurrent: false, publicOnly: true)
271284

272-
let info = cmdSet.info!
273-
274-
resolve([
285+
var keys = [
275286
"address": bytesToHex(masterPair.toEthereumAddress()),
276287
"public-key": bytesToHex(masterPair.publicKey),
277288
"wallet-root-address": bytesToHex(rootKeyPair.toEthereumAddress()),
278289
"wallet-root-public-key": bytesToHex(rootKeyPair.publicKey),
279-
"wallet-address": bytesToHex(walletKeyPair.toEthereumAddress()),
280-
"wallet-public-key": bytesToHex(walletKeyPair.publicKey),
281290
"whisper-address": bytesToHex(whisperKeyPair.toEthereumAddress()),
282291
"whisper-public-key": bytesToHex(whisperKeyPair.publicKey),
283292
"whisper-private-key": bytesToHex(whisperKeyPair.privateKey!),
284293
"encryption-public-key": bytesToHex(encryptionKeyPair.publicKey),
285294
"instance-uid": bytesToHex(info.instanceUID),
286295
"key-uid": bytesToHex(info.keyUID)
287-
])
296+
]
297+
298+
if rootKeyPair.isExtended {
299+
keys["wallet-root-chain-code"] = bytesToHex(rootKeyPair.chainCode!)
300+
} //else { (see note above)
301+
let walletKeyPair = try exportKey(cmdSet: cmdSet, path: .walletPath, makeCurrent: false, publicOnly: true)
302+
keys["wallet-address"] = bytesToHex(walletKeyPair.toEthereumAddress())
303+
keys["wallet-public-key"] = bytesToHex(walletKeyPair.publicKey)
304+
//}
305+
306+
resolve(keys)
288307
}
289308

290309
func getKeys(channel: CardChannel, pin: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
@@ -404,6 +423,19 @@ class SmartCard {
404423

405424
resolve(true)
406425
}
426+
427+
func getCardName(channel: CardChannel, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
428+
let cmdSet = KeycardCommandSet(cardChannel: channel)
429+
try cmdSet.select().checkOK()
430+
resolve(try cardNameOrDefault(cmdSet: cmdSet))
431+
}
432+
433+
func setCardName(channel: CardChannel, pin: String, name: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) throws -> Void {
434+
let cmdSet = try authenticatedCommandSet(channel: channel, pin: pin)
435+
let m = Metadata(name)
436+
try cmdSet.storeData(data: m.serialize(), type: Keycard.StoreDataP1.publicData.rawValue).checkOK()
437+
resolve(true)
438+
}
407439

408440
func randomPUK() -> String {
409441
return String(format: "%012ld", Int64.random(in: 0..<999999999999))
@@ -416,10 +448,27 @@ class SmartCard {
416448
}
417449

418450
func exportKey(cmdSet: KeycardCommandSet, path: DerivationPath, makeCurrent: Bool, publicOnly: Bool) throws -> BIP32KeyPair {
419-
let tlvRoot = try cmdSet.exportKey(path: path.rawValue, makeCurrent: makeCurrent, publicOnly: publicOnly).checkOK().data
451+
let option = publicOnly ? KeycardCommandSet.ExportOption.publicOnly : KeycardCommandSet.ExportOption.privateAndPublic
452+
return try exportKey(cmdSet: cmdSet, path: path, makeCurrent: makeCurrent, exportOption: option)
453+
}
454+
455+
func exportKey(cmdSet: KeycardCommandSet, path: DerivationPath, makeCurrent: Bool, exportOption: KeycardCommandSet.ExportOption) throws -> BIP32KeyPair {
456+
let tlvRoot = try cmdSet.exportKey(path: path.rawValue, makeCurrent: makeCurrent, exportOption: exportOption).checkOK().data
420457
os_log("Derived %@", path.rawValue)
421458
return try BIP32KeyPair(fromTLV: tlvRoot)
422459
}
460+
461+
func cardNameOrDefault(cmdSet: KeycardCommandSet) throws -> String {
462+
let data = try cmdSet.getData(type: Keycard.StoreDataP1.publicData.rawValue).checkOK().data
463+
464+
if data.count > 0 {
465+
do {
466+
return try Metadata.fromData(data).cardName
467+
} catch _ {}
468+
}
469+
470+
return ""
471+
}
423472

424473
func setPairings(newPairings: NSDictionary, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
425474
self.pairings.removeAll()

ios/StatusKeycard.m

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ @interface RCT_EXTERN_REMAP_MODULE(RNStatusKeycard, StatusKeycard, RCTEventEmitt
3333
RCT_EXTERN_METHOD(delete:(RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)
3434
RCT_EXTERN_METHOD(removeKey:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
3535
RCT_EXTERN_METHOD(removeKeyWithUnpair:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
36+
RCT_EXTERN_METHOD(getCardName:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
37+
RCT_EXTERN_METHOD(setCardName:(NSString *)pin name:(NSString *)name resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
3638
RCT_EXTERN_METHOD(unpairAndDelete:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
3739
RCT_EXTERN_METHOD(startNFC:(NSString *)prompt resolve:(RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)
3840
RCT_EXTERN_METHOD(stopNFC:(NSString *)err resolve:(RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)

0 commit comments

Comments
 (0)