Initial implementation of using YubiKey for decryption (#533)

This commit is contained in:
Mingshen Sun 2022-01-09 21:38:39 -08:00 committed by GitHub
parent 13804b79e6
commit 955e50c3d3
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 606 additions and 118 deletions

View file

@ -14,4 +14,10 @@ public extension UIAlertController {
alert.addAction(UIAlertAction.cancel())
return alert
}
class func showErrorAlert(title: String, message: String, completion: ((UIAlertAction) -> Void)? = nil) -> UIAlertController {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
alert.addAction(UIAlertAction.ok(handler: completion))
return alert
}
}

View file

@ -6,6 +6,8 @@
// Copyright © 2017 Bob Sun. All rights reserved.
//
import Foundation
public enum AppError: Error, Equatable {
case repositoryNotSet
case repositoryRemoteBranchNotFound(branchName: String)
@ -18,13 +20,31 @@ public enum AppError: Error, Equatable {
case gitPushNotSuccessful
case pgpPublicKeyNotFound(keyID: String)
case pgpPrivateKeyNotFound(keyID: String)
case yubiKey(YubiKeyError)
case passwordFileNotFound(path: String)
case keyExpiredOrIncompatible
case wrongPassphrase
case wrongPasswordFilename
case decryption
case encryption
case encoding
case unknown
case other(message: String)
}
public enum YubiKeyError: Error, Equatable {
case connection(message: String)
case selectApplication(message: String)
case verify(message: String)
case decipher(message: String)
}
extension YubiKeyError: LocalizedError {
public var errorDescription: String? {
switch self {
case let .connection(message), let .decipher(message), let .selectApplication(message), let .verify(message):
return message
}
}
}
extension AppError: LocalizedError {
@ -36,6 +56,8 @@ extension AppError: LocalizedError {
return localizationKey.localize(name)
case let .pgpPrivateKeyNotFound(keyID), let .pgpPublicKeyNotFound(keyID):
return localizationKey.localize(keyID)
case let .other(message):
return message.localize()
default:
return localizationKey.localize()
}

View file

@ -26,6 +26,7 @@ public extension DefaultsKeys {
var pgpKeySource: DefaultsKey<KeySource?> { .init("pgpKeySource") }
var pgpPublicKeyURL: DefaultsKey<URL?> { .init("pgpPublicKeyURL") }
var pgpPrivateKeyURL: DefaultsKey<URL?> { .init("pgpPrivateKeyURL") }
var isYubiKeyEnabled: DefaultsKey<Bool> { .init("isYubiKeyEnabled", defaultValue: false) }
// Keep them for legacy reasons.
var pgpPublicKeyArmor: DefaultsKey<String?> { .init("pgpPublicKeyArmor") }

View file

@ -0,0 +1,54 @@
//
// YubiKeyAPDU.swift
// passKit
//
// Copyright © 2022 Bob Sun. All rights reserved.
//
import YubiKit
public enum YubiKeyAPDU {
public static func selectOpenPGPApplication() -> YKFSelectApplicationAPDU {
let selectOpenPGPAPDU = YKFSelectApplicationAPDU(data: Data([0xD2, 0x76, 0x00, 0x01, 0x24, 0x01]))!
return selectOpenPGPAPDU
}
public static func verify(password: String) -> YKFAPDU {
let pw1: [UInt8] = Array(password.utf8)
var apdu: [UInt8] = []
apdu += [0x00] // CLA
apdu += [0x20] // INS: VERIFY
apdu += [0x00] // P1
apdu += [0x82] // P2: PW1
apdu += withUnsafeBytes(of: UInt8(pw1.count).bigEndian, Array.init)
apdu += pw1
let verifyApdu = YKFAPDU(data: Data(apdu))!
return verifyApdu
}
public static func decipher(data: Data) -> YKFAPDU {
var apdu: [UInt8] = []
apdu += [0x00] // CLA
apdu += [0x2A, 0x80, 0x86] // INS, P1, P2: PSO.DECIPHER
// Lc, An extended Lc field consists of three bytes:
// one byte set to '00' followed by two bytes not set to '0000' (1 to 65535 dec.).
apdu += [0x00] + withUnsafeBytes(of: UInt16(data.count + 1).bigEndian, Array.init)
// Padding indicator byte (00) for RSA or (02) for AES followed by cryptogram Cipher DO 'A6' for ECDH
apdu += [0x00]
apdu += data
apdu += [0x02, 0x00]
let decipherApdu = YKFAPDU(data: Data(apdu))!
return decipherApdu
}
public static func get_application_related_data() -> YKFAPDU {
var apdu: [UInt8] = []
apdu += [0x00] // CLA
apdu += [0xCA] // INS: GET DATA
apdu += [0x00]
apdu += [0x6E] // P2: application related data
apdu += [0x00]
return YKFAPDU(data: Data(apdu))!
}
}

View file

@ -0,0 +1,63 @@
//
// YubiKeyConnection.swift
// passKit
//
// Copyright © 2022 Bob Sun. All rights reserved.
//
import Foundation
import YubiKit
public class YubiKeyConnection: NSObject {
public static let shared = YubiKeyConnection()
var accessoryConnection: YKFAccessoryConnection?
var nfcConnection: YKFNFCConnection?
var connectionCallback: ((_ connection: YKFConnectionProtocol) -> Void)?
var cancellationCallback: ((_ error: Error) -> Void)?
override init() {
super.init()
YubiKitManager.shared.delegate = self
YubiKitManager.shared.startAccessoryConnection()
}
public func connection(cancellation: @escaping (_ error: Error) -> Void, completion: @escaping (_ connection: YKFConnectionProtocol) -> Void) {
if let connection = accessoryConnection {
completion(connection)
} else {
connectionCallback = completion
if #available(iOSApplicationExtension 13.0, *) {
YubiKitManager.shared.startNFCConnection()
}
}
cancellationCallback = cancellation
}
}
extension YubiKeyConnection: YKFManagerDelegate {
public func didConnectNFC(_ connection: YKFNFCConnection) {
nfcConnection = connection
if let callback = connectionCallback {
callback(connection)
}
}
public func didDisconnectNFC(_: YKFNFCConnection, error _: Error?) {
nfcConnection = nil
}
public func didConnectAccessory(_ connection: YKFAccessoryConnection) {
accessoryConnection = connection
}
public func didDisconnectAccessory(_: YKFAccessoryConnection, error _: Error?) {
accessoryConnection = nil
}
public func didFailConnectingNFC(_ error: Error) {
if let callback = cancellationCallback {
callback(error)
}
}
}

View file

@ -39,7 +39,7 @@ public extension PasswordEntity {
if let path = getPath().stringByAddingPercentEncodingForRFC3986(), let url = URL(string: path) {
return url
}
throw AppError.unknown
throw AppError.other(message: "cannot decode URL")
}
// XXX: define some getters to get core data, we need to consider

View file

@ -0,0 +1,55 @@
//
// AlertPresenting.swift
// pass
//
// Copyright © 2022 Bob Sun. All rights reserved.
//
import UIKit
public typealias AlertAction = (UIAlertAction) -> Void
public protocol AlertPresenting {
func presentAlert(title: String, message: String)
func presentFailureAlert(title: String?, message: String, action: AlertAction?)
func presentAlertWithAction(title: String, message: String, action: AlertAction?)
}
public extension AlertPresenting where Self: UIViewController {
func presentAlert(title: String, message: String) {
presentAlert(
title: title,
message: message,
actions: [UIAlertAction(title: "OK", style: .cancel, handler: nil)]
)
}
// swiftlint:disable function_default_parameter_at_end
func presentFailureAlert(title: String? = nil, message: String, action: AlertAction? = nil) {
let title = title ?? "Error"
presentAlert(
title: title,
message: message,
actions: [UIAlertAction(title: "OK", style: .cancel, handler: action)]
)
}
func presentAlertWithAction(title: String, message: String, action: AlertAction?) {
presentAlert(
title: title,
message: message,
actions: [
UIAlertAction(title: "Yes", style: .default, handler: action),
UIAlertAction(title: "No", style: .cancel, handler: nil),
]
)
}
private func presentAlert(title: String, message: String, actions: [UIAlertAction] = []) {
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
actions.forEach { action in
alertController.addAction(action)
}
present(alertController, animated: true, completion: nil)
}
}