Add unlock history, improve color scheme

This commit is contained in:
Christoph Hagen 2022-05-01 18:30:30 +02:00
parent 2a8833ff20
commit c334996d3e
13 changed files with 561 additions and 45 deletions

View File

@ -22,6 +22,10 @@
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 */; }; E28DED2D281E840B00259690 /* KeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2C281E840B00259690 /* KeyView.swift */; };
E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E28DED2E281E8A0500259690 /* SingleKeyView.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 */; }; 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 */; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; 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>"; }; E2C5C1F7281E769F00769EF6 /* ServerMessage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerMessage.swift; sourceTree = "<group>"; };
@ -79,9 +88,14 @@
884A45B5279F48C100D6E650 /* Sesame */ = { 884A45B5279F48C100D6E650 /* Sesame */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
E28DED38281EE9CF00259690 /* Info.plist */,
E2C5C1D92806FE4A00769EF6 /* API */, E2C5C1D92806FE4A00769EF6 /* API */,
884A45B6279F48C100D6E650 /* SesameApp.swift */, 884A45B6279F48C100D6E650 /* SesameApp.swift */,
884A45B8279F48C100D6E650 /* ContentView.swift */, 884A45B8279F48C100D6E650 /* ContentView.swift */,
E28DED30281EAE9100259690 /* HistoryView.swift */,
E28DED32281EB15B00259690 /* HistoryListItem.swift */,
E28DED34281EB17600259690 /* HistoryItem.swift */,
E28DED36281EC7FB00259690 /* HistoryManager.swift */,
E28DED2C281E840B00259690 /* KeyView.swift */, E28DED2C281E840B00259690 /* KeyView.swift */,
E28DED2E281E8A0500259690 /* SingleKeyView.swift */, E28DED2E281E8A0500259690 /* SingleKeyView.swift */,
884A45CC27A465F500D6E650 /* Client.swift */, 884A45CC27A465F500D6E650 /* Client.swift */,
@ -195,14 +209,18 @@
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 */, E28DED2F281E8A0500259690 /* SingleKeyView.swift in Sources */,
E28DED37281EC7FB00259690 /* HistoryManager.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 */,
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */, E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */,
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */, E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */,
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */, 884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */,
E28DED31281EAE9100259690 /* HistoryView.swift in Sources */,
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */, E24EE77927FF95E00011CFD2 /* Message.swift in Sources */,
E28DED35281EB17600259690 /* HistoryItem.swift in Sources */,
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */, 884A45C927A43D7900D6E650 /* ClientState.swift in Sources */,
E28DED33281EB15B00259690 /* HistoryListItem.swift in Sources */,
E28DED2D281E840B00259690 /* KeyView.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 */,
@ -340,12 +358,13 @@
DEVELOPMENT_TEAM = H8WR4M6QQ4; DEVELOPMENT_TEAM = H8WR4M6QQ4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Sesame/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",
@ -370,12 +389,13 @@
DEVELOPMENT_TEAM = H8WR4M6QQ4; DEVELOPMENT_TEAM = H8WR4M6QQ4;
ENABLE_PREVIEWS = YES; ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = Sesame/Info.plist;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait; INFOPLIST_KEY_UISupportedInterfaceOrientations = UIInterfaceOrientationPortrait;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = UIInterfaceOrientationPortrait;
LD_RUNPATH_SEARCH_PATHS = ( LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)", "$(inherited)",
"@executable_path/Frameworks", "@executable_path/Frameworks",

View File

@ -96,6 +96,14 @@ extension Message {
self.init(decodeFrom: data) 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 /// The message encoded to data
var encoded: Data { var encoded: Data {
mac + content.encoded mac + content.encoded

View File

@ -23,6 +23,7 @@ enum RejectionCause {
case invalidTime case invalidTime
case invalidAuthentication case invalidAuthentication
case timeout case timeout
case missingKey
} }
extension RejectionCause: CustomStringConvertible { extension RejectionCause: CustomStringConvertible {
@ -37,6 +38,8 @@ extension RejectionCause: CustomStringConvertible {
return "Invalid authentication" return "Invalid authentication"
case .timeout: case .timeout:
return "Device not responding" 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) /// The transmitted message was rejected (multiple possible reasons)
case messageRejected(RejectionCause) case messageRejected(RejectionCause)
case responseRejected(RejectionCause)
/// The device responded that the opening action was started /// The device responded that the opening action was started
case openSesame case openSesame
@ -104,7 +109,7 @@ enum ClientState {
var requiresDescription: Bool { var requiresDescription: Bool {
switch self { switch self {
case .deviceNotAvailable, .messageRejected, .internalError: case .deviceNotAvailable, .messageRejected, .internalError, .responseRejected:
return true return true
default: default:
return false return false
@ -114,21 +119,19 @@ enum ClientState {
var color: Color { var color: Color {
switch self { switch self {
case .noKeyAvailable: case .noKeyAvailable:
return .gray return Color(red: 50/255, green: 50/255, blue: 50/255)
case .requestingStatus:
return .yellow
case .deviceNotAvailable: case .deviceNotAvailable:
return Color(red: 1.0, green: 0.6, blue: 0.6) return Color(red: 150/255, green: 90/255, blue: 90/255)
case .messageRejected: case .messageRejected, .responseRejected:
return .red return Color(red: 160/255, green: 30/255, blue: 30/255)
case .internalError: case .internalError:
return Color(red: 0.7, green: 0, blue: 0) return Color(red: 100/255, green: 0/255, blue: 0/255)
case .ready: case .ready:
return Color(red: 0.7, green: 1.0, blue: 0.5) return Color(red: 115/255, green: 140/255, blue: 90/255)
case .waitingForResponse: case .requestingStatus, .waitingForResponse:
return Color(red: 0.9, green: 1.0, blue: 0.5) return Color(red: 160/255, green: 170/255, blue: 110/255)
case .openSesame: case .openSesame:
return .green return Color(red: 65/255, green: 110/255, blue: 60/255)
} }
} }
@ -166,7 +169,115 @@ extension ClientState: CustomStringConvertible {
return "Unlocked" return "Unlocked"
case .internalError(let e): case .internalError(let e):
return "Error: \(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)")
}
}
} }

View File

@ -11,6 +11,8 @@ struct ContentView: View {
@State @State
var keyManager = KeyManagement() var keyManager = KeyManagement()
let history = HistoryManager()
@State @State
var state: ClientState = .noKeyAvailable var state: ClientState = .noKeyAvailable
@ -109,8 +111,12 @@ struct ContentView: View {
.sheet(isPresented: $showKeySheet) { .sheet(isPresented: $showKeySheet) {
KeyView(keyManager: $keyManager) KeyView(keyManager: $keyManager)
} }
.sheet(isPresented: $showHistorySheet) {
HistoryView(manager: history)
} }
} }
.preferredColorScheme(.dark)
}
func mainButtonPressed() { func mainButtonPressed() {
guard let key = keyManager.get(.remoteKey), guard let key = keyManager.get(.remoteKey),
@ -119,51 +125,52 @@ struct ContentView: View {
} }
let count = UInt32(nextMessageCounter) let count = UInt32(nextMessageCounter)
let now = Date() let sentTime = Date()
let content = Message.Content( let content = Message.Content(
time: now.timestamp, time: sentTime.timestamp,
id: count) id: count)
let message = content.authenticate(using: key) let message = content.authenticate(using: key)
let historyItem = HistoryItem(sent: message, date: sentTime)
state = .waitingForResponse state = .waitingForResponse
print("Sending message \(count)") print("Sending message \(count)")
Task { Task {
let (newState, message) = await server.send(message, authToken: token) let (newState, message) = await server.send(message, authToken: token)
responseTime = now let receivedTime = Date.now
responseTime = receivedTime
state = newState state = newState
if let message = message { let finishedItem = historyItem.didReceive(response: newState, date: receivedTime, message: message)
processResponse(message, sendTime: now) 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 { guard let key = keyManager.get(.deviceKey) else {
save(historyItem: item.notAuthenticated())
return return
} }
guard message.isValid(using: key) else { guard message.isValid(using: key) else {
save(historyItem: item.invalidated())
return return
} }
nextMessageCounter = Int(message.content.id) nextMessageCounter = Int(message.content.id)
print("Next counter is \(message.content.id)") save(historyItem: item)
let now = Date() }
let total = now.timeIntervalSince(sendTime)
print("Total time: \(Int(total * 1000)) ms") private func save(historyItem: HistoryItem) {
let deviceTime = Date(timestamp: message.content.time) do {
let time1 = deviceTime.timeIntervalSince(sendTime) try history.save(item: historyItem)
let time2 = now.timeIntervalSince(deviceTime) } catch {
if time1 < 0 { print("Failed to save item: \(error)")
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")
} }
} }
private func startRegularStatusUpdates() { private func startRegularStatusUpdates() {
guard timer == nil else { guard timer == nil else {
return return
@ -187,7 +194,6 @@ struct ContentView: View {
return return
} }
hasActiveRequest = true hasActiveRequest = true
print("Checking device status")
Task { Task {
let newState = await server.deviceStatus(authToken: authToken.data) let newState = await server.deviceStatus(authToken: authToken.data)
hasActiveRequest = false hasActiveRequest = false
@ -198,13 +204,14 @@ struct ContentView: View {
state = newState state = newState
case .waitingForResponse: case .waitingForResponse:
return return
case .messageRejected, .openSesame, .internalError: case .messageRejected, .openSesame, .internalError, .responseRejected:
guard let time = responseTime else { guard let time = responseTime else {
state = newState state = newState
return return
} }
responseTime = nil responseTime = nil
// Wait at least 5 seconds after these states have been reached before changing the // 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) let elapsed = Date.now.timeIntervalSince(time)
guard elapsed < 5 else { guard elapsed < 5 else {
state = newState state = newState

168
Sesame/HistoryItem.swift Normal file
View 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
}
}

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

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

View File

@ -28,6 +28,15 @@ extension KeyManagement {
var keyLength: SymmetricKeySize { var keyLength: SymmetricKeySize {
.bits256 .bits256
} }
var usesHashing: Bool {
switch self {
case .authToken:
return true
default:
return false
}
}
} }
} }
@ -78,7 +87,6 @@ private struct KeyChain {
return nil return nil
} }
let key = item as! CFData let key = item as! CFData
print("\(type) loaded from keychain")
return SymmetricKey(data: key as Data) return SymmetricKey(data: key as Data)
} }

View File

@ -1,4 +1,5 @@
import SwiftUI import SwiftUI
import CryptoKit
struct SingleKeyView: View { struct SingleKeyView: View {
@ -11,7 +12,7 @@ struct SingleKeyView: View {
let type: KeyManagement.KeyType let type: KeyManagement.KeyType
private var generateText: String { private var generateText: String {
hasKey ? "Generate" : "Regenerate" hasKey ? "Regenerate" : "Generate"
} }
var hasKey: Bool { var hasKey: Bool {
@ -22,20 +23,32 @@ struct SingleKeyView: View {
keyManager.get(type)?.displayString ?? "-" 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 { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text(type.displayName) Text(type.displayName)
.bold() .bold()
Text(needRefresh ? content : content) Text(needRefresh ? content : content)
.font(.system(.body, design: .monospaced)) .font(.system(.body, design: .monospaced))
.foregroundColor(.secondary)
HStack() { HStack() {
Button(generateText) { Button(generateText) {
keyManager.generate(type) keyManager.generate(type)
needRefresh.toggle() needRefresh.toggle()
} }
.padding() .padding()
Button("Copy") {
UIPasteboard.general.string = content Button(type.usesHashing ? "Copy hash" : "Copy") {
UIPasteboard.general.string = copyText
} }
.disabled(!hasKey) .disabled(!hasKey)
.padding() .padding()

View File

@ -24,6 +24,13 @@ extension SymmetricKey {
} }
} }
extension SHA256.Digest {
var hexEncoded: String {
Data(map { $0 }).hexEncoded
}
}
extension String { extension String {
func split(by length: Int) -> [String] { func split(by length: Int) -> [String] {