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