Initial implementation of using YubiKey for decryption (#533)
This commit is contained in:
parent
13804b79e6
commit
955e50c3d3
23 changed files with 606 additions and 118 deletions
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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") }
|
||||
|
|
|
|||
54
passKit/Helpers/YubiKeyAPDU.swift
Normal file
54
passKit/Helpers/YubiKeyAPDU.swift
Normal 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))!
|
||||
}
|
||||
}
|
||||
63
passKit/Helpers/YubiKeyConnection.swift
Normal file
63
passKit/Helpers/YubiKeyConnection.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
55
passKit/Protocols/AlertPresenting.swift
Normal file
55
passKit/Protocols/AlertPresenting.swift
Normal 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)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue