TonNfcClientSwift
该库的开发是为了处理与 NFC TON Labs 安全卡的 iPhone 通信。它提供了一个有用的 API 来处理 NFC TON Labs 安全卡支持的所有功能(即 APDU 命令)。NFC TON Labs 安全卡的技术规范可以在以下地址找到 https://ton.surf/scard。
您必须拥有 iOS 版本 >= 13 和 iPhone 型号 >= 7。
安装
TonNfcClientSwift 通过 CocoaPods 提供。要安装它,将以下行添加到您的 Podfile 中:
pod 'TonNfcClientSwift'
然后从您的项目目录运行 pod install
。
为了让 NFC 功能正常工作,您还必须执行以下步骤。
-
转到“签名和功能”选项卡并添加功能“近距离通信标签读取”。
-
在 info.plist 中添加项目《隐私 - NFC 扫描使用说明》和值 测试 NFC。
-
在 info.plist 中添加项目《ISO7816 应用标识符用于 NFC 标签读取会话》并添加以下项:313132323333343435353636, A000000151000000。
-
在 info.plist 中添加项目《com.apple.developer.nfc.readersession.formats》并添加字符串项 TAG。
-
请确保您的项目部署版本为 iOS11+。否则,pod 安装会对此提出抗议。
注意:您无法使用模拟器进行 NFC 工作,您必须在 iPhone 上运行它,因此您还应设置开发团队。
示例应用
要在此库中运行示例项目,请先克隆仓库,然后从 Example 目录运行 pod install
。
简单示例
在TonNfcClientSwift API中,针对每一次NFC卡片操作都有一个函数。这些函数通过回调机制将结果输出给调用者。为了这个,我们定义了以下回调类型。
public typealias NfcResolver = ((Any) -> Void)
public typealias NfcRejecter = ((String, NSError) -> Void)
API中的任何函数都返回void,并且有两个参数的最后两个: resolve : @escaping NfcResolver, reject : @escaping NfcRejecter。它将后处理的卡片响应传给resolve,在发生任何异常的情况下,它将错误消息和错误对象传递给reject。因此,要使用该API,您必须定义自己的NfcRejecter和NfcResolver回调函数。
让我们看看来自类CardCoinManagerNfcApi的简单示例函数getMaxPinTries。它从卡片返回最大的PIN尝试次数。它具有以下签名。
public func getMaxPinTries(resolve : @escaping NfcResolver, reject : @escaping NfcRejecter)
为了让它工作,您需要按照以下步骤进行。
- 进行导入。
import TonNfcClientSwift
- 添加代码片段。
var cardCoinManagerNfcApi: CardCoinManagerNfcApi = CardCoinManagerNfcApi()
let resolve : NfcResolver = {(msg : Any) -> Void in
print("Caught msg : ")
print(msg)
}
let reject : NfcRejecter = {(errMsg : String, err : NSError) -> Void in
print("Error happened : " + errMsg)
}
cardCoinManagerNfcApi.getMaxPinTries(resolve: resolve, reject: reject)
运行应用程序,您将收到一个提示对话框以连接卡片。等待1-2秒以从卡片获取响应。检查您的Xcode控制台。您应该找到以下输出。
Caught msg :
{"message":"10","status":"ok"}
这是NFC卡片响应的JSON包装。
使用Promise
TonNfcClientSwift库使用了PromiseKit。因此,您也可以在项目中使用PromiseKit。它提供了以方便的方式制作NFC卡片操作链,避免回调地狱。所以让我们使用Promise重写上面的例子。
import TonNfcClientSwift
import PromiseKit
...
let cardCoinManagerNfcApi: CardCoinManagerNfcApi = CardCoinManagerNfcApi()
...
Promise<String> { promise in
cardCoinManagerNfcApi.getRemainingPinTries(
resolve: { msg in promise.fulfill(msg as! String) },
reject: { (errMsg : String, err : NSError) in promise.reject(err) }
)
}
.done{response in
print("Got PIN tries : " + response)
}
.catch{ error in
print("Error happened : " + error.localizedDescription)
}
下面您可以找到更多示例和iPhone NFC卡片操作链的详细说明。
关于响应格式更详细的内容
成功操作的示例
在卡片成功操作的情况下,TonNfcClientSwift库中的任何函数都会创建包含两个字段“message”和“status”的JSON字符串。其中“status”将包含“ok”。您将在“message”字段中找到预期的负载。因此,JSON可能看起来像这样。
{"message":"done","status":"ok"}
{"message":"generated","status":"ok"}
{"message":"HMac key to sign APDU data is generated","status":"ok"}
{"message":"980133A56A59F3A59F174FD457EB97BE0E3BAD59E271E291C1859C74C795A83368FD8C7405BC37E1C4146F4D175CF36421BF6AD2AFF4329F5A6C6D772247ED03","status":"ok"}
etc.
错误的场景
如果发生了一些错误,TonNfcClientSwift 库的函数会产生包含特定格式 JSON 字符串的错误消息。JSON 结构取决于错误类别。存在两种主要的错误类别。
小程序(卡片)错误
这是小程序(安装在卡上)抛出某些错误状态字(SW)的情况。因此 Swift 代码只是捕获它并将其丢弃。下面是一个示例错误 JSON。
{
"message":"Incorrect PIN (from Ton wallet applet).",
"status":"fail",
"errorCode":"6F07",
"errorTypeId":0,
"errorType":"Applet fail: card operation error",
"cardInstruction":"VERIFY_PIN",
"apdu":"B0 A2 00 00 44 35353538EA579CD62F072B82DA55E9C780FCD0610F88F3FA1DD0858FEC1BB55D01A884738A94113A2D8852AB7B18FFCB9424B66F952A665BF737BEB79F216EEFC3A2EE37 FFFFFFFF "
}
这里
-
errorCode — 卡(小程序)产生的错误状态字(SW)
-
cardInstruction — 失败的 APDU 命令的标题
-
errorTypeId — 错误类型 id(在这里总是为零)
-
errorType — 错误类型的描述
-
message — 包含卡片抛出的 errorCode 相应的错误消息
-
apdu — 失败的 APDU 命令的全文,以十六进制格式表示
Swift 错误
这是 Swift 代码内部发生错误的情况。基本示例:NFC 连接问题或不正确的输入数据格式,这些数据传递到 TonNfcClientSwift 库的外部世界。下面是一个示例错误 JSON。
{
"errorType": "Native code fail: incorrect format of input data",
"errorTypeId": "3",
"errorCode": "30006",
"message": "Pin must be a numeric string of length 4.",
"status": "fail"
}
你可以在这份文档中找到该库可以抛出的全部 JSON 错误消息的完整列表(及其完整分类)。
字符串格式
传递给 TonNfcClientSwift 库的大多数输入数据是以大于 0 的偶数长度的十六进制字符串表示的。这些十六进制字符串在库内部自然转换为字节数组,如:"0A0A" → [10, 10] 作为 [UInt8]。
此外,卡片产生的并包含在 JSON 响应中的负载通常也是以大于 0 的偶数长度的十六进制字符串表示的。例如,这是 getPublicKey 函数返回 ed25519 公钥的响应。
{"message":"B81F0E0E07316DAB6C320ECC6BF3DBA48A70101C5251CC31B1D8F831B36E9F2A","status":"ok"}
这里 B81F0E0E07316DAB6C320ECC6BF3DBA48A70101C5251CC31B1D8F831B36E9F2A 是长 32 字节的 ed25519 公钥,以十六进制格式表示。
使用卡测试工作
在准备好应用之后,您可能需要在您的iPhone上运行它。然后您需要建立NFC连接。为此,您应该从TonNfcClientSwift API中调用必要的函数(如getMaxPinTries)。它将启动NFC会话,您将得到连接卡的邀请。
为了建立连接,将卡靠近iPhone顶部(相机附近的字段)尽可能接近。之后,iPhone会将APDU命令发送到卡。在上面的例子中,它发送了请求getMaxPinTries。为了保持连接,您不能移动卡和iPhone,并且它们应该有物理接触。等待直到您看到以下画面。
检查您的Xcode控制台。您一定要找到以下输出
Start card operation: getMaxPinTries
Nfc session is active
Nfc Tag is connected.
===============================================================
===============================================================
>>> Send apdu 00 A4 04 00 a000000151000000FFFFFFFF
(SELECT_COIN_MANAGER)
SW1-SW2: 90, 00
APDU Response: 6f5c8408a000000151000000a550734a06072a864886fc6b01600c060a2a864886fc6b02020101630906072a864886fc6b03640b06092a864886fc6b040255650b06092b8510864864020103660c060a2b060104012a026e01029f6501ff
===============================================================
===============================================================
===============================================================
>>> Send apdu 80 CB 80 00 dfff028103100
(GET_PIN_TLT)
SW1-SW2: 90, 00
APDU Response: 0a
===============================================================
{
"message" : "10",
"status" : "ok"
}
在这里,您可以看到以原始格式发送到卡的APDU命令及其响应的日志。最后还有一个最终的封装响应。
卡激活
当用户第一次获得NFC TON Labs安全卡时,卡上的应用小件处于特殊状态。应用小件的主要功能目前已被锁定。应用小件正在等待用户身份验证。为了通过身份验证,用户必须拥有三个秘密的十六进制字符串 authenticationPassword, commonSecret, initialVector。元组 (authenticationPassword, commonSecret, initialVector) 被称为 卡激活数据。用户将(使用debots)从为其安全卡部署的跟踪智能合约中获取其激活数据。
在这一步骤中,不仅是卡正在等待用户身份验证。用户还通过验证一些散列来验证卡。
注意: 卡上打印的序列号(SN)与激活数据之间存在双射。
有关卡激活及其相关工作流程的详细信息,请这里查看。
现在,让我们假设用户以某种方式从debots(稍后将提供与debots合作的详细资料)将激活数据放入其应用程序中。然后为了激活卡,他可以使用以下示例代码片段。
let DEFAULT_PIN = "5555"
let SERIAL_NUMBER = "504394802433901126813236"
let COMMON_SECRET = "7256EFE7A77AFC7E9088266EF27A93CB01CD9432E0DB66D600745D506EE04AC4"
let IV = "1A550F4B413D0E971C28293F9183EA8A"
let PASSWORD = "F4B072E1DF2DB7CF6CD0CD681EC5CD2D071458D278E6546763CBB4860F8082FE14418C8A8A55E2106CBC6CB1174F4BA6D827A26A2D205F99B7E00401DA4C15ACC943274B92258114B5E11C16DA64484034F93771547FBE60DA70E273E6BD64F8A4201A9913B386BCA55B6678CFD7E7E68A646A7543E9E439DD5B60B9615079FE"
let cardCoinManagerNfcApi: CardCoinManagerNfcApi = CardCoinManagerNfcApi()
let cardActivationApi : CardActivationNfcApi = CardActivationNfcApi()
Promise<String> { promise in
cardCoinManagerNfcApi.getRootKeyStatus(
resolve: { msg in promise.fulfill(msg as! String) },
reject: { (errMsg : String, err : NSError) in promise.reject(err) }
)
}
.then{(response : String) -> Promise<String> in
print("Response from getRootKeyStatus : " + response)
let message = try self.extractMessage(jsonStr : response)
if (message == "generated") {
return Promise<String> { promise in promise.fulfill("Seed exists already")}
}
sleep(5)
return Promise<String> { promise in
self.cardCoinManagerNfcApi.generateSeed(
pin : self.DEFAULT_PIN,
resolve: { msg in promise.fulfill(msg as! String) },
reject: { (errMsg : String, err : NSError) in promise.reject(err) }
)
}
}
.then{(response : String) -> Promise<String> in
print("Response from generateSeed : " + response)
sleep(5)
return Promise<String> { promise in
self.cardActivationApi.getTonAppletState(
resolve: { msg in promise.fulfill(msg as! String) },
reject: { (errMsg : String, err : NSError) in promise.reject(err) }
)
}
}
.then{(response : String) -> Promise<String> in
print("Response from getTonAppletState : " + response)
let message = try self.extractMessage(jsonStr : response)
if (message != "TonWalletApplet waits two-factor authorization.") {
throw "Incorrect applet state : " + message
}
sleep(5)
return Promise<String> { promise in
self.cardActivationApi.getHashOfEncryptedCommonSecret(
resolve: { msg in promise.fulfill(msg as! String) },
reject: { (errMsg : String, err : NSError) in promise.reject(err) }
)
}
}
.then{(response : String) -> Promise<String> in
print("Response from getHashOfEncryptedCommonSecret : " + response)
//check hashOfEncryptedCommonSecret
sleep(5)
return Promise<String> { promise in
self.cardActivationApi.getHashOfEncryptedPassword(
resolve: { msg in promise.fulfill(msg as! String) },
reject: { (errMsg : String, err : NSError) in promise.reject(err) }
)
}
}
.then{(response : String) -> Promise<String> in
print("Response from getHashOfEncryptedPassword : " + response)
//check hashOfEncryptedPassword
sleep(5)
return Promise<String> { promise in
self.cardActivationApi.turnOnWallet(
newPin: self.DEFAULT_PIN, authenticationPassword: self.PASSWORD, commonSecret: self.COMMON_SECRET, initialVector: self.IV,
resolve: { msg in promise.fulfill(msg as! String) },
reject: { (errMsg : String, err : NSError) in promise.reject(err) }
)
}
}
.done{response in
print("Response from getTonAppletState : " + response)
let message = try self.extractMessage(jsonStr : response)
if (message != "TonWalletApplet is personalized.") {
throw "Applet state is not personalized. Incorrect applet state : " + message
}
}
.catch{ error in
print(" !Error happened : " + error.localizedDescription)
}
这里有一系列使用承诺构建的NFC卡操作。为了执行每个卡操作,您必须重新连接卡。每次您都会得到邀请对话框来建立连接(请参阅之前的测试用卡工作部分)。iPhone在使用NFC时有一些特点。NFC会话完成之后,可能无法立即建立新的会话(至少对于iPhone 7和8来说是这样的)。因此,如果您编写以下代码,您可能会得到错误。
cardCoinManagerNfcApi.getRootKeyStatus(resolve: resolve, reject: reject)
cardCoinManagerNfcApi.generateSeed(pin: DEFAULT_PIN, resolve: resolve, reject: reject)
在这里,第一次调用getRootKeyStatus 将会成功,但尝试立即调用 generateSeed 将会抛出错误 "系统资源不可用"。我们需要在新NFC会话开始之前进行短暂的延迟(大约3-5秒)。因此,这两个调用必须分离,例如,通过调用 sleep 函数。这就是为什么在演示卡激活的代码片段中,在每次API调用之前都有一个字符串 sleep(5)。
关于小工具状态和提供的功能
安装到NFC TON Labs安全卡上的小工具可能处于以下状态(模式)之一
- TonWalletApplet 等待两因素认证。
- TonWalletApplet 已个性化。
- TonWalletApplet 已被阻止。
- TonWalletApplet 已个性化并且等待从密钥链中删除密钥完成。
状态转换的一些细节
- 当用户首次拿到卡片时,小工具必须处于1状态(参见上一节)。
- 如果用户尝试传入错误的激活数据超过20次,那么小工具状态将切换到3状态。这是不可逆的。在这个状态下,小工具的所有功能都被阻止,只能调用getTonAppletState和getSerialNumber(详情请参见以下节的完整功能列表)。
- 在正确激活之后(≤20次尝试通过激活数据)小工具进入状态2。并且在此之后,不能返回到状态1。状态1变得不可到达。在状态2,主要的小工具功能可用。
- 如果用户开始从卡片密钥链中删除密钥的操作,那么小工具将切换到状态4。并且它将保持在该状态,直到当前删除操作完成。删除操作正确完成后,小工具返回到状态2。返回状态2的另一种方法是调用resetKeychain函数(详情请参见以下)。
- 如果小工具连续20次HMAC SHA256签名验证失败,状态2和4的小工具可能被切换到状态3(更多详情请参见以下)。
NFC TON Labs安全卡提供的功能可以分为几组。
- 卡片激活模块(在状态1可用)。
- 提供ed22519签名的加密模块(在状态2,4可用)。
- 维护恢复数据的模块(在状态2,4可用)。
- 密钥链模块(在状态2,4可用)。
- 提供一些辅助功能的CoinManager模块(在任何状态都可用)。
防止中间人攻击
我们通过HMAC SHA256签名保护了最关键的卡片操作(APDU命令)免受中间人攻击。在这种情况下,APDU的数据字段通过由卡片生成的32字节sault扩展,最终的字节数组被签名。生成的签名被添加到APDU数据的末尾,即其数据字段的结构为:负载 || sault || 签名(负载 || sault)。当卡片接收到此类APDU时,首先验证sault和签名。
HMAC SHA256的秘密密钥基于卡片激活数据生成(参见上文)。此密钥使用账户名“hmac_key_alias_SN”存储到iOS密钥链中(SN由卡片上打印的序列号替换)。该密钥由应用程序用于签署APDU命令数据字段。通常,在应用程序(调用cardActivationApi.turnOnWallet)正确激活卡片后,此密钥生成并存储到密钥链中。因此不需要额外的代码。
另一种情况是可能的。假设你之前激活了这张卡。之后,你重新安装了与NFC TON Labs安全卡协作的应用程序,或者你开始使用新的iPhone。那么iPhone可能没有签名的APDU命令的密钥(如果iCloud没有正确设置以保存来自钥匙箱的所有数据)。因此,如果密钥丢失了,你必须重新创建它。
createKeyForHmac(authenticationPassword : String, commonSecret : String, serialNumber : String, resolve : @escaping NfcResolver, reject : @escaping NfcRejecter)
你可以与多个NFC TON Labs安全卡一起工作。在这种情况下,你的iOS钥匙箱中有一堆密钥。每个密钥都有相应的SN标记。你可以获取你拥有钥匙的序列号列表。
使用HMAC SHA256保护的运算列表
- verifyPin, signForDefaultHdPath, sign(见以下章节)
- 与卡钥匙箱相关的所有功能
请求ED25519签名
NFC TON Labs安全卡提供的基本功能是Ed25519签名。你可以请求公钥,并为某些消息请求签名。
注意:signForDefaultHdPath、sign等函数受到HMAC SHA256签名的保护(见上一节)。但它们也有由PIN代码提供的额外保护。你有10次输入PIN的机会,如果10次失败,你将无法使用现有的种子(ed25519的密钥)。唯一解锁这些函数的方法是重置种子(见resetWallet函数)并生成新的种子(见generateSeed)。在重置种子后,PIN也将重置为默认值5555。
let hdIndex = "65"
Promise<String> { promise in
cardCryptoNfcApi.getPublicKey(hdIndex: hdIndex,
resolve: { msg in promise.fulfill(msg as! String) },
reject: { (errMsg : String, err : NSError) in promise.reject(err) }
)
}
.done{response in
print("Got public key : " + response)
}
.catch{ error in
print("Error happened : " + error.localizedDescription)
}
let hdIndex = "65"
let msg = "A456"
let pin = "5555"
Promise<String> { promise in
cardCryptoNfcApi.createKeyForHmac(authenticationPassword: PASSWORD, commonSecret: COMMON_SECRET, serialNumber: SERIAL_NUMBER,
resolve: { msg in promise.fulfill(msg as! String) },
reject: { (errMsg : String, err : NSError) in promise.reject(err) }
)
}
.then{(response : String) -> Promise<String> in
print("Response from createKeyForHmac : " + response)
return Promise<String> { promise in
self.cardCryptoNfcApi.verifyPinAndSign(data: msg, hdIndex: hdIndex, pin: pin,
resolve: { msg in promise.fulfill(msg as! String) },
reject: { (errMsg : String, err : NSError) in promise.reject(err) }
)
}
}
.done{response in
print("Got signature : " + response)
}
.catch{ error in
print("Error happened : " + error.localizedDescription)
}
卡片钥匙箱
在NFC TON Labs安全卡内部,我们实现了一个小型的、灵活的独立钥匙箱。它允许存储一些用户的密钥和秘密。密钥的最大数量是1023,密钥的最大大小是8192字节,可用的总存储量是32767字节。
每个密钥都有一个唯一的ID。这是基于卡片激活数据创建的密钥的HMAC SHA256签名。因此,ID是一个长度为64的十六进制字符串。
以下片段演示了与钥匙箱的交互。我们添加一个密钥,然后从卡片中检索它。然后我们用相同的密钥替换它。最后,我们删除这个密钥。
//the snippet is going to be hear soon
恢复模块
本模块用于存储/维护以下恢复服务的相关数据:多签名钱包地址(长度为64位的十六进制字符串)、TON Labs Surf的公钥(长度为64位的十六进制字符串)以及卡片激活数据的一部分:认证密码(长度为256位的十六进制字符串)、共同密钥(长度为64位的十六进制字符串)。这些数据可以在用户遗失装有Surf应用的iPhone及其Surf账户的助记词时,用于恢复访问多签名钱包。关于恢复服务的更多详细信息请在此处查看。
以下是一个示例代码片段,用于演示恢复数据结构和将其添加到NFC TON Labs安全卡的方式。
//the snippet is going to be hear soon
完整的函数列表
您可以在此处找到该库提供的所有函数的完整列表。
辅助类
- 在TonWalletConstants中可以找到所有必须的常量清单。
- 类ByteArrayAndHexHelper提供了处理字节数组、字节�数组的十六进制表示和整数的函数,用于简化与主API的工作。
作者
alinaT95, [email protected]
许可证
TonNfcClientSwift遵循MIT许可证。有关更多信息,请查看LICENSE文件。