Skip to content

Commit

Permalink
Merge pull request #35 from status-im/feature/ios
Browse files Browse the repository at this point in the history
add iOS support
  • Loading branch information
bitgamma authored Feb 1, 2021
2 parents 6d95a0d + ad9cb78 commit 62421a6
Show file tree
Hide file tree
Showing 11 changed files with 1,023 additions and 5 deletions.
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
/example

#
# OSX
#
.DS_Store
Expand Down Expand Up @@ -44,7 +46,9 @@ buck-out/
\.buckd/
*.keystore

/lib

# VSCode + Java
android/.project
android/.settings
.vscode
.vscode
Original file line number Diff line number Diff line change
Expand Up @@ -432,4 +432,19 @@ public void run() {
}).start();
}

// These three methods below are a nop on Android since NFC is always listening and we have a custom UI. They are needed in iOS to show the NFC dialog
@ReactMethod
public void startNFC(String prompt, final Promise promise) {
promise.resolve(true);
}

@ReactMethod
public void stopNFC(String error, final Promise promise) {
promise.resolve(true);
}

@ReactMethod
public void setNFCMessage(String message, final Promise promise) {
promise.resolve(true);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ public static String randomToken(int length) {

for (int i = 0; i < length; i++) {
char[] src = (i % 2) == 0 ? possibleCharacters : possibleDigits;
int idx = random.nextInt(src.length - 1);
int idx = random.nextInt(src.length);
buffer.append(src[idx]);
}

Expand Down
3 changes: 1 addition & 2 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@

import { NativeModules } from 'react-native';

const { RNStatusKeycard } = NativeModules;

export default RNStatusKeycard;
export default RNStatusKeycard;
372 changes: 372 additions & 0 deletions ios/SmartCard.swift

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions ios/StatusKeycard-Bridging-Header.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>
36 changes: 36 additions & 0 deletions ios/StatusKeycard.m
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

@interface RCT_EXTERN_REMAP_MODULE(RNStatusKeycard, StatusKeycard, RCTEventEmitter)

RCT_EXTERN_METHOD(nfcIsSupported:(RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(nfcIsEnabled:(RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(openNfcSettings:(RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(init:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(pair:(NSString *)pairingPassword resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(generateMnemonic:(NSString *)pairing words:(NSString *)words resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(generateAndLoadKey:(NSString *)mnemonic pairing: (NSString *)pairing pin:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(saveMnemonic:(NSString *)mnemonic pairing: (NSString *)pairing pin:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(getApplicationInfo:(NSString *)pairingBase64 resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(deriveKey:(NSString *)path pairing: (NSString *)pairing pin:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(exportKey:(NSString *)pairing pin:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(exportKeyWithPath:(NSString *)pairing pin:(NSString *)pin path:(NSString *)path resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(getKeys:(NSString *)pairing pin:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(sign:(NSString *)pairing pin:(NSString *)pin hash:(NSString *)hash resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(signWithPath:(NSString *)pairing pin:(NSString *)pin path:(NSString *)path hash:(NSString *)hash resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(signPinless:(NSString *)hash resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(installApplet:(RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(installAppletAndInitCard:(String *)pin resolve: (RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(verifyPin:(NSString *)pairing pin:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(changePin:(NSString *)pairing currentPin:(NSString *)currentPin newPin:(NSString *)newPin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(unblockPin:(NSString *)pairing puk:(NSString *)puk newPin:(NSString *)newPin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(unpair:(NSString *)pairing pin:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(delete:(RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removeKey:(NSString *)pairing pin:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(removeKeyWithUnpair:(NSString *)pairing pin:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(unpairAndDelete:(NSString *)pairing pin:(NSString *)pin resolve:(RCTPromiseResolveBlock)resolve reject:(RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(startNFC:(NSString *)prompt resolve:(RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(stopNFC:(NSString *)err resolve:(RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)
RCT_EXTERN_METHOD(setNFCMessage:(NSString *)message resolve:(RCTPromiseResolveBlock)resolve reject: (RCTPromiseRejectBlock)reject)

@end
258 changes: 258 additions & 0 deletions ios/StatusKeycard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import Foundation
import Keycard
import UIKit
import os.log

@objc(StatusKeycard)
class StatusKeycard: RCTEventEmitter {
let smartCard = SmartCard()
var cardChannel: CardChannel? = nil
var nfcStartPrompt: String = "Hold your iPhone near a Status Keycard."

@available(iOS 13.0, *)
private(set) lazy var keycardController: KeycardController? = nil

@objc
func nfcIsSupported(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
if #available(iOS 13.0, *) {
resolve(KeycardController.isAvailable)
} else {
resolve(false)
}
}

@objc
func nfcIsEnabled(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
// On iOS NFC is always enabled (if available)
nfcIsSupported(resolve, reject: reject)
}

@objc
func openNfcSettings(_ resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
// NFC cannot be enabled/disabled
reject("E_KEYCARD", "Unsupported on iOS", nil)
}

@objc
func `init`(_ pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.initialize(channel: channel, pin: pin, resolve: resolve, reject: reject) }
}

@objc
func pair(_ pairingPassword: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.pair(channel: channel, pairingPassword: pairingPassword, resolve: resolve, reject: reject) }
}

@objc
func generateMnemonic(_ pairing: String, words: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.generateMnemonic(channel: channel, pairingBase64: pairing, words: words, resolve: resolve, reject: reject) }
}

@objc
func generateAndLoadKey(_ mnemonic: String, pairing: String, pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.generateAndLoadKey(channel: channel, mnemonic: mnemonic, pairingBase64: pairing, pin: pin, resolve: resolve, reject: reject) }
}

@objc
func saveMnemonic(_ mnemonic: String, pairing: String, pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.saveMnemonic(channel: channel, mnemonic: mnemonic, pairingBase64: pairing, pin: pin, resolve: resolve, reject: reject) }
}

@objc
func getApplicationInfo(_ pairingBase64: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.getApplicationInfo(channel: channel, pairingBase64: pairingBase64, resolve: resolve, reject: reject) }
}

@objc
func deriveKey(_ path: String, pairing: String, pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.deriveKey(channel: channel, path: path, pairingBase64: pairing, pin: pin, resolve: resolve, reject: reject) }
}

@objc
func exportKey(_ pairing: String, pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.exportKey(channel: channel, pairingBase64: pairing, pin: pin, resolve: resolve, reject: reject) }
}

@objc
func exportKeyWithPath(_ pairing: String, pin: String, path: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.exportKeyWithPath(channel: channel, pairingBase64: pairing, pin: pin, path: path, resolve: resolve, reject: reject) }
}

@objc
func getKeys(_ pairing: String, pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.getKeys(channel: channel, pairingBase64: pairing, pin: pin, resolve: resolve, reject: reject) }
}

@objc
func sign(_ pairing: String, pin: String, hash: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.sign(channel: channel, pairingBase64: pairing, pin: pin, message: hash, resolve: resolve, reject: reject) }
}

@objc
func signWithPath(_ pairing: String, pin: String, path: String, hash: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.signWithPath(channel: channel, pairingBase64: pairing, pin: pin, path: path, message: hash, resolve: resolve, reject: reject) }
}

@objc
func signPinless(_ hash: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.signPinless(channel: channel, message: hash, resolve: resolve, reject: reject) }
}

@objc
func installApplet(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
reject("E_KEYCARD", "Not implemented (unused)", nil)
}

@objc
func installAppletAndInitCard(_ pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
reject("E_KEYCARD", "Not implemented (unused)", nil)
}

@objc
func verifyPin(_ pairing: String, pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.verifyPin(channel: channel, pairingBase64: pairing, pin: pin, resolve: resolve, reject: reject) }
}

@objc
func changePin(_ pairing: String, currentPin: String, newPin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.changePin(channel: channel, pairingBase64: pairing, currentPin: currentPin, newPin: newPin, resolve: resolve, reject: reject) }
}

@objc
func unblockPin(_ pairing: String, puk: String, newPin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.unblockPin(channel: channel, pairingBase64: pairing, puk: puk, newPin: newPin, resolve: resolve, reject: reject) }
}

@objc
func unpair(_ pairing: String, pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.unpair(channel: channel, pairingBase64: pairing, pin: pin, resolve: resolve, reject: reject) }
}

@objc
func delete(_ resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
reject("E_KEYCARD", "Not implemented (unused)", nil)
}

@objc
func removeKey(_ pairing: String, pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.removeKey(channel: channel, pairingBase64: pairing, pin: pin, resolve: resolve, reject: reject) }
}

@objc
func removeKeyWithUnpair(_ pairing: String, pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
keycardInvokation(reject) { [unowned self] channel in try self.smartCard.removeKeyWithUnpair(channel: channel, pairingBase64: pairing, pin: pin, resolve: resolve, reject: reject) }
}

@objc
func unpairAndDelete(_ pairing: String, pin: String, resolve: @escaping RCTPromiseResolveBlock, reject: @escaping RCTPromiseRejectBlock) -> Void {
reject("E_KEYCARD", "Not implemented (unused)", nil)
}

@objc
func startNFC(_ prompt: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
if #available(iOS 13.0, *) {
if (keycardController == nil) {
self.keycardController = KeycardController(onConnect: { [unowned self] channel in
self.cardChannel = channel

let feedbackGenerator = UINotificationFeedbackGenerator()
feedbackGenerator.prepare()

DispatchQueue.main.async {
feedbackGenerator.notificationOccurred(.success)
}
self.sendEvent(withName: "keyCardOnConnected", body: nil)
self.keycardController?.setAlert("Connected. Don't move your card.")
os_log("[react-native-status-keycard] card connected")
}, onFailure: { [unowned self] error in
self.cardChannel = nil
self.keycardController = nil

os_log("[react-native-status-keycard] NFCError: %@", String(describing: error))

if type(of: error) is NSError.Type {
let nsError = error as NSError
if nsError.code == 200 && nsError.domain == "NFCError" {
self.sendEvent(withName: "keyCardOnNFCUserCancelled", body: nil)
} else if nsError.code == 201 && nsError.domain == "NFCError" {
self.sendEvent(withName: "keyCardOnNFCTimeout", body: nil)
}
}
})

self.nfcStartPrompt = prompt.isEmpty ? "Hold your iPhone near a Status Keycard." : prompt
keycardController?.start(alertMessage: self.nfcStartPrompt)
resolve(true)
} else {
reject("E_KEYCARD", "already started", nil)
}
} else {
reject("E_KEYCARD", "unavailable", nil)
}
}

@objc
func stopNFC(_ err: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
if #available(iOS 13.0, *) {
if (err.isEmpty) {
self.keycardController?.stop(alertMessage: "Success")
} else {
self.keycardController?.stop(errorMessage: err)
}
self.cardChannel = nil
self.keycardController = nil
resolve(true)
} else {
reject("E_KEYCARD", "unavailable", nil)
}
}

@objc
func setNFCMessage(_ message: String, resolve: RCTPromiseResolveBlock, reject: RCTPromiseRejectBlock) -> Void {
if #available(iOS 13.0, *) {
self.keycardController?.setAlert(message)
resolve(true)
} else {
reject("E_KEYCARD", "unavailable", nil)
}
}

override static func requiresMainQueueSetup() -> Bool {
return true
}

override func supportedEvents() -> [String]! {
return ["keyCardOnConnected", "keyCardOnDisconnected", "keyCardOnNFCEnabled", "keyCardOnNFCDisabled", "keyCardOnNFCTimeout", "keyCardOnNFCUserCancelled"]
}

func keycardInvokation(_ reject: @escaping RCTPromiseRejectBlock, body: @escaping (CardChannel) throws -> Void) {
if #available(iOS 13.0, *) {
if self.cardChannel != nil {
DispatchQueue.global().async { [unowned self] in
do {
try body(self.cardChannel!)
} catch {
var errMsg = ""

if type(of: error) is NSError.Type {
let nsError = error as NSError
errMsg = "\(nsError.domain):\(nsError.code)"
if nsError.code == 100 && nsError.domain == "NFCError" {
self.sendEvent(withName: "keyCardOnDisconnected", body: nil)
self.keycardController?.restartPolling()
self.keycardController?.setAlert(self.nfcStartPrompt)
}
} else {
errMsg = "\(error)"
}
reject("E_KEYCARD", errMsg, error)
}
}
} else {
reject("E_KEYCARD", "not connected", nil)
}
} else {
reject("E_KEYCARD", "unavailable", nil)
}
}
}
Loading

0 comments on commit 62421a6

Please sign in to comment.