Use remote authentication, add key display screen

This commit is contained in:
Christoph Hagen 2022-05-01 14:07:43 +02:00
parent 921b9237f7
commit 2a8833ff20
14 changed files with 378 additions and 149 deletions

View File

@ -20,8 +20,11 @@
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77327FF95920011CFD2 /* DeviceResponse.swift */; }; E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77327FF95920011CFD2 /* DeviceResponse.swift */; };
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; }; E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; };
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; }; 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 */; }; E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */; };
E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.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 */ /* End PBXBuildFile section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
@ -38,8 +41,11 @@
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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 */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
@ -76,11 +82,12 @@
E2C5C1D92806FE4A00769EF6 /* API */, E2C5C1D92806FE4A00769EF6 /* API */,
884A45B6279F48C100D6E650 /* SesameApp.swift */, 884A45B6279F48C100D6E650 /* SesameApp.swift */,
884A45B8279F48C100D6E650 /* ContentView.swift */, 884A45B8279F48C100D6E650 /* ContentView.swift */,
E28DED2C281E840B00259690 /* KeyView.swift */,
E28DED2E281E8A0500259690 /* SingleKeyView.swift */,
884A45CC27A465F500D6E650 /* Client.swift */, 884A45CC27A465F500D6E650 /* Client.swift */,
884A45C827A43D7900D6E650 /* ClientState.swift */, 884A45C827A43D7900D6E650 /* ClientState.swift */,
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */, 884A45C4279F4BBE00D6E650 /* KeyManagement.swift */,
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */, 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */,
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */,
884A45BA279F48C300D6E650 /* Assets.xcassets */, 884A45BA279F48C300D6E650 /* Assets.xcassets */,
884A45BC279F48C300D6E650 /* Preview Content */, 884A45BC279F48C300D6E650 /* Preview Content */,
); );
@ -98,10 +105,12 @@
E2C5C1D92806FE4A00769EF6 /* API */ = { E2C5C1D92806FE4A00769EF6 /* API */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */,
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */, E24EE77327FF95920011CFD2 /* DeviceResponse.swift */,
E24EE77827FF95E00011CFD2 /* Message.swift */, E24EE77827FF95E00011CFD2 /* Message.swift */,
884A45CE27A5402D00D6E650 /* MessageResult.swift */, 884A45CE27A5402D00D6E650 /* MessageResult.swift */,
E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */, E2C5C1DA2806FE8900769EF6 /* RouteAPI.swift */,
E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */,
E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */, E2C5C1DC281B3AC400769EF6 /* UInt32+Extensions.swift */,
); );
path = API; path = API;
@ -185,6 +194,7 @@
files = ( files = (
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */, 884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */,
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */, 884A45B9279F48C100D6E650 /* ContentView.swift in Sources */,
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */,
E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */, E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */,
E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */, E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */,
884A45CD27A465F500D6E650 /* Client.swift in Sources */, 884A45CD27A465F500D6E650 /* Client.swift in Sources */,
@ -193,8 +203,10 @@
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */, 884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */,
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */, E24EE77927FF95E00011CFD2 /* Message.swift in Sources */,
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */, 884A45C927A43D7900D6E650 /* ClientState.swift in Sources */,
E28DED2D281E840B00259690 /* KeyView.swift in Sources */,
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */, 884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */,
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */, 884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
E2C5C1F8281E769F00769EF6 /* ServerMessage.swift in Sources */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };

View File

@ -16,6 +16,11 @@ struct DeviceResponse {
.init(event: .deviceNotConnected) .init(event: .deviceNotConnected)
} }
/// Shorthand property for a connected event.
static var deviceConnected: DeviceResponse {
.init(event: .deviceConnected)
}
/// Shorthand property for an unexpected socket event. /// Shorthand property for an unexpected socket event.
static var unexpectedSocketEvent: DeviceResponse { static var unexpectedSocketEvent: DeviceResponse {
.init(event: .unexpectedSocketEvent) .init(event: .unexpectedSocketEvent)

View File

@ -101,6 +101,10 @@ extension Message {
mac + content.encoded mac + content.encoded
} }
var bytes: [UInt8] {
Array(encoded)
}
/** /**
Create a message from received bytes. Create a message from received bytes.
- Parameter data: The sequence of bytes - Parameter data: The sequence of bytes

View File

@ -38,6 +38,9 @@ enum MessageResult: UInt8 {
/// Another message is being processed by the device /// Another message is being processed by the device
case operationInProgress = 14 case operationInProgress = 14
/// The device is connected
case deviceConnected = 15
} }
extension MessageResult: CustomStringConvertible { extension MessageResult: CustomStringConvertible {
@ -66,6 +69,8 @@ extension MessageResult: CustomStringConvertible {
return "The device did not respond" return "The device did not respond"
case .operationInProgress: case .operationInProgress:
return "Another operation is in progress" return "Another operation is in progress"
case .deviceConnected:
return "The device is connected"
} }
} }
} }

View 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)
}
}

View File

@ -11,40 +11,19 @@ struct Client {
self.server = server self.server = server
} }
private enum RequestReponse: Error { func deviceStatus(authToken: Data) async -> ClientState {
case requestFailed await send(path: .getDeviceStatus, data: authToken).state
case unknownResponseData(Data)
case unknownResponseString(String)
case success(UInt8)
} }
func deviceStatus() async -> ClientState { func send(_ message: Message, authToken: Data) async -> (state: ClientState, response: Message?) {
let url = server.appendingPathComponent(RouteAPI.getDeviceStatus.rawValue) let serverMessage = ServerMessage(authToken: authToken, message: message)
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData) return await send(path: .postMessage, data: serverMessage.encoded)
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?) { private func send(path: RouteAPI, data: Data) async -> (state: ClientState, response: Message?) {
let url = server.appendingPathComponent(RouteAPI.postMessage.rawValue) let url = server.appendingPathComponent(path.rawValue)
var request = URLRequest(url: url) var request = URLRequest(url: url)
request.httpBody = message.encoded request.httpBody = data
request.httpMethod = "POST" request.httpMethod = "POST"
guard let data = await fulfill(request) else { guard let data = await fulfill(request) else {
return (.deviceNotAvailable(.serverNotReached), nil) return (.deviceNotAvailable(.serverNotReached), nil)
@ -81,22 +60,6 @@ struct Client {
return nil 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 { class NeverCacheDelegate: NSObject, NSURLConnectionDataDelegate {

View File

@ -93,16 +93,13 @@ enum ClientState {
self = .deviceNotAvailable(.deviceDisconnected) self = .deviceNotAvailable(.deviceDisconnected)
case .operationInProgress: case .operationInProgress:
self = .waitingForResponse self = .waitingForResponse
case .deviceConnected:
self = .ready
} }
} }
var actionText: String { var actionText: String {
switch self { "Unlock"
case .noKeyAvailable:
return "Create key"
default:
return "Unlock"
}
} }
var requiresDescription: Bool { var requiresDescription: Bool {
@ -137,7 +134,7 @@ enum ClientState {
var allowsAction: Bool { var allowsAction: Bool {
switch self { switch self {
case .requestingStatus, .deviceNotAvailable, .waitingForResponse: case .requestingStatus, .deviceNotAvailable, .waitingForResponse, .noKeyAvailable:
return false return false
default: default:
return true return true

View File

@ -8,6 +8,9 @@ struct ContentView: View {
@AppStorage("counter") @AppStorage("counter")
var nextMessageCounter: Int = 0 var nextMessageCounter: Int = 0
@State
var keyManager = KeyManagement()
@State @State
var state: ClientState = .noKeyAvailable var state: ClientState = .noKeyAvailable
@ -20,6 +23,12 @@ struct ContentView: View {
@State @State
private var responseTime: Date? = nil private var responseTime: Date? = nil
@State
private var showKeySheet = false
@State
private var showHistorySheet = false
var isPerformingRequests: Bool { var isPerformingRequests: Bool {
hasActiveRequest || hasActiveRequest ||
state == .waitingForResponse state == .waitingForResponse
@ -28,7 +37,7 @@ struct ContentView: View {
var buttonBackground: Color { var buttonBackground: Color {
state.allowsAction ? state.allowsAction ?
.white.opacity(0.2) : .white.opacity(0.2) :
.gray.opacity(0.2) .black.opacity(0.2)
} }
let buttonBorderWidth: CGFloat = 3 let buttonBorderWidth: CGFloat = 3
@ -39,16 +48,44 @@ struct ContentView: View {
private let buttonWidth: CGFloat = 250 private let buttonWidth: CGFloat = 250
private let smallButtonHeight: CGFloat = 50
private let smallButtonWidth: CGFloat = 120
private let smallButtonBorderWidth: CGFloat = 1
var body: some View { var body: some View {
GeometryReader { geo in GeometryReader { geo in
VStack(spacing: 20) { 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() Spacer()
if state.requiresDescription { if state.requiresDescription {
Text(state.description) Text(state.description)
.padding() .padding()
} }
Button(state.actionText, action: mainButtonPressed) Button(state.actionText, action: mainButtonPressed)
.frame(width: buttonWidth, height: buttonWidth, alignment: .center) .frame(width: buttonWidth,
height: buttonWidth)
.background(buttonBackground) .background(buttonBackground)
.cornerRadius(buttonWidth / 2) .cornerRadius(buttonWidth / 2)
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2).stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor)) .overlay(RoundedRectangle(cornerRadius: buttonWidth / 2).stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
@ -57,8 +94,9 @@ struct ContentView: View {
.disabled(!state.allowsAction) .disabled(!state.allowsAction)
.padding(.bottom, (geo.size.width-buttonWidth) / 2) .padding(.bottom, (geo.size.width-buttonWidth) / 2)
} }
.background(state.color)
.onAppear { .onAppear {
if KeyManagement.hasKey { if keyManager.hasAllKeys {
state = .requestingStatus state = .requestingStatus
} }
startRegularStatusUpdates() startRegularStatusUpdates()
@ -67,20 +105,19 @@ struct ContentView: View {
endRegularStatusUpdates() endRegularStatusUpdates()
} }
.frame(width: geo.size.width, height: geo.size.height) .frame(width: geo.size.width, height: geo.size.height)
.background(state.color)
.animation(.easeInOut, value: state.color) .animation(.easeInOut, value: state.color)
.sheet(isPresented: $showKeySheet) {
KeyView(keyManager: $keyManager)
}
} }
} }
func mainButtonPressed() { func mainButtonPressed() {
guard let key = KeyManagement.key?.remote else { guard let key = keyManager.get(.remoteKey),
generateKey() let token = keyManager.get(.authToken)?.data else {
return return
} }
sendMessage(using: key)
}
func sendMessage(using key: SymmetricKey) {
let count = UInt32(nextMessageCounter) let count = UInt32(nextMessageCounter)
let now = Date() let now = Date()
let content = Message.Content( let content = Message.Content(
@ -90,7 +127,7 @@ struct ContentView: View {
state = .waitingForResponse state = .waitingForResponse
print("Sending message \(count)") print("Sending message \(count)")
Task { Task {
let (newState, message) = try await server.send(message) let (newState, message) = await server.send(message, authToken: token)
responseTime = now responseTime = now
state = newState state = newState
if let message = message { if let message = message {
@ -100,7 +137,7 @@ struct ContentView: View {
} }
private func processResponse(_ message: Message, sendTime: Date) { private func processResponse(_ message: Message, sendTime: Date) {
guard let key = KeyManagement.key?.device else { guard let key = keyManager.get(.deviceKey) else {
return return
} }
guard message.isValid(using: key) else { guard message.isValid(using: key) else {
@ -115,9 +152,13 @@ struct ContentView: View {
let time1 = deviceTime.timeIntervalSince(sendTime) let time1 = deviceTime.timeIntervalSince(sendTime)
let time2 = now.timeIntervalSince(deviceTime) let time2 = now.timeIntervalSince(deviceTime)
if time1 < 0 { 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 { } 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 { } else {
print("Device time synchronized") print("Device time synchronized")
} }
@ -139,13 +180,16 @@ struct ContentView: View {
} }
func checkDeviceStatus(_ timer: Timer) { func checkDeviceStatus(_ timer: Timer) {
guard let authToken = keyManager.get(.authToken) else {
return
}
guard !hasActiveRequest else { guard !hasActiveRequest else {
return return
} }
hasActiveRequest = true hasActiveRequest = true
print("Checking device status") print("Checking device status")
Task { Task {
let newState = await server.deviceStatus() let newState = await server.deviceStatus(authToken: authToken.data)
hasActiveRequest = false hasActiveRequest = false
switch state { switch state {
case .noKeyAvailable: case .noKeyAvailable:
@ -173,16 +217,6 @@ struct ContentView: View {
} }
} }
} }
func generateKey() {
print("Regenerate key")
KeyManagement.generateNewKeys()
state = .requestingStatus
}
func shareKey() {
}
} }
struct ContentView_Previews: PreviewProvider { struct ContentView_Previews: PreviewProvider {

View File

@ -2,95 +2,154 @@ import Foundation
import CryptoKit import CryptoKit
import SwiftUI 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] = [ var keyLength: SymmetricKeySize {
kSecClass as String: kSecClassInternetPassword, .bits256
kSecAttrAccount as String: "account", }
kSecAttrServer as String: "christophhagen.de", }
kSecAttrLabel as String: "sesame"] }
private static func loadKeys() -> Data? { extension KeyManagement.KeyType: CustomStringConvertible {
var query = query
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 query[kSecReturnData as String] = kCFBooleanTrue
var item: CFTypeRef? var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item) let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess else { guard status == errSecSuccess else {
print("Failed to get key: \(status)") print("Failed to get \(type): \(status)")
return nil return nil
} }
let key = item as! CFData let key = item as! CFData
print("Key loaded from keychain") print("\(type) loaded from keychain")
return key as Data return SymmetricKey(data: key as Data)
} }
private static func deleteKeys() { func delete(_ type: KeyManagement.KeyType) {
let status = SecItemDelete(query as CFDictionary) let status = SecItemDelete(baseQuery(for: type) as CFDictionary)
guard status == errSecSuccess || status == errSecItemNotFound else { guard status == errSecSuccess || status == errSecItemNotFound else {
print("Failed to remove key: \(status)") print("Failed to remove \(type): \(status)")
return return
} }
print("Key removed from keychain") print("\(type) removed from keychain")
} }
private static func saveKeys(_ data: Data) { func has(_ type: KeyManagement.KeyType) -> Bool {
var query = query load(type) != nil
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 { final class KeyManagement: ObservableObject {
key != nil
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
} }
private(set) static var key: (device: SymmetricKey, remote: SymmetricKey)? { init() {
get { self.keyChain = KeyChain(domain: "christophhagen.de")
guard let data = keyData else { updateKeyStates()
return nil
} }
let device = SymmetricKey(data: data.prefix(32))
let remote = SymmetricKey(data: data.advanced(by: 32)) func has(_ type: KeyType) -> Bool {
return (device, remote) switch type {
} case .deviceKey:
set { return hasDeviceKey
guard let key = newValue else { case .remoteKey:
keyData = nil return hasRemoteKey
return case .authToken:
} return hasAuthToken
keyData = key.device.data + key.remote.data
} }
} }
static func generateNewKeys() { func get(_ type: KeyType) -> SymmetricKey? {
let device = SymmetricKey(size: .bits256) keyChain.load(type)
let remote = SymmetricKey(size: .bits256) }
key = (device, remote)
print("New keys:") func delete(_ type: KeyType) {
print("Device: \(device.data.hexEncoded)") keyChain.delete(type)
print("Remote: \(remote.data.hexEncoded)") 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
View 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()))
}
}

View 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)
}
}

View File

@ -11,6 +11,10 @@ extension SymmetricKey {
data.base64EncodedString() data.base64EncodedString()
} }
var displayString: String {
data.hexEncoded.uppercased().split(by: 4).joined(separator: " ")
}
var codeString: String { var codeString: String {
" {" + " {" +
withUnsafeBytes { withUnsafeBytes {
@ -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) }
}
}