Use remote authentication, add key display screen
This commit is contained in:
parent
921b9237f7
commit
2a8833ff20
@ -20,8 +20,11 @@
|
||||
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77327FF95920011CFD2 /* DeviceResponse.swift */; };
|
||||
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; };
|
||||
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; };
|
||||
E28DED2D281E840B00259690 /* KeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2C281E840B00259690 /* KeyView.swift */; };
|
||||
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2E281E8A0500259690 /* SingleKeyView.swift */; };
|
||||
E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */; };
|
||||
E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */; };
|
||||
E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */; };
|
||||
/* End PBXBuildFile section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -38,8 +41,11 @@
|
||||
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
||||
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceResponse.swift; sourceTree = "<group>"; };
|
||||
E24EE77827FF95E00011CFD2 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
|
||||
E28DED2C281E840B00259690 /* KeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyView.swift; sourceTree = "<group>"; };
|
||||
E28DED2E281E8A0500259690 /* SingleKeyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleKeyView.swift; sourceTree = "<group>"; };
|
||||
E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RouteAPI.swift; sourceTree = "<group>"; };
|
||||
E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UInt32+Extensions.swift"; sourceTree = "<group>"; };
|
||||
E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMessage.swift; sourceTree = "<group>"; };
|
||||
/* End PBXFileReference section */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -76,11 +82,12 @@
|
||||
E2C5C1D92806FE4A00769EF6 /* API */,
|
||||
884A45B6279F48C100D6E650 /* SesameApp.swift */,
|
||||
884A45B8279F48C100D6E650 /* ContentView.swift */,
|
||||
E28DED2C281E840B00259690 /* KeyView.swift */,
|
||||
E28DED2E281E8A0500259690 /* SingleKeyView.swift */,
|
||||
884A45CC27A465F500D6E650 /* Client.swift */,
|
||||
884A45C827A43D7900D6E650 /* ClientState.swift */,
|
||||
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */,
|
||||
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */,
|
||||
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */,
|
||||
884A45BA279F48C300D6E650 /* Assets.xcassets */,
|
||||
884A45BC279F48C300D6E650 /* Preview Content */,
|
||||
);
|
||||
@ -98,10 +105,12 @@
|
||||
E2C5C1D92806FE4A00769EF6 /* API */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */,
|
||||
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */,
|
||||
E24EE77827FF95E00011CFD2 /* Message.swift */,
|
||||
884A45CE27A5402D00D6E650 /* MessageResult.swift */,
|
||||
E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */,
|
||||
E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */,
|
||||
E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */,
|
||||
);
|
||||
path = API;
|
||||
@ -185,6 +194,7 @@
|
||||
files = (
|
||||
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */,
|
||||
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */,
|
||||
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */,
|
||||
E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */,
|
||||
E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */,
|
||||
884A45CD27A465F500D6E650 /* Client.swift in Sources */,
|
||||
@ -193,8 +203,10 @@
|
||||
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */,
|
||||
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */,
|
||||
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */,
|
||||
E28DED2D281E840B00259690 /* KeyView.swift in Sources */,
|
||||
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */,
|
||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
||||
E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
Binary file not shown.
@ -16,6 +16,11 @@ struct DeviceResponse {
|
||||
.init(event: .deviceNotConnected)
|
||||
}
|
||||
|
||||
/// Shorthand property for a connected event.
|
||||
static var deviceConnected: DeviceResponse {
|
||||
.init(event: .deviceConnected)
|
||||
}
|
||||
|
||||
/// Shorthand property for an unexpected socket event.
|
||||
static var unexpectedSocketEvent: DeviceResponse {
|
||||
.init(event: .unexpectedSocketEvent)
|
||||
|
@ -101,6 +101,10 @@ extension Message {
|
||||
mac + content.encoded
|
||||
}
|
||||
|
||||
var bytes: [UInt8] {
|
||||
Array(encoded)
|
||||
}
|
||||
|
||||
/**
|
||||
Create a message from received bytes.
|
||||
- Parameter data: The sequence of bytes
|
||||
|
@ -38,6 +38,9 @@ enum MessageResult: UInt8 {
|
||||
|
||||
/// Another message is being processed by the device
|
||||
case operationInProgress = 14
|
||||
|
||||
/// The device is connected
|
||||
case deviceConnected = 15
|
||||
}
|
||||
|
||||
extension MessageResult: CustomStringConvertible {
|
||||
@ -66,6 +69,8 @@ extension MessageResult: CustomStringConvertible {
|
||||
return "The device did not respond"
|
||||
case .operationInProgress:
|
||||
return "Another operation is in progress"
|
||||
case .deviceConnected:
|
||||
return "The device is connected"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
51
Sesame/API/ServerMessage.swift
Normal file
51
Sesame/API/ServerMessage.swift
Normal file
@ -0,0 +1,51 @@
|
||||
import Foundation
|
||||
import NIOCore
|
||||
|
||||
#if canImport(CryptoKit)
|
||||
import CryptoKit
|
||||
#else
|
||||
import Crypto
|
||||
#endif
|
||||
|
||||
struct ServerMessage {
|
||||
|
||||
static let authTokenSize = SHA256.byteCount
|
||||
|
||||
static let length = authTokenSize + Message.length
|
||||
|
||||
let authToken: Data
|
||||
|
||||
let message: Message
|
||||
|
||||
init(authToken: Data, message: Message) {
|
||||
self.authToken = authToken
|
||||
self.message = message
|
||||
}
|
||||
|
||||
/**
|
||||
Decode a message from a byte buffer.
|
||||
The buffer must contain at least `ServerMessage.length` bytes, or it will return `nil`.
|
||||
- Parameter buffer: The buffer containing the bytes.
|
||||
*/
|
||||
init?(decodeFrom buffer: ByteBuffer) {
|
||||
guard let data = buffer.getBytes(at: 0, length: ServerMessage.length) else {
|
||||
return nil
|
||||
}
|
||||
self.authToken = Data(data.prefix(ServerMessage.authTokenSize))
|
||||
self.message = Message(decodeFrom: Data(data.dropFirst(ServerMessage.authTokenSize)))
|
||||
}
|
||||
|
||||
var encoded: Data {
|
||||
authToken + message.encoded
|
||||
}
|
||||
|
||||
static func token(from buffer: ByteBuffer) -> Data? {
|
||||
guard buffer.readableBytes == authTokenSize else {
|
||||
return nil
|
||||
}
|
||||
guard let bytes = buffer.getBytes(at: 0, length: authTokenSize) else {
|
||||
return nil
|
||||
}
|
||||
return Data(bytes)
|
||||
}
|
||||
}
|
@ -10,41 +10,20 @@ struct Client {
|
||||
init(server: URL) {
|
||||
self.server = server
|
||||
}
|
||||
|
||||
func deviceStatus(authToken: Data) async -> ClientState {
|
||||
await send(path: .getDeviceStatus, data: authToken).state
|
||||
}
|
||||
|
||||
private enum RequestReponse: Error {
|
||||
case requestFailed
|
||||
case unknownResponseData(Data)
|
||||
case unknownResponseString(String)
|
||||
case success(UInt8)
|
||||
func send(_ message: Message, authToken: Data) async -> (state: ClientState, response: Message?) {
|
||||
let serverMessage = ServerMessage(authToken: authToken, message: message)
|
||||
return await send(path: .postMessage, data: serverMessage.encoded)
|
||||
}
|
||||
|
||||
func deviceStatus() async -> ClientState {
|
||||
let url = server.appendingPathComponent(RouteAPI.getDeviceStatus.rawValue)
|
||||
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
|
||||
let response = await integerReponse(to: request)
|
||||
switch response {
|
||||
case .requestFailed:
|
||||
return .deviceNotAvailable(.serverNotReached)
|
||||
case .unknownResponseData(let data):
|
||||
return .internalError("Unknown status (\(data.count) bytes)")
|
||||
case .unknownResponseString(let string):
|
||||
return .internalError("Unknown status (\(string.prefix(15)))")
|
||||
case .success(let int):
|
||||
switch int {
|
||||
case 0:
|
||||
return .deviceNotAvailable(.deviceDisconnected)
|
||||
case 1:
|
||||
return .ready
|
||||
default:
|
||||
return .internalError("Invalid status: \(int)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func send(_ message: Message) async throws -> (state: ClientState, response: Message?) {
|
||||
let url = server.appendingPathComponent(RouteAPI.postMessage.rawValue)
|
||||
private func send(path: RouteAPI, data: Data) async -> (state: ClientState, response: Message?) {
|
||||
let url = server.appendingPathComponent(path.rawValue)
|
||||
var request = URLRequest(url: url)
|
||||
request.httpBody = message.encoded
|
||||
request.httpBody = data
|
||||
request.httpMethod = "POST"
|
||||
guard let data = await fulfill(request) else {
|
||||
return (.deviceNotAvailable(.serverNotReached), nil)
|
||||
@ -81,22 +60,6 @@ struct Client {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func integerReponse(to request: URLRequest) async -> RequestReponse {
|
||||
guard let data = await fulfill(request) else {
|
||||
return .requestFailed
|
||||
}
|
||||
guard let string = String(data: data, encoding: .utf8) else {
|
||||
print("Unexpected device status data: \([UInt8](data))")
|
||||
return .unknownResponseData(data)
|
||||
}
|
||||
guard let int = UInt8(string) else {
|
||||
print("Unexpected device status '\(string)'")
|
||||
return .unknownResponseString(string)
|
||||
}
|
||||
return .success(int)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NeverCacheDelegate: NSObject, NSURLConnectionDataDelegate {
|
||||
|
@ -93,16 +93,13 @@ enum ClientState {
|
||||
self = .deviceNotAvailable(.deviceDisconnected)
|
||||
case .operationInProgress:
|
||||
self = .waitingForResponse
|
||||
case .deviceConnected:
|
||||
self = .ready
|
||||
}
|
||||
}
|
||||
|
||||
var actionText: String {
|
||||
switch self {
|
||||
case .noKeyAvailable:
|
||||
return "Create key"
|
||||
default:
|
||||
return "Unlock"
|
||||
}
|
||||
"Unlock"
|
||||
}
|
||||
|
||||
var requiresDescription: Bool {
|
||||
@ -137,7 +134,7 @@ enum ClientState {
|
||||
|
||||
var allowsAction: Bool {
|
||||
switch self {
|
||||
case .requestingStatus, .deviceNotAvailable, .waitingForResponse:
|
||||
case .requestingStatus, .deviceNotAvailable, .waitingForResponse, .noKeyAvailable:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
|
@ -7,6 +7,9 @@ struct ContentView: View {
|
||||
|
||||
@AppStorage("counter")
|
||||
var nextMessageCounter: Int = 0
|
||||
|
||||
@State
|
||||
var keyManager = KeyManagement()
|
||||
|
||||
@State
|
||||
var state: ClientState = .noKeyAvailable
|
||||
@ -20,6 +23,12 @@ struct ContentView: View {
|
||||
@State
|
||||
private var responseTime: Date? = nil
|
||||
|
||||
@State
|
||||
private var showKeySheet = false
|
||||
|
||||
@State
|
||||
private var showHistorySheet = false
|
||||
|
||||
var isPerformingRequests: Bool {
|
||||
hasActiveRequest ||
|
||||
state == .waitingForResponse
|
||||
@ -28,7 +37,7 @@ struct ContentView: View {
|
||||
var buttonBackground: Color {
|
||||
state.allowsAction ?
|
||||
.white.opacity(0.2) :
|
||||
.gray.opacity(0.2)
|
||||
.black.opacity(0.2)
|
||||
}
|
||||
|
||||
let buttonBorderWidth: CGFloat = 3
|
||||
@ -39,16 +48,44 @@ struct ContentView: View {
|
||||
|
||||
private let buttonWidth: CGFloat = 250
|
||||
|
||||
private let smallButtonHeight: CGFloat = 50
|
||||
|
||||
private let smallButtonWidth: CGFloat = 120
|
||||
|
||||
private let smallButtonBorderWidth: CGFloat = 1
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
VStack(spacing: 20) {
|
||||
HStack {
|
||||
Button("History", action: { showHistorySheet = true })
|
||||
.frame(width: smallButtonWidth,
|
||||
height: smallButtonHeight)
|
||||
.background(.white.opacity(0.2))
|
||||
.cornerRadius(smallButtonHeight / 2)
|
||||
.overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white))
|
||||
.foregroundColor(.white)
|
||||
.font(.title2)
|
||||
.padding()
|
||||
Spacer()
|
||||
Button("Keys", action: { showKeySheet = true })
|
||||
.frame(width: smallButtonWidth,
|
||||
height: smallButtonHeight)
|
||||
.background(.white.opacity(0.2))
|
||||
.cornerRadius(smallButtonHeight / 2)
|
||||
.overlay(RoundedRectangle(cornerRadius: smallButtonHeight / 2).stroke(lineWidth: smallButtonBorderWidth).foregroundColor(.white))
|
||||
.foregroundColor(.white)
|
||||
.font(.title2)
|
||||
.padding()
|
||||
}
|
||||
Spacer()
|
||||
if state.requiresDescription {
|
||||
Text(state.description)
|
||||
.padding()
|
||||
}
|
||||
Button(state.actionText, action: mainButtonPressed)
|
||||
.frame(width: buttonWidth, height: buttonWidth, alignment: .center)
|
||||
.frame(width: buttonWidth,
|
||||
height: buttonWidth)
|
||||
.background(buttonBackground)
|
||||
.cornerRadius(buttonWidth / 2)
|
||||
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2).stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
|
||||
@ -57,8 +94,9 @@ struct ContentView: View {
|
||||
.disabled(!state.allowsAction)
|
||||
.padding(.bottom, (geo.size.width-buttonWidth) / 2)
|
||||
}
|
||||
.background(state.color)
|
||||
.onAppear {
|
||||
if KeyManagement.hasKey {
|
||||
if keyManager.hasAllKeys {
|
||||
state = .requestingStatus
|
||||
}
|
||||
startRegularStatusUpdates()
|
||||
@ -67,20 +105,19 @@ struct ContentView: View {
|
||||
endRegularStatusUpdates()
|
||||
}
|
||||
.frame(width: geo.size.width, height: geo.size.height)
|
||||
.background(state.color)
|
||||
.animation(.easeInOut, value: state.color)
|
||||
.sheet(isPresented: $showKeySheet) {
|
||||
KeyView(keyManager: $keyManager)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func mainButtonPressed() {
|
||||
guard let key = KeyManagement.key?.remote else {
|
||||
generateKey()
|
||||
guard let key = keyManager.get(.remoteKey),
|
||||
let token = keyManager.get(.authToken)?.data else {
|
||||
return
|
||||
}
|
||||
sendMessage(using: key)
|
||||
}
|
||||
|
||||
func sendMessage(using key: SymmetricKey) {
|
||||
|
||||
let count = UInt32(nextMessageCounter)
|
||||
let now = Date()
|
||||
let content = Message.Content(
|
||||
@ -90,7 +127,7 @@ struct ContentView: View {
|
||||
state = .waitingForResponse
|
||||
print("Sending message \(count)")
|
||||
Task {
|
||||
let (newState, message) = try await server.send(message)
|
||||
let (newState, message) = await server.send(message, authToken: token)
|
||||
responseTime = now
|
||||
state = newState
|
||||
if let message = message {
|
||||
@ -100,7 +137,7 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
private func processResponse(_ message: Message, sendTime: Date) {
|
||||
guard let key = KeyManagement.key?.device else {
|
||||
guard let key = keyManager.get(.deviceKey) else {
|
||||
return
|
||||
}
|
||||
guard message.isValid(using: key) else {
|
||||
@ -115,9 +152,13 @@ struct ContentView: View {
|
||||
let time1 = deviceTime.timeIntervalSince(sendTime)
|
||||
let time2 = now.timeIntervalSince(deviceTime)
|
||||
if time1 < 0 {
|
||||
print("Device time behind by at least \(Int(-time1 * 1000)) ms behind")
|
||||
print("Device time behind by at least \(Int(-time1 * 1000)) ms")
|
||||
print("Device: \(deviceTime)")
|
||||
print("Remote: \(now)")
|
||||
} else if time2 < 0 {
|
||||
print("Device time behind by at least \(Int(-time2 * 1000)) ms ahead")
|
||||
print("Device time ahead by at least \(Int(-time2 * 1000)) ms")
|
||||
print("Device: \(deviceTime)")
|
||||
print("Remote: \(now)")
|
||||
} else {
|
||||
print("Device time synchronized")
|
||||
}
|
||||
@ -139,13 +180,16 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
func checkDeviceStatus(_ timer: Timer) {
|
||||
guard let authToken = keyManager.get(.authToken) else {
|
||||
return
|
||||
}
|
||||
guard !hasActiveRequest else {
|
||||
return
|
||||
}
|
||||
hasActiveRequest = true
|
||||
print("Checking device status")
|
||||
Task {
|
||||
let newState = await server.deviceStatus()
|
||||
let newState = await server.deviceStatus(authToken: authToken.data)
|
||||
hasActiveRequest = false
|
||||
switch state {
|
||||
case .noKeyAvailable:
|
||||
@ -173,16 +217,6 @@ struct ContentView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func generateKey() {
|
||||
print("Regenerate key")
|
||||
KeyManagement.generateNewKeys()
|
||||
state = .requestingStatus
|
||||
}
|
||||
|
||||
func shareKey() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
|
@ -2,95 +2,154 @@ import Foundation
|
||||
import CryptoKit
|
||||
import SwiftUI
|
||||
|
||||
final class KeyManagement {
|
||||
extension KeyManagement {
|
||||
|
||||
static let tag = "com.ch.sesame.key".data(using: .utf8)!
|
||||
enum KeyType: String, Identifiable, CaseIterable {
|
||||
|
||||
private static let label = "sesame"
|
||||
case deviceKey = "sesame-device"
|
||||
case remoteKey = "sesame-remote"
|
||||
case authToken = "sesame-remote-auth"
|
||||
|
||||
private static let keyType = kSecAttrKeyTypeEC
|
||||
var id: String {
|
||||
rawValue
|
||||
}
|
||||
|
||||
private static let keyClass = kSecAttrKeyClassSymmetric
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .deviceKey:
|
||||
return "Device Key"
|
||||
case .remoteKey:
|
||||
return "Remote Key"
|
||||
case .authToken:
|
||||
return "Authentication Token"
|
||||
}
|
||||
}
|
||||
|
||||
private static let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: "account",
|
||||
kSecAttrServer as String: "christophhagen.de",
|
||||
kSecAttrLabel as String: "sesame"]
|
||||
var keyLength: SymmetricKeySize {
|
||||
.bits256
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static func loadKeys() -> Data? {
|
||||
var query = query
|
||||
extension KeyManagement.KeyType: CustomStringConvertible {
|
||||
|
||||
var description: String {
|
||||
displayName
|
||||
}
|
||||
}
|
||||
|
||||
private struct KeyChain {
|
||||
|
||||
private let keyType = kSecAttrKeyTypeEC
|
||||
|
||||
private let keyClass = kSecAttrKeyClassSymmetric
|
||||
|
||||
private let domain: String
|
||||
|
||||
init(domain: String) {
|
||||
self.domain = domain
|
||||
}
|
||||
|
||||
private func baseQuery(for type: KeyManagement.KeyType) -> [String : Any] {
|
||||
[kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: type.rawValue,
|
||||
kSecAttrServer as String: domain]
|
||||
}
|
||||
|
||||
func save(_ type: KeyManagement.KeyType, _ key: SymmetricKey) {
|
||||
var query = baseQuery(for: type)
|
||||
query[kSecValueData as String] = key.data
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
guard status == errSecSuccess else {
|
||||
print("Failed to store \(type): \(status)")
|
||||
return
|
||||
}
|
||||
print("\(type) saved to keychain")
|
||||
}
|
||||
|
||||
func load(_ type: KeyManagement.KeyType) -> SymmetricKey? {
|
||||
var query = baseQuery(for: type)
|
||||
query[kSecReturnData as String] = kCFBooleanTrue
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status == errSecSuccess else {
|
||||
print("Failed to get key: \(status)")
|
||||
print("Failed to get \(type): \(status)")
|
||||
return nil
|
||||
}
|
||||
let key = item as! CFData
|
||||
print("Key loaded from keychain")
|
||||
return key as Data
|
||||
print("\(type) loaded from keychain")
|
||||
return SymmetricKey(data: key as Data)
|
||||
}
|
||||
|
||||
private static func deleteKeys() {
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
func delete(_ type: KeyManagement.KeyType) {
|
||||
let status = SecItemDelete(baseQuery(for: type) as CFDictionary)
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||
print("Failed to remove key: \(status)")
|
||||
print("Failed to remove \(type): \(status)")
|
||||
return
|
||||
}
|
||||
print("Key removed from keychain")
|
||||
print("\(type) removed from keychain")
|
||||
}
|
||||
|
||||
private static func saveKeys(_ data: Data) {
|
||||
var query = query
|
||||
query[kSecValueData as String] = data
|
||||
let status = SecItemAdd(query as CFDictionary, nil)
|
||||
guard status == errSecSuccess else {
|
||||
print("Failed to store key: \(status)")
|
||||
return
|
||||
}
|
||||
print("Key saved to keychain")
|
||||
}
|
||||
|
||||
private static var keyData: Data? = loadKeys() {
|
||||
didSet {
|
||||
guard let data = keyData else {
|
||||
deleteKeys()
|
||||
return
|
||||
}
|
||||
saveKeys(data)
|
||||
}
|
||||
}
|
||||
|
||||
static var hasKey: Bool {
|
||||
key != nil
|
||||
}
|
||||
|
||||
private(set) static var key: (device: SymmetricKey, remote: SymmetricKey)? {
|
||||
get {
|
||||
guard let data = keyData else {
|
||||
return nil
|
||||
}
|
||||
let device = SymmetricKey(data: data.prefix(32))
|
||||
let remote = SymmetricKey(data: data.advanced(by: 32))
|
||||
return (device, remote)
|
||||
}
|
||||
set {
|
||||
guard let key = newValue else {
|
||||
keyData = nil
|
||||
return
|
||||
}
|
||||
keyData = key.device.data + key.remote.data
|
||||
}
|
||||
}
|
||||
|
||||
static func generateNewKeys() {
|
||||
let device = SymmetricKey(size: .bits256)
|
||||
let remote = SymmetricKey(size: .bits256)
|
||||
key = (device, remote)
|
||||
print("New keys:")
|
||||
print("Device: \(device.data.hexEncoded)")
|
||||
print("Remote: \(remote.data.hexEncoded)")
|
||||
func has(_ type: KeyManagement.KeyType) -> Bool {
|
||||
load(type) != nil
|
||||
}
|
||||
}
|
||||
|
||||
final class KeyManagement: ObservableObject {
|
||||
|
||||
private let keyChain: KeyChain
|
||||
|
||||
@Published
|
||||
private(set) var hasRemoteKey = false
|
||||
|
||||
@Published
|
||||
private(set) var hasDeviceKey = false
|
||||
|
||||
@Published
|
||||
private(set) var hasAuthToken = false
|
||||
|
||||
var hasAllKeys: Bool {
|
||||
hasRemoteKey && hasDeviceKey && hasAuthToken
|
||||
}
|
||||
|
||||
init() {
|
||||
self.keyChain = KeyChain(domain: "christophhagen.de")
|
||||
updateKeyStates()
|
||||
}
|
||||
|
||||
func has(_ type: KeyType) -> Bool {
|
||||
switch type {
|
||||
case .deviceKey:
|
||||
return hasDeviceKey
|
||||
case .remoteKey:
|
||||
return hasRemoteKey
|
||||
case .authToken:
|
||||
return hasAuthToken
|
||||
}
|
||||
}
|
||||
|
||||
func get(_ type: KeyType) -> SymmetricKey? {
|
||||
keyChain.load(type)
|
||||
}
|
||||
|
||||
func delete(_ type: KeyType) {
|
||||
keyChain.delete(type)
|
||||
updateKeyStates()
|
||||
}
|
||||
|
||||
func generate(_ type: KeyType) {
|
||||
let key = SymmetricKey(size: type.keyLength)
|
||||
if keyChain.has(type) {
|
||||
keyChain.delete(type)
|
||||
}
|
||||
keyChain.save(type, key)
|
||||
updateKeyStates()
|
||||
}
|
||||
|
||||
private func updateKeyStates() {
|
||||
self.hasRemoteKey = keyChain.has(.remoteKey)
|
||||
self.hasDeviceKey = keyChain.has(.deviceKey)
|
||||
self.hasAuthToken = keyChain.has(.authToken)
|
||||
}
|
||||
}
|
||||
|
25
Sesame/KeyView.swift
Normal file
25
Sesame/KeyView.swift
Normal file
@ -0,0 +1,25 @@
|
||||
import SwiftUI
|
||||
|
||||
struct KeyView: View {
|
||||
|
||||
@Binding
|
||||
var keyManager: KeyManagement
|
||||
|
||||
var body: some View {
|
||||
GeometryReader { geo in
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
ForEach(KeyManagement.KeyType.allCases) { keyType in
|
||||
SingleKeyView(
|
||||
keyManager: $keyManager,
|
||||
type: keyType)
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct KeyView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
KeyView(keyManager: .constant(KeyManagement()))
|
||||
}
|
||||
}
|
54
Sesame/SingleKeyView.swift
Normal file
54
Sesame/SingleKeyView.swift
Normal file
@ -0,0 +1,54 @@
|
||||
import SwiftUI
|
||||
|
||||
struct SingleKeyView: View {
|
||||
|
||||
@State
|
||||
private var needRefresh = false
|
||||
|
||||
@Binding
|
||||
var keyManager: KeyManagement
|
||||
|
||||
let type: KeyManagement.KeyType
|
||||
|
||||
private var generateText: String {
|
||||
hasKey ? "Generate" : "Regenerate"
|
||||
}
|
||||
|
||||
var hasKey: Bool {
|
||||
keyManager.has(type)
|
||||
}
|
||||
|
||||
var content: String {
|
||||
keyManager.get(type)?.displayString ?? "-"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(type.displayName)
|
||||
.bold()
|
||||
Text(needRefresh ? content : content)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
HStack() {
|
||||
Button(generateText) {
|
||||
keyManager.generate(type)
|
||||
needRefresh.toggle()
|
||||
}
|
||||
.padding()
|
||||
Button("Copy") {
|
||||
UIPasteboard.general.string = content
|
||||
}
|
||||
.disabled(!hasKey)
|
||||
.padding()
|
||||
Spacer()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct SingleKeyView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
SingleKeyView(
|
||||
keyManager: .constant(KeyManagement()),
|
||||
type: .deviceKey)
|
||||
}
|
||||
}
|
@ -10,6 +10,10 @@ extension SymmetricKey {
|
||||
var base64: String {
|
||||
data.base64EncodedString()
|
||||
}
|
||||
|
||||
var displayString: String {
|
||||
data.hexEncoded.uppercased().split(by: 4).joined(separator: " ")
|
||||
}
|
||||
|
||||
var codeString: String {
|
||||
" {" +
|
||||
@ -19,3 +23,19 @@ extension SymmetricKey {
|
||||
"},"
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
func split(by length: Int) -> [String] {
|
||||
var startIndex = self.startIndex
|
||||
var results = [Substring]()
|
||||
|
||||
while startIndex < self.endIndex {
|
||||
let endIndex = self.index(startIndex, offsetBy: length, limitedBy: self.endIndex) ?? self.endIndex
|
||||
results.append(self[startIndex..<endIndex])
|
||||
startIndex = endIndex
|
||||
}
|
||||
|
||||
return results.map { String($0) }
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user