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 */; };
|
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;
|
||||||
};
|
};
|
||||||
|
Binary file not shown.
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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
|
||||||
|
@ -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 {
|
||||||
|
@ -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
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)
|
||||||
|
}
|
||||||
|
}
|
@ -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) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user