Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,7 @@ jobs:
run: swift build --triple arm64-apple-ios18.0 --sdk "$(xcrun --sdk iphoneos --show-sdk-path)"
- name: Build for iOS simulator
run: swift build --triple arm64-apple-ios18.0-simulator --sdk "$(xcrun --sdk iphonesimulator --show-sdk-path)"
- name: Run tests on iOS simulator
run: |
DEVICE_ID=$(xcrun simctl list devices available --json | jq -r '[.devices | to_entries[] | select(.key | contains("iOS")) | .value[] | select(.name | startswith("iPhone"))][0].udid')
xcodebuild test -scheme IdCardLib -destination "id=$DEVICE_ID"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ timeline.xctimeline
playground.xcworkspace

.DS_Store

## Swift Package Manager
.swiftpm/
10 changes: 9 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ let package = Package(
.library(name: "IdCardLib", targets: ["nfclib"])
],
dependencies: [
.package(url: "https://github.com/leif-ibsen/BigInt.git", from: "1.15.0"),
.package(url: "https://github.com/leif-ibsen/BigInt", from: "1.15.0"),
.package(url: "https://github.com/leif-ibsen/SwiftECC", from: "5.1.0")
],
targets: [
Expand All @@ -21,6 +21,14 @@ let package = Package(
.product(name: "BigInt", package: "BigInt"),
.product(name: "SwiftECC", package: "SwiftECC")
]
),
.testTarget(
name: "nfclibTests",
dependencies: [
"nfclib",
.product(name: "BigInt", package: "BigInt"),
.product(name: "SwiftECC", package: "SwiftECC")
]
)
]
)
24 changes: 12 additions & 12 deletions Sources/nfclib/CardActions/Idemia.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,18 @@

import Foundation

private let ATR = Bytes(hex: "3B DB 96 00 80 B1 FE 45 1F 83 00 12 23 3F 53 65 49 44 0F 90 00 F1")
private let ATRv2 = Bytes(hex: "3B DC 96 00 80 B1 FE 45 1F 83 00 12 23 3F 54 65 49 44 32 0F 90 00 C3")
private let kAID = Bytes(hex: "A0 00 00 00 77 01 08 00 07 00 00 FE 00 00 01 00")
private let kAIDQSCD = Bytes(hex: "51 53 43 44 20 41 70 70 6C 69 63 61 74 69 6F 6E")
private let kAIDOberthur = Bytes(hex: "E8 28 BD 08 0F F2 50 4F 54 20 41 57 50")
private let kAID: Bytes = [
0xA0, 0x00, 0x00, 0x00, 0x77, 0x01, 0x08, 0x00,
0x07, 0x00, 0x00, 0xFE, 0x00, 0x00, 0x01, 0x00
]
private let kAIDQSCD: Bytes = [
0x51, 0x53, 0x43, 0x44, 0x20, 0x41, 0x70, 0x70,
0x6C, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6F, 0x6E
]
private let kAIDOberthur: Bytes = [
0xE8, 0x28, 0xBD, 0x08, 0x0F, 0xF2, 0x50, 0x4F,
0x54, 0x20, 0x41, 0x57, 0x50
]
private let AUTHKEY: UInt8 = 0x81
private let SIGNKEY: UInt8 = 0x9F

Expand All @@ -33,13 +40,6 @@ final class Idemia: CardCommandsInternal {
let reader: CardReader
let fillChar: UInt8 = 0xFF

required init?(reader: CardReader, atr: Bytes) {
guard atr == ATR || atr == ATRv2 else {
return nil
}
self.reader = reader
}

required init?(reader: CardReader, aid: Bytes) {
guard aid == kAID else {
return nil
Expand Down
28 changes: 9 additions & 19 deletions Sources/nfclib/CardActions/Thales.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,21 @@ extension CodeType {
}

final class Thales: CardCommandsInternal {
static private let ATR = Bytes(hex: "3B FF 96 00 00 80 31 FE 43 80 31 B8 53 65 49 44 64 B0 85 05 10 12 23 3F 1D")
static private let kAID = Bytes(hex: "A0 00 00 00 63 50 4B 43 53 2D 31 35")
static private let kAIDGlobal = Bytes(hex: "A0 00 00 00 18 10 02 03 00 00 00 00 00 00 00 01")
static private let kAID: Bytes = [
0xA0, 0x00, 0x00, 0x00, 0x63, 0x50, 0x4B, 0x43,
0x53, 0x2D, 0x31, 0x35
]
static private let kAIDGlobal: Bytes = [
0xA0, 0x00, 0x00, 0x00, 0x18, 0x10, 0x02, 0x03,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01
]
static private let AUTHKEY: UInt8 = 0x01
static private let SIGNKEY: UInt8 = 0x05

let canChangePUK: Bool = false
let reader: CardReader
let fillChar: UInt8 = 0x00

init?(reader: any CardReader, atr: Bytes) throws {
guard atr == Thales.ATR else {
return nil
}
self.reader = reader
}

required init?(reader: CardReader, aid: Bytes) {
guard aid == Thales.kAIDGlobal else {
return nil
Expand Down Expand Up @@ -139,9 +137,7 @@ final class Thales: CardCommandsInternal {
p2Byte: 0xA0,
data: TLV(
tag: 0x90,
value: Data(
hash
)
bytes: hash
).data
)
return try await reader.sendAPDU(ins: 0x2A, p1Byte: 0x9E, p2Byte: 0x9A, leByte: 0x00)
Expand All @@ -167,9 +163,3 @@ final class Thales: CardCommandsInternal {
return try await reader.sendAPDU(ins: 0x2A, p1Byte: 0x80, p2Byte: 0x86, data: [0x00] + hash, leByte: 0x00)
}
}

extension Bytes {
init(hex: String) {
self = hex.split(separator: " ").compactMap { UInt8($0, radix: 16) }
}
}
30 changes: 14 additions & 16 deletions Sources/nfclib/Extensions/DataProtocol+Extension.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,19 @@ extension DataProtocol where Self.Index == Int {

extension DataProtocol where Self.Index == Int, Self: MutableDataProtocol {
init?(hex: String) {
guard hex.count.isMultiple(of: 2) else { return nil }
let chars = hex.map { $0 }
let bytes = stride(from: 0, to: chars.count, by: 2)
.map { String(chars[$0]) + String(chars[$0 + 1]) }
.compactMap { UInt8($0, radix: 16) }
guard hex.count / bytes.count == 2 else { return nil }
self.init(bytes)
self.init()
reserveCapacity(hex.utf8.count / 2)
var high: UInt8?
for char in hex where char != " " {
guard let value = char.hexDigitValue.map(UInt8.init) else { return nil }
if let highValue = high {
append(highValue << 4 | value)
high = nil
} else {
high = value
}
}
guard high == nil else { return nil }
}

func addPadding() -> Self {
Expand All @@ -45,14 +51,6 @@ extension DataProtocol where Self.Index == Int, Self: MutableDataProtocol {
return self + padding
}

public static func ^ (xVal: Self, yVal: Self) -> Self {
var result = xVal
for index in 0..<result.count {
result[index] ^= yVal[index]
}
return result
}

static func ^ <D: Collection>(lhs: Self, rhs: D) -> Self where D.Element == Self.Element {
precondition(lhs.count == rhs.count, "XOR operands must have equal length")
var result = lhs
Expand All @@ -66,7 +64,7 @@ extension DataProtocol where Self.Index == Int, Self: MutableDataProtocol {
var index = endIndex
while index != startIndex {
formIndex(before: &index)
self[index] += 1
self[index] &+= 1
if self[index] != 0 {
break
}
Expand Down
20 changes: 2 additions & 18 deletions Sources/nfclib/Extensions/Logger+Extensions.swift
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
/*
* Copyright 2017 - 2025 Riigi Infosüsteemi Amet
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
// SPDX-FileCopyrightText: Estonian Information System Authority
// SPDX-License-Identifier: LGPL-2.1-or-later

import os

Expand Down
25 changes: 4 additions & 21 deletions Sources/nfclib/Utilities/AES.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ class AES {
}

func encrypt<T: DataType>(_ data: T) throws -> Bytes {
return try crypt(data: data, operation: kCCEncrypt)
try crypt(data: data, operation: kCCEncrypt)
}

func decrypt<T: DataType>(_ data: T) throws -> Bytes {
return try crypt(data: data, operation: kCCDecrypt)
try crypt(data: data, operation: kCCDecrypt)
}

private func crypt<T: DataType>(data: T, operation: Int) throws -> Bytes {
Expand Down Expand Up @@ -51,24 +51,7 @@ class AES {
}

public class CMAC {
static let RBytes: Bytes = [
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x00,
0x87
]
static let RBytes: Bytes = Bytes(repeating: 0x00, count: BlockSize - 1) + [0x87]
let cipher: AES.CBC
let k1Bytes: Bytes
let k2Bytes: Bytes
Expand All @@ -90,7 +73,7 @@ class AES {
mLast = Bytes(last).addPadding() ^ k2Bytes
}
} else {
mLast = Bytes().addPadding() ^ k1Bytes
mLast = Bytes().addPadding() ^ k2Bytes
}

var xVal = Bytes(repeating: 0x00, count: BlockSize)
Expand Down
74 changes: 74 additions & 0 deletions Tests/nfclibTests/AESTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: Estonian Information System Authority
// SPDX-License-Identifier: LGPL-2.1-or-later

import Testing
@testable import nfclib

// NIST SP 800-38A / 800-38B example plaintext
private let message = Bytes(hex: """
6bc1bee22e409f96e93d7e117393172a ae2d8a571e03ac9c9eb76fac45af8e51 \
30c81c46a35ce411e5fbc1191a0a52ef f69f2445df4f9b17ad2b417be66c3710
""")!
private let key128 = Bytes(hex: "2b7e151628aed2a6abf7158809cf4f3c")!
private let key256 = Bytes(hex: "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4")!

struct AESCBCTests {
// NIST SP 800-38A F.2.5 / F.2.6 CBC-AES256
let ivVal = Bytes(hex: "000102030405060708090a0b0c0d0e0f")!
let cipherText = Bytes(hex: """
f58c4c04d6e5f1ba779eabfb5f7bfbd6 9cfc4e967edb808d679f777bc6702c7d \
39f23369a9d9bacfa530e26304231461 b2eb05e2c39be9fcda6c19078c6a9d1b
""")!

@Test func encryptAES256() throws {
#expect(try AES.CBC(key: key256, ivVal: ivVal).encrypt(message) == cipherText)
}

@Test func decryptAES256() throws {
#expect(try AES.CBC(key: key256, ivVal: ivVal).decrypt(cipherText) == message)
}

@Test func rejectsPartialBlock() {
#expect(throws: IdCardInternalError.self) {
try AES.CBC(key: key256).encrypt(Bytes(repeating: 0, count: 15))
}
}
}

struct AESCMACTests {
// RFC 4493 section 2.3
@Test func subkeysAES128() throws {
let cmac = try AES.CMAC(key: key128)
#expect(cmac.k1Bytes == Bytes(hex: "fbeed618357133667c85e08f7236a8de")!)
#expect(cmac.k2Bytes == Bytes(hex: "f7ddac306ae266ccf90bc11ee46d513b")!)
}

// RFC 4493 section 4, examples 1-4
@Test(arguments: [
(0, "bb1d6929e95937287fa37d129b756746"),
(16, "070a16b46b4d4144f79bdd9dd04a287c"),
(40, "dfa66747de9ae63030ca32611497c827"),
(64, "51f0bebf7e3b9d92fc49741779363cfe")
])
func macAES128(length: Int, expected: String) throws {
let mac = try AES.CMAC(key: key128).authenticate(bytes: Bytes(message.prefix(length)), count: 16)
#expect(Bytes(mac) == Bytes(hex: expected)!)
}

// NIST SP 800-38B appendix D.3
@Test(arguments: [
(0, "028962f61b7bf89efc6b551f4667d983"),
(16, "28a7023f452e8f82bd4bf28d8c37c35c"),
(40, "aaf3d8f1de5640c232f5b169b9c911e6"),
(64, "e1992190549f6ed5696a2c056c315410")
])
func macAES256(length: Int, expected: String) throws {
let mac = try AES.CMAC(key: key256).authenticate(bytes: Bytes(message.prefix(length)), count: 16)
#expect(Bytes(mac) == Bytes(hex: expected)!)
}

@Test func defaultTruncationIs8Bytes() throws {
let mac = try AES.CMAC(key: key256).authenticate(bytes: message)
#expect(Bytes(mac) == Bytes(hex: "e1992190549f6ed5")!)
}
}
Loading
Loading