Add unlock history, improve color scheme
This commit is contained in:
parent
2a8833ff20
commit
c334996d3e
@ -22,6 +22,10 @@
|
||||
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 */; };
|
||||
E28DED31281EAE9100259690 /* HistoryView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED30281EAE9100259690 /* HistoryView.swift */; };
|
||||
E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED32281EB15B00259690 /* HistoryListItem.swift */; };
|
||||
E28DED35281EB17600259690 /* HistoryItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED34281EB17600259690 /* HistoryItem.swift */; };
|
||||
E28DED37281EC7FB00259690 /* HistoryManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED36281EC7FB00259690 /* HistoryManager.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 */; };
|
||||
@ -43,6 +47,11 @@
|
||||
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>"; };
|
||||
E28DED30281EAE9100259690 /* HistoryView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryView.swift; sourceTree = "<group>"; };
|
||||
E28DED32281EB15B00259690 /* HistoryListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryListItem.swift; sourceTree = "<group>"; };
|
||||
E28DED34281EB17600259690 /* HistoryItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryItem.swift; sourceTree = "<group>"; };
|
||||
E28DED36281EC7FB00259690 /* HistoryManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HistoryManager.swift; sourceTree = "<group>"; };
|
||||
E28DED38281EE9CF00259690 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; 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>"; };
|
||||
@ -79,9 +88,14 @@
|
||||
884A45B5279F48C100D6E650 /* Sesame */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
E28DED38281EE9CF00259690 /* Info.plist */,
|
||||
E2C5C1D92806FE4A00769EF6 /* API */,
|
||||
884A45B6279F48C100D6E650 /* SesameApp.swift */,
|
||||
884A45B8279F48C100D6E650 /* ContentView.swift */,
|
||||
E28DED30281EAE9100259690 /* HistoryView.swift */,
|
||||
E28DED32281EB15B00259690 /* HistoryListItem.swift */,
|
||||
E28DED34281EB17600259690 /* HistoryItem.swift */,
|
||||
E28DED36281EC7FB00259690 /* HistoryManager.swift */,
|
||||
E28DED2C281E840B00259690 /* KeyView.swift */,
|
||||
E28DED2E281E8A0500259690 /* SingleKeyView.swift */,
|
||||
884A45CC27A465F500D6E650 /* Client.swift */,
|
||||
@ -195,14 +209,18 @@
|
||||
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */,
|
||||
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */,
|
||||
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */,
|
||||
E28DED37281EC7FB00259690 /* HistoryManager.swift in Sources */,
|
||||
E2C5C1DB2806FE8900769EF6 /* RouteAPI.swift in Sources */,
|
||||
E2C5C1DD281B3AC400769EF6 /* UInt32+Extensions.swift in Sources */,
|
||||
884A45CD27A465F500D6E650 /* Client.swift in Sources */,
|
||||
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */,
|
||||
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */,
|
||||
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */,
|
||||
E28DED31281EAE9100259690 /* HistoryView.swift in Sources */,
|
||||
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */,
|
||||
E28DED35281EB17600259690 /* HistoryItem.swift in Sources */,
|
||||
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */,
|
||||
E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */,
|
||||
E28DED2D281E840B00259690 /* KeyView.swift in Sources */,
|
||||
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */,
|
||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
||||
@ -340,12 +358,13 @@
|
||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Sesame/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
@ -370,12 +389,13 @@
|
||||
DEVELOPMENT_TEAM = H8WR4M6QQ4;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = Sesame/Info.plist;
|
||||
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
|
||||
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
|
||||
INFOPLIST_KEY_UILaunchScreen_Generation = YES;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/Frameworks",
|
||||
|
Binary file not shown.
@ -96,6 +96,14 @@ extension Message {
|
||||
self.init(decodeFrom: data)
|
||||
}
|
||||
|
||||
init?(decodeFrom data: Data, index: inout Int) {
|
||||
guard index + Message.length <= data.count else {
|
||||
return nil
|
||||
}
|
||||
self.init(decodeFrom: data.advanced(by: index))
|
||||
index += Message.length
|
||||
}
|
||||
|
||||
/// The message encoded to data
|
||||
var encoded: Data {
|
||||
mac + content.encoded
|
||||
|
@ -23,6 +23,7 @@ enum RejectionCause {
|
||||
case invalidTime
|
||||
case invalidAuthentication
|
||||
case timeout
|
||||
case missingKey
|
||||
}
|
||||
|
||||
extension RejectionCause: CustomStringConvertible {
|
||||
@ -37,6 +38,8 @@ extension RejectionCause: CustomStringConvertible {
|
||||
return "Invalid authentication"
|
||||
case .timeout:
|
||||
return "Device not responding"
|
||||
case .missingKey:
|
||||
return "No key to verify message"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -61,6 +64,8 @@ enum ClientState {
|
||||
/// The transmitted message was rejected (multiple possible reasons)
|
||||
case messageRejected(RejectionCause)
|
||||
|
||||
case responseRejected(RejectionCause)
|
||||
|
||||
/// The device responded that the opening action was started
|
||||
case openSesame
|
||||
|
||||
@ -104,7 +109,7 @@ enum ClientState {
|
||||
|
||||
var requiresDescription: Bool {
|
||||
switch self {
|
||||
case .deviceNotAvailable, .messageRejected, .internalError:
|
||||
case .deviceNotAvailable, .messageRejected, .internalError, .responseRejected:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@ -114,21 +119,19 @@ enum ClientState {
|
||||
var color: Color {
|
||||
switch self {
|
||||
case .noKeyAvailable:
|
||||
return .gray
|
||||
case .requestingStatus:
|
||||
return .yellow
|
||||
return Color(red: 50/255, green: 50/255, blue: 50/255)
|
||||
case .deviceNotAvailable:
|
||||
return Color(red: 1.0, green: 0.6, blue: 0.6)
|
||||
case .messageRejected:
|
||||
return .red
|
||||
return Color(red: 150/255, green: 90/255, blue: 90/255)
|
||||
case .messageRejected, .responseRejected:
|
||||
return Color(red: 160/255, green: 30/255, blue: 30/255)
|
||||
case .internalError:
|
||||
return Color(red: 0.7, green: 0, blue: 0)
|
||||
return Color(red: 100/255, green: 0/255, blue: 0/255)
|
||||
case .ready:
|
||||
return Color(red: 0.7, green: 1.0, blue: 0.5)
|
||||
case .waitingForResponse:
|
||||
return Color(red: 0.9, green: 1.0, blue: 0.5)
|
||||
return Color(red: 115/255, green: 140/255, blue: 90/255)
|
||||
case .requestingStatus, .waitingForResponse:
|
||||
return Color(red: 160/255, green: 170/255, blue: 110/255)
|
||||
case .openSesame:
|
||||
return .green
|
||||
return Color(red: 65/255, green: 110/255, blue: 60/255)
|
||||
}
|
||||
}
|
||||
|
||||
@ -166,7 +169,115 @@ extension ClientState: CustomStringConvertible {
|
||||
return "Unlocked"
|
||||
case .internalError(let e):
|
||||
return "Error: \(e)"
|
||||
case .responseRejected(let cause):
|
||||
switch cause {
|
||||
case .invalidAuthentication:
|
||||
return "Device message not authenticated"
|
||||
default:
|
||||
return cause.description
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
|
||||
extension ClientState {
|
||||
|
||||
var encoded: Data {
|
||||
Data([code])
|
||||
}
|
||||
|
||||
private var code: UInt8 {
|
||||
switch self {
|
||||
case .noKeyAvailable:
|
||||
return 1
|
||||
case .requestingStatus:
|
||||
return 2
|
||||
case .deviceNotAvailable(let connectionError):
|
||||
switch connectionError {
|
||||
case .serverNotReached:
|
||||
return 3
|
||||
case .deviceDisconnected:
|
||||
return 4
|
||||
}
|
||||
case .ready:
|
||||
return 5
|
||||
case .waitingForResponse:
|
||||
return 6
|
||||
case .messageRejected(let rejectionCause):
|
||||
switch rejectionCause {
|
||||
case .invalidCounter:
|
||||
return 7
|
||||
case .invalidTime:
|
||||
return 8
|
||||
case .invalidAuthentication:
|
||||
return 9
|
||||
case .timeout:
|
||||
return 10
|
||||
case .missingKey:
|
||||
return 11
|
||||
}
|
||||
case .responseRejected(let rejectionCause):
|
||||
switch rejectionCause {
|
||||
case .invalidCounter:
|
||||
return 12
|
||||
case .invalidTime:
|
||||
return 13
|
||||
case .invalidAuthentication:
|
||||
return 14
|
||||
case .timeout:
|
||||
return 15
|
||||
case .missingKey:
|
||||
return 16
|
||||
}
|
||||
case .openSesame:
|
||||
return 17
|
||||
case .internalError(_):
|
||||
return 18
|
||||
}
|
||||
}
|
||||
|
||||
init(code: UInt8) {
|
||||
switch code {
|
||||
case 1:
|
||||
self = .noKeyAvailable
|
||||
case 2:
|
||||
self = .requestingStatus
|
||||
case 3:
|
||||
self = .deviceNotAvailable(.serverNotReached)
|
||||
case 4:
|
||||
self = .deviceNotAvailable(.deviceDisconnected)
|
||||
case 5:
|
||||
self = .ready
|
||||
case 6:
|
||||
self = .waitingForResponse
|
||||
case 7:
|
||||
self = .messageRejected(.invalidCounter)
|
||||
case 8:
|
||||
self = .messageRejected(.invalidTime)
|
||||
case 9:
|
||||
self = .messageRejected(.invalidAuthentication)
|
||||
case 10:
|
||||
self = .messageRejected(.timeout)
|
||||
case 11:
|
||||
self = .messageRejected(.missingKey)
|
||||
case 12:
|
||||
self = .responseRejected(.invalidCounter)
|
||||
case 13:
|
||||
self = .responseRejected(.invalidTime)
|
||||
case 14:
|
||||
self = .responseRejected(.invalidAuthentication)
|
||||
case 15:
|
||||
self = .responseRejected(.timeout)
|
||||
case 16:
|
||||
self = .responseRejected(.missingKey)
|
||||
case 17:
|
||||
self = .openSesame
|
||||
case 18:
|
||||
self = .internalError("")
|
||||
default:
|
||||
self = .internalError("Unknown code \(code)")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -10,6 +10,8 @@ struct ContentView: View {
|
||||
|
||||
@State
|
||||
var keyManager = KeyManagement()
|
||||
|
||||
let history = HistoryManager()
|
||||
|
||||
@State
|
||||
var state: ClientState = .noKeyAvailable
|
||||
@ -109,7 +111,11 @@ struct ContentView: View {
|
||||
.sheet(isPresented: $showKeySheet) {
|
||||
KeyView(keyManager: $keyManager)
|
||||
}
|
||||
.sheet(isPresented: $showHistorySheet) {
|
||||
HistoryView(manager: history)
|
||||
}
|
||||
}
|
||||
.preferredColorScheme(.dark)
|
||||
}
|
||||
|
||||
func mainButtonPressed() {
|
||||
@ -119,51 +125,52 @@ struct ContentView: View {
|
||||
}
|
||||
|
||||
let count = UInt32(nextMessageCounter)
|
||||
let now = Date()
|
||||
let sentTime = Date()
|
||||
let content = Message.Content(
|
||||
time: now.timestamp,
|
||||
time: sentTime.timestamp,
|
||||
id: count)
|
||||
let message = content.authenticate(using: key)
|
||||
let historyItem = HistoryItem(sent: message, date: sentTime)
|
||||
state = .waitingForResponse
|
||||
print("Sending message \(count)")
|
||||
Task {
|
||||
let (newState, message) = await server.send(message, authToken: token)
|
||||
responseTime = now
|
||||
let receivedTime = Date.now
|
||||
responseTime = receivedTime
|
||||
state = newState
|
||||
if let message = message {
|
||||
processResponse(message, sendTime: now)
|
||||
}
|
||||
let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: message)
|
||||
print("Interval: \(receivedTime.timeIntervalSince(sentTime))", "\(finishedItem.roundTripTime ?? -1)")
|
||||
process(item: finishedItem)
|
||||
}
|
||||
}
|
||||
|
||||
private func processResponse(_ message: Message, sendTime: Date) {
|
||||
private func process(item: HistoryItem) {
|
||||
guard let message = item.incomingMessage else {
|
||||
save(historyItem: item)
|
||||
return
|
||||
}
|
||||
|
||||
guard let key = keyManager.get(.deviceKey) else {
|
||||
save(historyItem: item.notAuthenticated())
|
||||
return
|
||||
}
|
||||
guard message.isValid(using: key) else {
|
||||
save(historyItem: item.invalidated())
|
||||
return
|
||||
}
|
||||
nextMessageCounter = Int(message.content.id)
|
||||
print("Next counter is \(message.content.id)")
|
||||
let now = Date()
|
||||
let total = now.timeIntervalSince(sendTime)
|
||||
print("Total time: \(Int(total * 1000)) ms")
|
||||
let deviceTime = Date(timestamp: message.content.time)
|
||||
let time1 = deviceTime.timeIntervalSince(sendTime)
|
||||
let time2 = now.timeIntervalSince(deviceTime)
|
||||
if time1 < 0 {
|
||||
print("Device time behind by at least \(Int(-time1 * 1000)) ms")
|
||||
print("Device: \(deviceTime)")
|
||||
print("Remote: \(now)")
|
||||
} else if time2 < 0 {
|
||||
print("Device time ahead by at least \(Int(-time2 * 1000)) ms")
|
||||
print("Device: \(deviceTime)")
|
||||
print("Remote: \(now)")
|
||||
} else {
|
||||
print("Device time synchronized")
|
||||
save(historyItem: item)
|
||||
}
|
||||
|
||||
private func save(historyItem: HistoryItem) {
|
||||
do {
|
||||
try history.save(item: historyItem)
|
||||
} catch {
|
||||
print("Failed to save item: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func startRegularStatusUpdates() {
|
||||
guard timer == nil else {
|
||||
return
|
||||
@ -187,7 +194,6 @@ struct ContentView: View {
|
||||
return
|
||||
}
|
||||
hasActiveRequest = true
|
||||
print("Checking device status")
|
||||
Task {
|
||||
let newState = await server.deviceStatus(authToken: authToken.data)
|
||||
hasActiveRequest = false
|
||||
@ -198,13 +204,14 @@ struct ContentView: View {
|
||||
state = newState
|
||||
case .waitingForResponse:
|
||||
return
|
||||
case .messageRejected, .openSesame, .internalError:
|
||||
case .messageRejected, .openSesame, .internalError, .responseRejected:
|
||||
guard let time = responseTime else {
|
||||
state = newState
|
||||
return
|
||||
}
|
||||
responseTime = nil
|
||||
// Wait at least 5 seconds after these states have been reached before changing the
|
||||
// interface to allow sufficient time to see the result
|
||||
let elapsed = Date.now.timeIntervalSince(time)
|
||||
guard elapsed < 5 else {
|
||||
state = newState
|
||||
|
168
Sesame/HistoryItem.swift
Normal file
168
Sesame/HistoryItem.swift
Normal file
@ -0,0 +1,168 @@
|
||||
import Foundation
|
||||
|
||||
struct HistoryItem {
|
||||
|
||||
let outgoingDate: Date
|
||||
|
||||
let outgoingMessage: Message
|
||||
|
||||
let incomingDate: Date?
|
||||
|
||||
let incomingMessage: Message?
|
||||
|
||||
let response: ClientState?
|
||||
|
||||
init(sent message: Message, date: Date) {
|
||||
self.outgoingDate = date
|
||||
self.outgoingMessage = message
|
||||
self.incomingDate = nil
|
||||
self.incomingMessage = nil
|
||||
self.response = nil
|
||||
}
|
||||
|
||||
func didReceive(response: ClientState, date: Date?, message: Message?) -> HistoryItem {
|
||||
.init(sent: self, response: response, date: date, message: message)
|
||||
}
|
||||
|
||||
func invalidated() -> HistoryItem {
|
||||
didReceive(response: .responseRejected(.invalidAuthentication), date: incomingDate, message: incomingMessage)
|
||||
}
|
||||
|
||||
func notAuthenticated() -> HistoryItem {
|
||||
didReceive(response: .responseRejected(.missingKey), date: incomingDate, message: incomingMessage)
|
||||
}
|
||||
|
||||
private init(sent: HistoryItem, response: ClientState, date: Date?, message: Message?) {
|
||||
self.outgoingDate = sent.outgoingDate
|
||||
self.outgoingMessage = sent.outgoingMessage
|
||||
self.incomingDate = date
|
||||
self.incomingMessage = message
|
||||
self.response = response
|
||||
}
|
||||
|
||||
// MARK: Statistics
|
||||
|
||||
var roundTripTime: TimeInterval? {
|
||||
incomingDate?.timeIntervalSince(outgoingDate)
|
||||
}
|
||||
|
||||
var deviceTime: Date? {
|
||||
guard let timestamp = incomingMessage?.content.time else {
|
||||
return nil
|
||||
}
|
||||
return Date(timestamp: timestamp)
|
||||
}
|
||||
|
||||
var requestLatency: TimeInterval? {
|
||||
deviceTime?.timeIntervalSince(outgoingDate)
|
||||
}
|
||||
|
||||
var responseLatency: TimeInterval? {
|
||||
guard let deviceTime = deviceTime else {
|
||||
return nil
|
||||
}
|
||||
return incomingDate?.timeIntervalSince(deviceTime)
|
||||
}
|
||||
|
||||
var clockOffset: Int? {
|
||||
guard let interval = roundTripTime, let deviceTime = deviceTime else {
|
||||
return nil
|
||||
}
|
||||
let estimatedArrival = outgoingDate.advanced(by: interval / 2)
|
||||
return Int(deviceTime.timeIntervalSince(estimatedArrival))
|
||||
}
|
||||
|
||||
// MARK: Coding
|
||||
|
||||
static func testEncoding() {
|
||||
|
||||
}
|
||||
|
||||
var encoded: Data {
|
||||
var result = outgoingDate.encoded + outgoingMessage.encoded
|
||||
if let date = incomingDate {
|
||||
result += Data([1]) + date.encoded
|
||||
} else {
|
||||
result += Data([0])
|
||||
}
|
||||
if let message = incomingMessage {
|
||||
result += Data([1]) + message.encoded
|
||||
} else {
|
||||
result += Data([0])
|
||||
}
|
||||
result += response?.encoded ?? Data([0])
|
||||
return result
|
||||
}
|
||||
|
||||
init?(decodeFrom data: Data, index: inout Int) {
|
||||
guard let outgoingDate = Date(decodeFrom: data, index: &index) else {
|
||||
return nil
|
||||
}
|
||||
self.outgoingDate = outgoingDate
|
||||
|
||||
guard let outgoingMessage = Message(decodeFrom: data, index: &index) else {
|
||||
return nil
|
||||
}
|
||||
self.outgoingMessage = outgoingMessage
|
||||
|
||||
if data[index] > 0 {
|
||||
index += 1
|
||||
guard let incomingDate = Date(decodeFrom: data, index: &index) else {
|
||||
return nil
|
||||
}
|
||||
self.incomingDate = incomingDate
|
||||
} else {
|
||||
self.incomingDate = nil
|
||||
index += 1
|
||||
}
|
||||
|
||||
if data[index] > 0 {
|
||||
index += 1
|
||||
guard let incomingMessage = Message(decodeFrom: data, index: &index) else {
|
||||
return nil
|
||||
}
|
||||
self.incomingMessage = incomingMessage
|
||||
} else {
|
||||
self.incomingMessage = nil
|
||||
index += 1
|
||||
}
|
||||
guard index < data.count else {
|
||||
return nil
|
||||
}
|
||||
self.response = ClientState(code: data[index])
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
private extension Date {
|
||||
|
||||
static var encodedSize: Int {
|
||||
MemoryLayout<Double>.size
|
||||
}
|
||||
|
||||
var encoded: Data {
|
||||
.init(from: timeIntervalSince1970)
|
||||
}
|
||||
|
||||
init?(decodeFrom data: Data, index: inout Int) {
|
||||
guard index + Date.encodedSize <= data.count else {
|
||||
return nil
|
||||
}
|
||||
self.init(timeIntervalSince1970: data.advanced(by: index).convert(into: .zero))
|
||||
index += Date.encodedSize
|
||||
}
|
||||
}
|
||||
|
||||
extension HistoryItem: Identifiable {
|
||||
|
||||
var id: UInt32 {
|
||||
outgoingDate.timestamp
|
||||
}
|
||||
}
|
||||
|
||||
extension HistoryItem: Comparable {
|
||||
|
||||
static func < (lhs: HistoryItem, rhs: HistoryItem) -> Bool {
|
||||
lhs.outgoingDate < rhs.outgoingDate
|
||||
}
|
||||
}
|
89
Sesame/HistoryListItem.swift
Normal file
89
Sesame/HistoryListItem.swift
Normal file
@ -0,0 +1,89 @@
|
||||
import SwiftUI
|
||||
|
||||
private let df: DateFormatter = {
|
||||
let df = DateFormatter()
|
||||
df.dateStyle = .short
|
||||
df.timeStyle = .short
|
||||
return df
|
||||
}()
|
||||
|
||||
struct HistoryListItem: View {
|
||||
|
||||
let entry: HistoryItem
|
||||
|
||||
var entryTime: String {
|
||||
df.string(from: entry.outgoingDate)
|
||||
}
|
||||
|
||||
var roundTripText: String {
|
||||
guard let time = entry.roundTripTime else {
|
||||
return ""
|
||||
}
|
||||
return "⇆ \(Int(time * 1000)) ms"
|
||||
}
|
||||
|
||||
var counterText: String {
|
||||
let sentCounter = entry.outgoingMessage.content.id
|
||||
let startText = "🔗 \(sentCounter)"
|
||||
guard let rCounter = entry.incomingMessage?.content.id else {
|
||||
return startText
|
||||
}
|
||||
let diff = Int(rCounter) - Int(sentCounter)
|
||||
guard diff != 1 else {
|
||||
return startText
|
||||
}
|
||||
return startText + "→\(rCounter)"
|
||||
}
|
||||
|
||||
var timeOffsetText: String {
|
||||
guard let offset = entry.clockOffset, offset != 0 else {
|
||||
return ""
|
||||
}
|
||||
return "🕓 \(offset) s"
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
HStack {
|
||||
Text(entry.response?.description ?? "")
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
Text(entryTime)
|
||||
}.padding(.bottom, 1)
|
||||
HStack {
|
||||
Text(roundTripText)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Text(counterText)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Text(timeOffsetText)
|
||||
.font(.subheadline)
|
||||
.foregroundColor(.secondary)
|
||||
Spacer()
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
|
||||
struct HistoryListItem_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HistoryListItem(entry: .mock)
|
||||
}
|
||||
}
|
||||
|
||||
private extension HistoryItem {
|
||||
|
||||
static var mock: HistoryItem {
|
||||
let mac = Data(repeating: 42, count: 32)
|
||||
let content = Message.Content(time: Date.now.timestamp, id: 123)
|
||||
let content2 = Message.Content(time: (Date.now + 1).timestamp, id: 124)
|
||||
return .init(
|
||||
sent: Message(mac: mac, content: content),
|
||||
date: .now)
|
||||
.didReceive(
|
||||
response: .openSesame,
|
||||
date: .now + 2,
|
||||
message: Message(mac: mac, content: content2))
|
||||
}
|
||||
}
|
59
Sesame/HistoryManager.swift
Normal file
59
Sesame/HistoryManager.swift
Normal file
@ -0,0 +1,59 @@
|
||||
import Foundation
|
||||
|
||||
final class HistoryManager {
|
||||
|
||||
private var fm: FileManager {
|
||||
.default
|
||||
}
|
||||
|
||||
var documentDirectory: URL {
|
||||
try! fm.url(
|
||||
for: .documentDirectory,
|
||||
in: .userDomainMask,
|
||||
appropriateFor: nil, create: true)
|
||||
}
|
||||
|
||||
private var fileUrl: URL {
|
||||
documentDirectory.appendingPathComponent("history.bin")
|
||||
}
|
||||
|
||||
func loadEntries() -> [HistoryItem] {
|
||||
let url = fileUrl
|
||||
guard fm.fileExists(atPath: url.path) else {
|
||||
print("No history data found")
|
||||
return []
|
||||
}
|
||||
let content: Data
|
||||
do {
|
||||
content = try Data(contentsOf: url)
|
||||
} catch {
|
||||
print("Failed to read history data: \(error)")
|
||||
return []
|
||||
}
|
||||
var index = 0
|
||||
var entries = [HistoryItem]()
|
||||
while index < content.count {
|
||||
guard let entry = HistoryItem(decodeFrom: content, index: &index) else {
|
||||
print("Failed to read entry at index \(index)")
|
||||
return entries
|
||||
}
|
||||
entries.append(entry)
|
||||
}
|
||||
return entries.sorted().reversed()
|
||||
}
|
||||
|
||||
func save(item: HistoryItem) throws {
|
||||
let url = fileUrl
|
||||
let data = item.encoded
|
||||
guard fm.fileExists(atPath: url.path) else {
|
||||
try data.write(to: url)
|
||||
print("First history item written")
|
||||
return
|
||||
}
|
||||
let handle = try FileHandle(forWritingTo: url)
|
||||
try handle.seekToEnd()
|
||||
try handle.write(contentsOf: data)
|
||||
try handle.close()
|
||||
print("History item written")
|
||||
}
|
||||
}
|
18
Sesame/HistoryView.swift
Normal file
18
Sesame/HistoryView.swift
Normal file
@ -0,0 +1,18 @@
|
||||
import SwiftUI
|
||||
|
||||
struct HistoryView: View {
|
||||
|
||||
let manager: HistoryManager
|
||||
|
||||
var body: some View {
|
||||
List(manager.loadEntries()) { entry in
|
||||
HistoryListItem(entry: entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HistoryView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
HistoryView(manager: .init())
|
||||
}
|
||||
}
|
8
Sesame/Info.plist
Normal file
8
Sesame/Info.plist
Normal file
@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>UIStatusBarStyle</key>
|
||||
<string>UIStatusBarStyleDarkContent</string>
|
||||
</dict>
|
||||
</plist>
|
@ -28,6 +28,15 @@ extension KeyManagement {
|
||||
var keyLength: SymmetricKeySize {
|
||||
.bits256
|
||||
}
|
||||
|
||||
var usesHashing: Bool {
|
||||
switch self {
|
||||
case .authToken:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -78,7 +87,6 @@ private struct KeyChain {
|
||||
return nil
|
||||
}
|
||||
let key = item as! CFData
|
||||
print("\(type) loaded from keychain")
|
||||
return SymmetricKey(data: key as Data)
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
import SwiftUI
|
||||
import CryptoKit
|
||||
|
||||
struct SingleKeyView: View {
|
||||
|
||||
@ -11,7 +12,7 @@ struct SingleKeyView: View {
|
||||
let type: KeyManagement.KeyType
|
||||
|
||||
private var generateText: String {
|
||||
hasKey ? "Generate" : "Regenerate"
|
||||
hasKey ? "Regenerate" : "Generate"
|
||||
}
|
||||
|
||||
var hasKey: Bool {
|
||||
@ -22,20 +23,32 @@ struct SingleKeyView: View {
|
||||
keyManager.get(type)?.displayString ?? "-"
|
||||
}
|
||||
|
||||
var copyText: String {
|
||||
guard let key = keyManager.get(type)?.data else {
|
||||
return ""
|
||||
}
|
||||
guard type.usesHashing else {
|
||||
return key.hexEncoded
|
||||
}
|
||||
return SHA256.hash(data: key).hexEncoded
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(type.displayName)
|
||||
.bold()
|
||||
Text(needRefresh ? content : content)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.foregroundColor(.secondary)
|
||||
HStack() {
|
||||
Button(generateText) {
|
||||
keyManager.generate(type)
|
||||
needRefresh.toggle()
|
||||
}
|
||||
.padding()
|
||||
Button("Copy") {
|
||||
UIPasteboard.general.string = content
|
||||
|
||||
Button(type.usesHashing ? "Copy hash" : "Copy") {
|
||||
UIPasteboard.general.string = copyText
|
||||
}
|
||||
.disabled(!hasKey)
|
||||
.padding()
|
||||
|
@ -24,6 +24,13 @@ extension SymmetricKey {
|
||||
}
|
||||
}
|
||||
|
||||
extension SHA256.Digest {
|
||||
|
||||
var hexEncoded: String {
|
||||
Data(map { $0 }).hexEncoded
|
||||
}
|
||||
}
|
||||
|
||||
extension String {
|
||||
|
||||
func split(by length: Int) -> [String] {
|
||||
|
Loading…
Reference in New Issue
Block a user