TonNfcClientSwift 1.2.1

TonNfcClientSwift 1.2.1

AlinaSnakeAlinaTONLabs 维护。



  • tonlabs

TonNfcClientSwift

CI Status Version License Platform

该库的开发是为了处理与 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,您必须定义自己的NfcRejecterNfcResolver回调函数。

让我们看看来自类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安全卡上的小工具可能处于以下状态(模式)之一

  1. TonWalletApplet 等待两因素认证。
  2. TonWalletApplet 已个性化。
  3. TonWalletApplet 已被阻止。
  4. 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文件。