Move to new key system
This commit is contained in:
parent
6027228728
commit
a4221c47f7
@ -16,7 +16,12 @@
|
|||||||
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.swift */; };
|
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45C827A43D7900D6E650 /* ClientState.swift */; };
|
||||||
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; };
|
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */; };
|
||||||
884A45CD27A465F500D6E650 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
|
884A45CD27A465F500D6E650 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CC27A465F500D6E650 /* Client.swift */; };
|
||||||
884A45CF27A5402D00D6E650 /* Response.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CE27A5402D00D6E650 /* Response.swift */; };
|
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CE27A5402D00D6E650 /* MessageResult.swift */; };
|
||||||
|
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */; };
|
||||||
|
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77327FF95920011CFD2 /* DeviceResponse.swift */; };
|
||||||
|
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */ = {isa = PBXBuildFile; productRef = E24EE77627FF95C00011CFD2 /* NIOCore */; };
|
||||||
|
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77827FF95E00011CFD2 /* Message.swift */; };
|
||||||
|
E24EE77B280058240011CFD2 /* Message+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24EE77A280058240011CFD2 /* Message+Extensions.swift */; };
|
||||||
/* End PBXBuildFile section */
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
/* Begin PBXFileReference section */
|
/* Begin PBXFileReference section */
|
||||||
@ -30,7 +35,11 @@
|
|||||||
884A45C827A43D7900D6E650 /* ClientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientState.swift; sourceTree = "<group>"; };
|
884A45C827A43D7900D6E650 /* ClientState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ClientState.swift; sourceTree = "<group>"; };
|
||||||
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SymmetricKey+Extensions.swift"; sourceTree = "<group>"; };
|
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SymmetricKey+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
884A45CC27A465F500D6E650 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
|
884A45CC27A465F500D6E650 /* Client.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Client.swift; sourceTree = "<group>"; };
|
||||||
884A45CE27A5402D00D6E650 /* Response.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Response.swift; sourceTree = "<group>"; };
|
884A45CE27A5402D00D6E650 /* MessageResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageResult.swift; sourceTree = "<group>"; };
|
||||||
|
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Data+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
|
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceResponse.swift; sourceTree = "<group>"; };
|
||||||
|
E24EE77827FF95E00011CFD2 /* Message.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Message.swift; sourceTree = "<group>"; };
|
||||||
|
E24EE77A280058240011CFD2 /* Message+Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Message+Extensions.swift"; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
@ -38,6 +47,7 @@
|
|||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
@ -64,15 +74,19 @@
|
|||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
884A45B6279F48C100D6E650 /* SesameApp.swift */,
|
884A45B6279F48C100D6E650 /* SesameApp.swift */,
|
||||||
|
E24EE77827FF95E00011CFD2 /* Message.swift */,
|
||||||
|
E24EE77A280058240011CFD2 /* Message+Extensions.swift */,
|
||||||
884A45B8279F48C100D6E650 /* ContentView.swift */,
|
884A45B8279F48C100D6E650 /* ContentView.swift */,
|
||||||
884A45CC27A465F500D6E650 /* Client.swift */,
|
884A45CC27A465F500D6E650 /* Client.swift */,
|
||||||
884A45CE27A5402D00D6E650 /* Response.swift */,
|
884A45CE27A5402D00D6E650 /* MessageResult.swift */,
|
||||||
884A45C827A43D7900D6E650 /* ClientState.swift */,
|
884A45C827A43D7900D6E650 /* ClientState.swift */,
|
||||||
884A45C627A429EF00D6E650 /* ShareSheet.swift */,
|
884A45C627A429EF00D6E650 /* ShareSheet.swift */,
|
||||||
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */,
|
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */,
|
||||||
|
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */,
|
||||||
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */,
|
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */,
|
||||||
884A45BA279F48C300D6E650 /* Assets.xcassets */,
|
884A45BA279F48C300D6E650 /* Assets.xcassets */,
|
||||||
884A45BC279F48C300D6E650 /* Preview Content */,
|
884A45BC279F48C300D6E650 /* Preview Content */,
|
||||||
|
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */,
|
||||||
);
|
);
|
||||||
path = Sesame;
|
path = Sesame;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -101,6 +115,9 @@
|
|||||||
dependencies = (
|
dependencies = (
|
||||||
);
|
);
|
||||||
name = Sesame;
|
name = Sesame;
|
||||||
|
packageProductDependencies = (
|
||||||
|
E24EE77627FF95C00011CFD2 /* NIOCore */,
|
||||||
|
);
|
||||||
productName = Sesame;
|
productName = Sesame;
|
||||||
productReference = 884A45B3279F48C100D6E650 /* Sesame.app */;
|
productReference = 884A45B3279F48C100D6E650 /* Sesame.app */;
|
||||||
productType = "com.apple.product-type.application";
|
productType = "com.apple.product-type.application";
|
||||||
@ -129,6 +146,9 @@
|
|||||||
Base,
|
Base,
|
||||||
);
|
);
|
||||||
mainGroup = 884A45AA279F48C100D6E650;
|
mainGroup = 884A45AA279F48C100D6E650;
|
||||||
|
packageReferences = (
|
||||||
|
E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */,
|
||||||
|
);
|
||||||
productRefGroup = 884A45B4279F48C100D6E650 /* Products */;
|
productRefGroup = 884A45B4279F48C100D6E650 /* Products */;
|
||||||
projectDirPath = "";
|
projectDirPath = "";
|
||||||
projectRoot = "";
|
projectRoot = "";
|
||||||
@ -155,10 +175,14 @@
|
|||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
884A45CF27A5402D00D6E650 /* Response.swift in Sources */,
|
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */,
|
||||||
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */,
|
884A45B9279F48C100D6E650 /* ContentView.swift in Sources */,
|
||||||
884A45CD27A465F500D6E650 /* Client.swift in Sources */,
|
884A45CD27A465F500D6E650 /* Client.swift in Sources */,
|
||||||
|
E24EE77B280058240011CFD2 /* Message+Extensions.swift in Sources */,
|
||||||
|
E24EE77227FDCCC00011CFD2 /* Data+Extensions.swift in Sources */,
|
||||||
|
E24EE77427FF95920011CFD2 /* DeviceResponse.swift in Sources */,
|
||||||
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */,
|
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */,
|
||||||
|
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */,
|
||||||
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */,
|
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */,
|
||||||
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */,
|
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */,
|
||||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
||||||
@ -367,6 +391,25 @@
|
|||||||
defaultConfigurationName = Release;
|
defaultConfigurationName = Release;
|
||||||
};
|
};
|
||||||
/* End XCConfigurationList section */
|
/* End XCConfigurationList section */
|
||||||
|
|
||||||
|
/* Begin XCRemoteSwiftPackageReference section */
|
||||||
|
E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */ = {
|
||||||
|
isa = XCRemoteSwiftPackageReference;
|
||||||
|
repositoryURL = "https://github.com/apple/swift-nio.git";
|
||||||
|
requirement = {
|
||||||
|
kind = upToNextMajorVersion;
|
||||||
|
minimumVersion = 2.0.0;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
/* End XCRemoteSwiftPackageReference section */
|
||||||
|
|
||||||
|
/* Begin XCSwiftPackageProductDependency section */
|
||||||
|
E24EE77627FF95C00011CFD2 /* NIOCore */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
package = E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */;
|
||||||
|
productName = NIOCore;
|
||||||
|
};
|
||||||
|
/* End XCSwiftPackageProductDependency section */
|
||||||
};
|
};
|
||||||
rootObject = 884A45AB279F48C100D6E650 /* Project object */;
|
rootObject = 884A45AB279F48C100D6E650 /* Project object */;
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"pins" : [
|
||||||
|
{
|
||||||
|
"identity" : "swift-nio",
|
||||||
|
"kind" : "remoteSourceControl",
|
||||||
|
"location" : "https://github.com/apple/swift-nio.git",
|
||||||
|
"state" : {
|
||||||
|
"revision" : "d6e3762e0a5f7ede652559f53623baf11006e17c",
|
||||||
|
"version" : "2.39.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"version" : 2
|
||||||
|
}
|
Binary file not shown.
@ -4,10 +4,73 @@
|
|||||||
<dict>
|
<dict>
|
||||||
<key>SchemeUserState</key>
|
<key>SchemeUserState</key>
|
||||||
<dict>
|
<dict>
|
||||||
|
<key>BitcoinKit (Playground) 1.xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>5</integer>
|
||||||
|
</dict>
|
||||||
|
<key>BitcoinKit (Playground) 2.xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>6</integer>
|
||||||
|
</dict>
|
||||||
|
<key>BitcoinKit (Playground) 3.xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>7</integer>
|
||||||
|
</dict>
|
||||||
|
<key>BitcoinKit (Playground) 4.xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>8</integer>
|
||||||
|
</dict>
|
||||||
|
<key>BitcoinKit (Playground) 5.xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>9</integer>
|
||||||
|
</dict>
|
||||||
|
<key>BitcoinKit (Playground).xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>4</integer>
|
||||||
|
</dict>
|
||||||
|
<key>Demo (Playground) 1.xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>2</integer>
|
||||||
|
</dict>
|
||||||
|
<key>Demo (Playground) 2.xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>3</integer>
|
||||||
|
</dict>
|
||||||
|
<key>Demo (Playground).xcscheme</key>
|
||||||
|
<dict>
|
||||||
|
<key>isShown</key>
|
||||||
|
<false/>
|
||||||
|
<key>orderHint</key>
|
||||||
|
<integer>0</integer>
|
||||||
|
</dict>
|
||||||
<key>Sesame.xcscheme_^#shared#^_</key>
|
<key>Sesame.xcscheme_^#shared#^_</key>
|
||||||
<dict>
|
<dict>
|
||||||
<key>orderHint</key>
|
<key>orderHint</key>
|
||||||
<integer>0</integer>
|
<integer>1</integer>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
</dict>
|
</dict>
|
||||||
|
@ -13,86 +13,88 @@ struct Client {
|
|||||||
|
|
||||||
private enum RequestReponse: Error {
|
private enum RequestReponse: Error {
|
||||||
case requestFailed
|
case requestFailed
|
||||||
case unknownResponse
|
case unknownResponseData(Data)
|
||||||
|
case unknownResponseString(String)
|
||||||
case success(UInt8)
|
case success(UInt8)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deviceStatus() async throws -> ClientState {
|
func deviceStatus() async -> ClientState {
|
||||||
let url = server.appendingPathComponent("status")
|
let url = server.appendingPathComponent("status")
|
||||||
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
|
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
|
||||||
let response = await integerReponse(to: request)
|
let response = await integerReponse(to: request)
|
||||||
switch response {
|
switch response {
|
||||||
case .requestFailed:
|
case .requestFailed:
|
||||||
return .statusRequestFailed
|
return .deviceNotAvailable(.serverNotReached)
|
||||||
case .unknownResponse:
|
case .unknownResponseData(let data):
|
||||||
return .unknownDeviceStatus
|
return .internalError("Unknown status (\(data.count) bytes)")
|
||||||
|
case .unknownResponseString(let string):
|
||||||
|
return .internalError("Unknown status (\(string.prefix(15)))")
|
||||||
case .success(let int):
|
case .success(let int):
|
||||||
switch int {
|
switch int {
|
||||||
case 0:
|
case 0:
|
||||||
return .deviceDisconnected
|
return .deviceNotAvailable(.deviceDisconnected)
|
||||||
case 1:
|
case 1:
|
||||||
return .deviceConnected
|
return .ready
|
||||||
default:
|
default:
|
||||||
print("Unexpected device status '\(int)'")
|
return .internalError("Invalid status: \(int)")
|
||||||
return .unknownDeviceStatus
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func keyResponse(key: SymmetricKey, id: Int) async throws -> ClientState {
|
func send(_ message: Message) async throws -> (state: ClientState, response: Message?) {
|
||||||
let url = server.appendingPathComponent("key/\(id)")
|
let url = server.appendingPathComponent("message")
|
||||||
var request = URLRequest(url: url)
|
var request = URLRequest(url: url)
|
||||||
request.httpBody = key.data
|
request.httpBody = message.encoded
|
||||||
request.httpMethod = "POST"
|
request.httpMethod = "POST"
|
||||||
let response = await integerReponse(to: request)
|
guard let data = await fulfill(request) else {
|
||||||
switch response {
|
return (.deviceNotAvailable(.serverNotReached), nil)
|
||||||
case .requestFailed:
|
|
||||||
return .statusRequestFailed
|
|
||||||
case .unknownResponse:
|
|
||||||
return .unknownDeviceStatus
|
|
||||||
case .success(let int):
|
|
||||||
guard let status = KeyResult(rawValue: int) else {
|
|
||||||
print("Invalid key response: \(int)")
|
|
||||||
return .unknownDeviceStatus
|
|
||||||
}
|
|
||||||
return ClientState(keyResult: status)
|
|
||||||
}
|
}
|
||||||
|
guard let byte = data.first else {
|
||||||
|
return (.internalError("Empty response"), nil)
|
||||||
|
}
|
||||||
|
guard let status = MessageResult(rawValue: byte) else {
|
||||||
|
return (.internalError("Invalid message response: \(byte)"), nil)
|
||||||
|
}
|
||||||
|
let result = ClientState(keyResult: status)
|
||||||
|
guard data.count == Message.length + 1 else {
|
||||||
|
return (result, nil)
|
||||||
|
}
|
||||||
|
let messageData = Array(data.advanced(by: 1))
|
||||||
|
let message = Message(decodeFrom: messageData)
|
||||||
|
return (result, message)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func fulfill(_ request: URLRequest) async -> Result<Data, RequestReponse> {
|
private func fulfill(_ request: URLRequest) async -> Data? {
|
||||||
do {
|
do {
|
||||||
let (data, response) = try await URLSession.shared.data(for: request)
|
let (data, response) = try await URLSession.shared.data(for: request)
|
||||||
guard let code = (response as? HTTPURLResponse)?.statusCode else {
|
guard let code = (response as? HTTPURLResponse)?.statusCode else {
|
||||||
print("No response from server")
|
print("No response from server")
|
||||||
return .failure(.requestFailed)
|
return nil
|
||||||
}
|
}
|
||||||
guard code == 200 else {
|
guard code == 200 else {
|
||||||
print("Invalid server response \(code)")
|
print("Invalid server response \(code)")
|
||||||
return .failure(.requestFailed)
|
return nil
|
||||||
}
|
}
|
||||||
return .success(data)
|
return data
|
||||||
} catch {
|
} catch {
|
||||||
print("Request failed: \(error)")
|
print("Request failed: \(error)")
|
||||||
return .failure(.requestFailed)
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func integerReponse(to request: URLRequest) async -> RequestReponse {
|
private func integerReponse(to request: URLRequest) async -> RequestReponse {
|
||||||
let response = await fulfill(request)
|
guard let data = await fulfill(request) else {
|
||||||
switch response {
|
return .requestFailed
|
||||||
case .failure(let cause):
|
|
||||||
return cause
|
|
||||||
case .success(let data):
|
|
||||||
guard let string = String(data: data, encoding: .utf8) else {
|
|
||||||
print("Unexpected device status data: \([UInt8](data))")
|
|
||||||
return .unknownResponse
|
|
||||||
}
|
|
||||||
guard let int = UInt8(string) else {
|
|
||||||
print("Unexpected device status '\(string)'")
|
|
||||||
return .unknownResponse
|
|
||||||
}
|
|
||||||
return .success(int)
|
|
||||||
}
|
}
|
||||||
|
guard let string = String(data: data, encoding: .utf8) else {
|
||||||
|
print("Unexpected device status data: \([UInt8](data))")
|
||||||
|
return .unknownResponseData(data)
|
||||||
|
}
|
||||||
|
guard let int = UInt8(string) else {
|
||||||
|
print("Unexpected device status '\(string)'")
|
||||||
|
return .unknownResponseString(string)
|
||||||
|
}
|
||||||
|
return .success(int)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,137 +1,156 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum ClientState {
|
enum ConnectionError {
|
||||||
|
case serverNotReached
|
||||||
/// The initial state after app launch
|
|
||||||
case initial
|
|
||||||
|
|
||||||
/// There are no keys stored locally on the client. New keys must be generated before use.
|
|
||||||
case noKeysAvailable
|
|
||||||
|
|
||||||
/// New keys have been generated and can now be transmitted to the device.
|
|
||||||
case newKeysGenerated
|
|
||||||
|
|
||||||
/// The device status could not be determined
|
|
||||||
case statusRequestFailed
|
|
||||||
|
|
||||||
/// The status received from the server could not be decoded
|
|
||||||
case unknownDeviceStatus
|
|
||||||
|
|
||||||
/// The remote device is not connected (no socket opened)
|
|
||||||
case deviceDisconnected
|
case deviceDisconnected
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ConnectionError: CustomStringConvertible {
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .serverNotReached:
|
||||||
|
return "Server unavailable"
|
||||||
|
case .deviceDisconnected:
|
||||||
|
return "Device disconnected"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum RejectionCause {
|
||||||
|
case invalidCounter
|
||||||
|
case invalidTime
|
||||||
|
case invalidAuthentication
|
||||||
|
case timeout
|
||||||
|
}
|
||||||
|
|
||||||
|
extension RejectionCause: CustomStringConvertible {
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .invalidCounter:
|
||||||
|
return "Invalid counter"
|
||||||
|
case .invalidTime:
|
||||||
|
return "Invalid time"
|
||||||
|
case .invalidAuthentication:
|
||||||
|
return "Invalid authentication"
|
||||||
|
case .timeout:
|
||||||
|
return "Device not responding"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ClientState {
|
||||||
|
|
||||||
|
/// There is no key stored locally on the client. A new key must be generated before use.
|
||||||
|
case noKeyAvailable
|
||||||
|
|
||||||
|
/// The device status is being requested
|
||||||
|
case requestingStatus
|
||||||
|
|
||||||
|
/// The remote device is not connected (no socket opened)
|
||||||
|
case deviceNotAvailable(ConnectionError)
|
||||||
|
|
||||||
|
/// The device is connected and ready to receive a message
|
||||||
|
case ready
|
||||||
|
|
||||||
/// The device is connected and ready to receive a key
|
/// The message is being transmitted and a response is awaited
|
||||||
case deviceConnected
|
|
||||||
|
|
||||||
/// The key is being transmitted and a response is awaited
|
|
||||||
case waitingForResponse
|
case waitingForResponse
|
||||||
|
|
||||||
/// The transmitted key was rejected (multiple possible reasons)
|
/// The transmitted message was rejected (multiple possible reasons)
|
||||||
case keyRejected
|
case messageRejected(RejectionCause)
|
||||||
|
|
||||||
/// Internal errors with the implementation
|
|
||||||
case internalError
|
|
||||||
|
|
||||||
/// The configuration of the devices doesn't match
|
|
||||||
case configurationError
|
|
||||||
|
|
||||||
/// The device responded that the opening action was started
|
/// The device responded that the opening action was started
|
||||||
case openSesame
|
case openSesame
|
||||||
|
|
||||||
/// All keys have been used
|
case internalError(String)
|
||||||
case allKeysUsed
|
|
||||||
|
|
||||||
var canSendKey: Bool {
|
var canSendKey: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .deviceConnected, .openSesame, .keyRejected:
|
case .ready, .openSesame, .messageRejected:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(keyResult: KeyResult) {
|
init(keyResult: MessageResult) {
|
||||||
switch keyResult {
|
switch keyResult {
|
||||||
case .textReceived, .unexpectedSocketEvent, .unknownDeviceError:
|
case .messageAuthenticationFailed:
|
||||||
self = .unknownDeviceStatus
|
self = .messageRejected(.invalidAuthentication)
|
||||||
case .invalidPayloadSize, .invalidKeyIndex, .invalidKey:
|
case .messageTimeMismatch:
|
||||||
self = .configurationError
|
self = .messageRejected(.invalidTime)
|
||||||
case .keyAlreadyUsed, .keyWasSkipped:
|
case .messageCounterInvalid:
|
||||||
self = .keyRejected
|
self = .messageRejected(.invalidCounter)
|
||||||
case .keyAccepted:
|
case .deviceTimedOut:
|
||||||
|
self = .messageRejected(.timeout)
|
||||||
|
case .messageAccepted:
|
||||||
self = .openSesame
|
self = .openSesame
|
||||||
case .noBodyData, .corruptkeyData:
|
case .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent:
|
||||||
self = .internalError
|
self = .internalError(keyResult.description)
|
||||||
case .deviceNotConnected, .deviceTimedOut:
|
case .deviceNotConnected:
|
||||||
self = .deviceDisconnected
|
self = .deviceNotAvailable(.deviceDisconnected)
|
||||||
|
case .operationInProgress:
|
||||||
|
self = .waitingForResponse
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var description: String {
|
|
||||||
switch self {
|
|
||||||
case .initial:
|
|
||||||
return "Checking state..."
|
|
||||||
case .noKeysAvailable:
|
|
||||||
return "No keys found"
|
|
||||||
case .newKeysGenerated:
|
|
||||||
return "New keys generated"
|
|
||||||
case .deviceDisconnected:
|
|
||||||
return "Device not connected"
|
|
||||||
case .statusRequestFailed:
|
|
||||||
return "Unable to get device status"
|
|
||||||
case .unknownDeviceStatus:
|
|
||||||
return "Unknown device status"
|
|
||||||
case .deviceConnected:
|
|
||||||
return "Device connected"
|
|
||||||
case .waitingForResponse:
|
|
||||||
return "Waiting for response"
|
|
||||||
case .internalError:
|
|
||||||
return "An internal error occured"
|
|
||||||
case .configurationError:
|
|
||||||
return "Configuration error"
|
|
||||||
|
|
||||||
case .allKeysUsed:
|
|
||||||
return "No fresh keys available"
|
|
||||||
case .keyRejected:
|
|
||||||
return "The key was rejected"
|
|
||||||
case .openSesame:
|
|
||||||
return "Unlocked"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var openButtonText: String {
|
var openButtonText: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .initial, .statusRequestFailed, .unknownDeviceStatus, .deviceDisconnected, .newKeysGenerated, .configurationError, .internalError:
|
case .noKeyAvailable:
|
||||||
return "Connect"
|
return "Create key"
|
||||||
case .allKeysUsed, .noKeysAvailable:
|
default:
|
||||||
return "Disabled"
|
|
||||||
case .deviceConnected, .keyRejected, .openSesame:
|
|
||||||
return "Unlock"
|
return "Unlock"
|
||||||
case .waitingForResponse:
|
|
||||||
return "Unlocking..."
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var openButtonColor: Color {
|
var openButtonColor: Color {
|
||||||
switch self {
|
switch self {
|
||||||
case .initial, .newKeysGenerated, .statusRequestFailed, .waitingForResponse:
|
case .noKeyAvailable, .requestingStatus:
|
||||||
return .yellow
|
return .yellow
|
||||||
case .noKeysAvailable, .allKeysUsed, .deviceDisconnected, .unknownDeviceStatus, .keyRejected, .configurationError, .internalError:
|
case .deviceNotAvailable, .messageRejected, .internalError:
|
||||||
return .red
|
return .red
|
||||||
case .deviceConnected, .openSesame:
|
case .ready, .waitingForResponse, .openSesame:
|
||||||
return .green
|
return .green
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var openActionIsEnabled: Bool {
|
var openButtonIsEnabled: Bool {
|
||||||
switch self {
|
switch self {
|
||||||
case .allKeysUsed, .noKeysAvailable, .waitingForResponse:
|
case .requestingStatus, .deviceNotAvailable, .waitingForResponse:
|
||||||
return false
|
return false
|
||||||
default:
|
default:
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ClientState: Equatable {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension ClientState: CustomStringConvertible {
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .noKeyAvailable:
|
||||||
|
return "No key set."
|
||||||
|
case .requestingStatus:
|
||||||
|
return "Checking device status"
|
||||||
|
case .deviceNotAvailable(let status):
|
||||||
|
return status.description
|
||||||
|
case .ready:
|
||||||
|
return "Ready"
|
||||||
|
case .waitingForResponse:
|
||||||
|
return "Unlocking..."
|
||||||
|
case .messageRejected(let cause):
|
||||||
|
return cause.description
|
||||||
|
case .openSesame:
|
||||||
|
return "Unlocked"
|
||||||
|
case .internalError(let e):
|
||||||
|
return "Error: \(e)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,154 +1,189 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
import CryptoKit
|
import CryptoKit
|
||||||
|
|
||||||
let keyManager = try! KeyManagement()
|
|
||||||
let server = Client(server: URL(string: "https://christophhagen.de/sesame/")!)
|
let server = Client(server: URL(string: "https://christophhagen.de/sesame/")!)
|
||||||
|
|
||||||
struct ContentView: View {
|
struct ContentView: View {
|
||||||
|
|
||||||
|
@AppStorage("counter")
|
||||||
|
var nextMessageCounter: Int = 0
|
||||||
|
|
||||||
@State var state: ClientState = .initial
|
@State
|
||||||
|
var state: ClientState = .noKeyAvailable
|
||||||
var canShareKey = false
|
|
||||||
|
@State
|
||||||
@State var showNewKeyWarning = false
|
private var timer: Timer?
|
||||||
|
|
||||||
@State var showKeyGenerationFailedWarning = false
|
@State
|
||||||
|
private var hasActiveRequest = false
|
||||||
@State var showShareSheetForNewKeys = false
|
|
||||||
|
@State
|
||||||
@State var activeRequestCount = 0
|
private var responseTime: Date? = nil
|
||||||
|
|
||||||
var isPerformingRequests: Bool {
|
var isPerformingRequests: Bool {
|
||||||
activeRequestCount > 0
|
hasActiveRequest ||
|
||||||
|
state == .waitingForResponse
|
||||||
}
|
}
|
||||||
|
|
||||||
var keyText: String {
|
var buttonBackground: Color {
|
||||||
let totalKeys = keyManager.numberOfKeys
|
state.openButtonIsEnabled ?
|
||||||
guard totalKeys > 0 else {
|
.white.opacity(0.2) :
|
||||||
return "No keys available"
|
.gray.opacity(0.2)
|
||||||
}
|
|
||||||
let unusedKeys = keyManager.unusedKeyCount
|
|
||||||
guard unusedKeys > 0 else {
|
|
||||||
return "All keys used"
|
|
||||||
}
|
|
||||||
return "\(totalKeys - unusedKeys) / \(totalKeys) keys used"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let buttonBorderWidth: CGFloat = 3
|
||||||
|
|
||||||
|
var buttonColor: Color {
|
||||||
|
state.openButtonIsEnabled ? .white : .gray
|
||||||
|
}
|
||||||
|
|
||||||
private let buttonWidth: CGFloat = 200
|
private let buttonWidth: CGFloat = 200
|
||||||
|
|
||||||
private let topButtonHeight: CGFloat = 60
|
private let topButtonHeight: CGFloat = 60
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 20) {
|
GeometryReader { geo in
|
||||||
Text(keyText)
|
VStack(spacing: 20) {
|
||||||
Button("Generate new keys", action: {
|
Spacer()
|
||||||
showNewKeyWarning = true
|
HStack {
|
||||||
print("Key regeneration requested")
|
if isPerformingRequests {
|
||||||
})
|
ProgressView()
|
||||||
.padding()
|
.progressViewStyle(CircularProgressViewStyle())
|
||||||
.frame(width: buttonWidth, height: topButtonHeight)
|
}
|
||||||
.background(.blue)
|
Text(state.description)
|
||||||
.foregroundColor(.white)
|
.padding()
|
||||||
.cornerRadius(topButtonHeight / 2)
|
|
||||||
Button("Share one-time key", action: shareKey)
|
|
||||||
.padding()
|
|
||||||
.frame(width: buttonWidth, height: topButtonHeight)
|
|
||||||
.background(.mint)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.cornerRadius(topButtonHeight / 2)
|
|
||||||
.disabled(!canShareKey)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
HStack {
|
|
||||||
if isPerformingRequests {
|
|
||||||
ProgressView()
|
|
||||||
.progressViewStyle(CircularProgressViewStyle())
|
|
||||||
}
|
}
|
||||||
Text(state.description)
|
Button(state.openButtonText, action: mainButtonPressed)
|
||||||
.padding()
|
.frame(width: buttonWidth, height: buttonWidth, alignment: .center)
|
||||||
|
.background(buttonBackground)
|
||||||
|
.cornerRadius(buttonWidth / 2)
|
||||||
|
.overlay(RoundedRectangle(cornerRadius: buttonWidth / 2).stroke(lineWidth: buttonBorderWidth).foregroundColor(buttonColor))
|
||||||
|
.foregroundColor(buttonColor)
|
||||||
|
.font(.title)
|
||||||
|
.disabled(!state.openButtonIsEnabled)
|
||||||
|
.padding(20)
|
||||||
}
|
}
|
||||||
Button(state.openButtonText, action: mainButtonPressed)
|
.onAppear {
|
||||||
.frame(width: buttonWidth, height: 80, alignment: .center)
|
if KeyManagement.hasKey {
|
||||||
.background(state.openButtonColor)
|
state = .requestingStatus
|
||||||
.cornerRadius(100)
|
}
|
||||||
.foregroundColor(.white)
|
startRegularStatusUpdates()
|
||||||
.font(.title2)
|
}
|
||||||
.disabled(!state.openActionIsEnabled)
|
.onDisappear {
|
||||||
|
endRegularStatusUpdates()
|
||||||
|
}
|
||||||
|
.frame(width: geo.size.width, height: geo.size.height)
|
||||||
|
.background(state.openButtonColor)
|
||||||
|
.animation(.easeInOut, value: state.openButtonColor)
|
||||||
}
|
}
|
||||||
.padding(20)
|
|
||||||
.onAppear {
|
|
||||||
checkInitialDeviceStatus()
|
|
||||||
}.alert(isPresented: $showKeyGenerationFailedWarning) {
|
|
||||||
Alert(title: Text("The keys could not be generated"),
|
|
||||||
message: Text("All previous keys will be deleted and the lock will be blocked. Are you sure?"),
|
|
||||||
dismissButton: .default(Text("Okay")))
|
|
||||||
}.shareSheet(isPresented: $showShareSheetForNewKeys, items: [keyManager.exportFile])
|
|
||||||
.alert(isPresented: $showNewKeyWarning) {
|
|
||||||
Alert(title: Text("Generate new keys"),
|
|
||||||
message: Text("All previous keys will be deleted and the lock will be blocked. Are you sure?"),
|
|
||||||
primaryButton: .destructive(Text("Generate"), action: regenerateKeys),
|
|
||||||
secondaryButton: .cancel())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func mainButtonPressed() {
|
func mainButtonPressed() {
|
||||||
print("Main button pressed")
|
guard let key = KeyManagement.key?.remote else {
|
||||||
if state.canSendKey {
|
generateKey()
|
||||||
sendKey()
|
|
||||||
} else {
|
|
||||||
checkInitialDeviceStatus()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sendKey() {
|
|
||||||
guard let key = keyManager.useNextKey() else {
|
|
||||||
state = .allKeysUsed
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
state = .waitingForResponse
|
sendMessage(using: key)
|
||||||
activeRequestCount += 1
|
|
||||||
print("Sending key \(key.id)")
|
|
||||||
Task {
|
|
||||||
let newState = try await server.keyResponse(key: key.key, id: key.id)
|
|
||||||
activeRequestCount -= 1
|
|
||||||
state = newState
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkInitialDeviceStatus() {
|
func sendMessage(using key: SymmetricKey) {
|
||||||
|
let count = UInt32(nextMessageCounter)
|
||||||
|
let now = Date()
|
||||||
|
let content = Message.Content(
|
||||||
|
time: now.timestamp,
|
||||||
|
id: count)
|
||||||
|
let message = content.authenticate(using: key)
|
||||||
|
state = .waitingForResponse
|
||||||
|
print("Sending message \(count)")
|
||||||
|
Task {
|
||||||
|
let (newState, message) = try await server.send(message)
|
||||||
|
responseTime = now
|
||||||
|
state = newState
|
||||||
|
if let message = message {
|
||||||
|
processResponse(message, sendTime: now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func processResponse(_ message: Message, sendTime: Date) {
|
||||||
|
guard let key = KeyManagement.key?.device else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard message.isValid(using: key) else {
|
||||||
|
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 behind")
|
||||||
|
} else if time2 < 0 {
|
||||||
|
print("Device time behind by at least \(Int(-time2 * 1000)) ms ahead")
|
||||||
|
} else {
|
||||||
|
print("Device time synchronized")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startRegularStatusUpdates() {
|
||||||
|
guard timer == nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
timer = Timer.scheduledTimer(withTimeInterval: 5, repeats: true, block: checkDeviceStatus)
|
||||||
|
timer!.fire()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func endRegularStatusUpdates() {
|
||||||
|
timer?.invalidate()
|
||||||
|
timer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkDeviceStatus(_ timer: Timer) {
|
||||||
|
guard !hasActiveRequest else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
hasActiveRequest = true
|
||||||
print("Checking device status")
|
print("Checking device status")
|
||||||
Task {
|
Task {
|
||||||
do {
|
let newState = await server.deviceStatus()
|
||||||
activeRequestCount += 1
|
hasActiveRequest = false
|
||||||
let newState = try await server.deviceStatus()
|
switch state {
|
||||||
activeRequestCount -= 1
|
case .noKeyAvailable:
|
||||||
print("Device status: \(newState)")
|
return
|
||||||
switch newState {
|
case .requestingStatus, .deviceNotAvailable, .ready:
|
||||||
case .noKeysAvailable, .allKeysUsed:
|
state = newState
|
||||||
|
case .waitingForResponse:
|
||||||
|
return
|
||||||
|
case .messageRejected, .openSesame, .internalError:
|
||||||
|
guard let time = responseTime else {
|
||||||
|
state = newState
|
||||||
return
|
return
|
||||||
default:
|
}
|
||||||
|
responseTime = nil
|
||||||
|
// Wait at least 5 seconds after these states have been reached before changing the
|
||||||
|
let elapsed = Date.now.timeIntervalSince(time)
|
||||||
|
guard elapsed < 5 else {
|
||||||
|
state = newState
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let secondsToWait = Int(elapsed.rounded(.up))
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(secondsToWait)) {
|
||||||
state = newState
|
state = newState
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
print("Failed to get device status: \(error)")
|
|
||||||
state = .statusRequestFailed
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func regenerateKeys() {
|
func generateKey() {
|
||||||
print("Regenerate keys")
|
print("Regenerate key")
|
||||||
do {
|
KeyManagement.generateNewKeys()
|
||||||
try keyManager.regenerateKeys()
|
state = .requestingStatus
|
||||||
state = .newKeysGenerated
|
|
||||||
showKeyGenerationFailedWarning = false
|
|
||||||
showShareSheetForNewKeys = true
|
|
||||||
checkInitialDeviceStatus()
|
|
||||||
} catch {
|
|
||||||
state = .noKeysAvailable
|
|
||||||
showKeyGenerationFailedWarning = true
|
|
||||||
showShareSheetForNewKeys = false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func shareKey() {
|
func shareKey() {
|
||||||
@ -162,3 +197,14 @@ struct ContentView_Previews: PreviewProvider {
|
|||||||
.previewDevice("iPhone 8")
|
.previewDevice("iPhone 8")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension Date {
|
||||||
|
|
||||||
|
var timestamp: UInt32 {
|
||||||
|
UInt32(timeIntervalSince1970.rounded())
|
||||||
|
}
|
||||||
|
|
||||||
|
init(timestamp: UInt32) {
|
||||||
|
self.init(timeIntervalSince1970: TimeInterval(timestamp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
42
Sesame/Data+Extensions.swift
Normal file
42
Sesame/Data+Extensions.swift
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
extension Data {
|
||||||
|
|
||||||
|
public var hexEncoded: String {
|
||||||
|
return map { String(format: "%02hhx", $0) }.joined()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert 0 ... 9, a ... f, A ...F to their decimal value,
|
||||||
|
// return nil for all other input characters
|
||||||
|
private func decodeNibble(_ u: UInt16) -> UInt8? {
|
||||||
|
switch(u) {
|
||||||
|
case 0x30 ... 0x39:
|
||||||
|
return UInt8(u - 0x30)
|
||||||
|
case 0x41 ... 0x46:
|
||||||
|
return UInt8(u - 0x41 + 10)
|
||||||
|
case 0x61 ... 0x66:
|
||||||
|
return UInt8(u - 0x61 + 10)
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public init?(fromHexEncodedString string: String) {
|
||||||
|
let utf16 = string.utf16
|
||||||
|
self.init(capacity: utf16.count/2)
|
||||||
|
|
||||||
|
var i = utf16.startIndex
|
||||||
|
guard utf16.count % 2 == 0 else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
while i != utf16.endIndex {
|
||||||
|
guard let hi = decodeNibble(utf16[i]),
|
||||||
|
let lo = decodeNibble(utf16[utf16.index(i, offsetBy: 1, limitedBy: utf16.endIndex)!]) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var value = hi << 4 + lo
|
||||||
|
self.append(&value, count: 1)
|
||||||
|
i = utf16.index(i, offsetBy: 2, limitedBy: utf16.endIndex)!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
65
Sesame/DeviceResponse.swift
Normal file
65
Sesame/DeviceResponse.swift
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import Foundation
|
||||||
|
import NIOCore
|
||||||
|
|
||||||
|
|
||||||
|
struct DeviceResponse {
|
||||||
|
|
||||||
|
static var deviceTimedOut: DeviceResponse {
|
||||||
|
.init(event: .deviceTimedOut)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var deviceNotConnected: DeviceResponse {
|
||||||
|
.init(event: .deviceNotConnected)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var unexpectedSocketEvent: DeviceResponse {
|
||||||
|
.init(event: .unexpectedSocketEvent)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var invalidMessageData: DeviceResponse {
|
||||||
|
.init(event: .invalidMessageData)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var noBodyData: DeviceResponse {
|
||||||
|
.init(event: .noBodyData)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var operationInProgress: DeviceResponse {
|
||||||
|
.init(event: .operationInProgress)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The response to a key from the server
|
||||||
|
let event: MessageResult
|
||||||
|
|
||||||
|
/// The index of the next key to use
|
||||||
|
let response: Message?
|
||||||
|
|
||||||
|
init?(_ buffer: ByteBuffer) {
|
||||||
|
guard let byte = buffer.getBytes(at: 0, length: 1) else {
|
||||||
|
print("No bytes received from device")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
guard let event = MessageResult(rawValue: byte[0]) else {
|
||||||
|
print("Unknown response \(byte[0]) received from device")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.event = event
|
||||||
|
guard let data = buffer.getSlice(at: 1, length: Message.length) else {
|
||||||
|
self.response = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
self.response = Message(decodeFrom: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(event: MessageResult) {
|
||||||
|
self.event = event
|
||||||
|
self.response = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoded: Data {
|
||||||
|
guard let message = response else {
|
||||||
|
return Data([event.rawValue])
|
||||||
|
}
|
||||||
|
return Data([event.rawValue]) + message.encoded
|
||||||
|
}
|
||||||
|
}
|
@ -3,116 +3,94 @@ import CryptoKit
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
final class KeyManagement {
|
final class KeyManagement {
|
||||||
|
|
||||||
static let securityKeySize: SymmetricKeySize = .bits128
|
static let tag = "com.ch.sesame.key".data(using: .utf8)!
|
||||||
|
|
||||||
enum KeyError: Error {
|
private static let label = "sesame"
|
||||||
/// Keys which are already in use can't be exported
|
|
||||||
case exportAttemptOfUsedKeys
|
private static let keyType = kSecAttrKeyTypeEC
|
||||||
}
|
|
||||||
|
private static let keyClass = kSecAttrKeyClassSymmetric
|
||||||
static var documentsDirectory: URL {
|
|
||||||
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
private static let query: [String: Any] = [
|
||||||
return paths[0]
|
kSecClass as String: kSecClassInternetPassword,
|
||||||
}
|
kSecAttrAccount as String: "account",
|
||||||
|
kSecAttrServer as String: "christophhagen.de",
|
||||||
private let keyFile = KeyManagement.documentsDirectory.appendingPathComponent("keys")
|
]//kSecAttrLabel as String: "sesame"]
|
||||||
|
|
||||||
let exportFile = KeyManagement.documentsDirectory.appendingPathComponent("export.cpp")
|
private static func loadKeys() -> Data? {
|
||||||
|
var query = query
|
||||||
private var keys: [(key: SymmetricKey, used: Bool)] {
|
query[kSecReturnData as String] = kCFBooleanTrue
|
||||||
didSet {
|
|
||||||
do {
|
var item: CFTypeRef?
|
||||||
try saveKeys()
|
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||||
} catch {
|
guard status == errSecSuccess else {
|
||||||
print("Failed to save changed keys: \(error)")
|
print("Failed to get key: \(status)")
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var numberOfKeys: Int {
|
|
||||||
keys.count
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasUsedKeys: Bool {
|
|
||||||
keys.contains { $0.used }
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasUnusedKeys: Bool {
|
|
||||||
unusedKeyCount > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
var unusedKeyCount: Int {
|
|
||||||
guard let id = nextKeyId else {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return keys.count - id + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
var usedKeyCount: Int {
|
|
||||||
nextKeyId ?? keys.count
|
|
||||||
}
|
|
||||||
|
|
||||||
var lastKeyId: Int? {
|
|
||||||
keys.lastIndex { $0.used }
|
|
||||||
}
|
|
||||||
|
|
||||||
var nextKeyId: Int? {
|
|
||||||
let index = lastKeyId ?? -1 + 1
|
|
||||||
guard index < keys.count else {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return index
|
let key = item as! CFData
|
||||||
|
print("Key loaded from keychain")
|
||||||
|
return key as Data
|
||||||
}
|
}
|
||||||
|
|
||||||
init() throws {
|
private static func deleteKeys() {
|
||||||
guard FileManager.default.fileExists(atPath: keyFile.path) else {
|
let status = SecItemDelete(query as CFDictionary)
|
||||||
self.keys = []
|
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||||
|
print("Failed to remove key: \(status)")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let content = try String(contentsOf: keyFile)
|
print("Key removed from keychain")
|
||||||
self.keys = content.components(separatedBy: "\n")
|
|
||||||
.enumerated().compactMap { (index, line) -> (SymmetricKey, Bool)? in
|
|
||||||
let parts = line.components(separatedBy: ":")
|
|
||||||
guard parts.count == 2 else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
let keyData = Data(base64Encoded: parts[0])!
|
|
||||||
return (SymmetricKey(data: keyData), parts[1] != "0")
|
|
||||||
}
|
|
||||||
print("\(unusedKeyCount) / \(keys.count) keys remaining")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func useNextKey() -> (key: SymmetricKey, id: Int)? {
|
private static func saveKeys(_ data: Data) {
|
||||||
guard let index = nextKeyId else {
|
var query = query
|
||||||
return nil
|
query[kSecValueData as String] = data
|
||||||
|
let status = SecItemAdd(query as CFDictionary, nil)
|
||||||
|
guard status == errSecSuccess else {
|
||||||
|
print("Failed to store key: \(status)")
|
||||||
|
return
|
||||||
}
|
}
|
||||||
let key = keys[index].key
|
print("Key saved to keychain")
|
||||||
keys[index].used = true
|
|
||||||
return (key, index)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func regenerateKeys(count: Int = 100) throws {
|
private static var keyData: Data? = loadKeys() {
|
||||||
self.keys = Self.generateKeys(count: count)
|
didSet {
|
||||||
.map { ($0, false) }
|
guard let data = keyData else {
|
||||||
let keyString = keys.map { $0.key.codeString }.joined(separator: "\n")
|
deleteKeys()
|
||||||
try keyString.write(to: exportFile, atomically: false, encoding: .utf8)
|
return
|
||||||
}
|
|
||||||
|
|
||||||
private func saveKeys() throws {
|
|
||||||
let content = keys.map { key, used -> String in
|
|
||||||
let keyString = key.withUnsafeBytes {
|
|
||||||
return Data(Array($0)).base64EncodedString()
|
|
||||||
}
|
}
|
||||||
return keyString + ":" + (used ? "1" : "0")
|
saveKeys(data)
|
||||||
}.joined(separator: "\n")
|
|
||||||
try content.write(to: keyFile, atomically: true, encoding: .utf8)
|
|
||||||
print("Keys saved")
|
|
||||||
}
|
|
||||||
|
|
||||||
static func generateKeys(count: Int = 100) -> [SymmetricKey] {
|
|
||||||
(0..<count).map { _ in
|
|
||||||
SymmetricKey(size: securityKeySize)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static var hasKey: Bool {
|
||||||
|
key != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private(set) static var key: (device: SymmetricKey, remote: SymmetricKey)? {
|
||||||
|
get {
|
||||||
|
guard let data = keyData else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
let device = SymmetricKey(data: data.prefix(32))
|
||||||
|
let remote = SymmetricKey(data: data.advanced(by: 32))
|
||||||
|
return (device, remote)
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
guard let key = newValue else {
|
||||||
|
keyData = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keyData = key.device.data + key.remote.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static func generateNewKeys() {
|
||||||
|
let device = SymmetricKey(size: .bits256)
|
||||||
|
let remote = SymmetricKey(size: .bits256)
|
||||||
|
key = (device, remote)
|
||||||
|
print("New keys:")
|
||||||
|
print("Device: \(device.data.hexEncoded)")
|
||||||
|
print("Remote: \(remote.data.hexEncoded)")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
40
Sesame/Message+Extensions.swift
Normal file
40
Sesame/Message+Extensions.swift
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
//
|
||||||
|
// Message+Extensions.swift
|
||||||
|
// Sesame
|
||||||
|
//
|
||||||
|
// Created by CH on 08.04.22.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CryptoKit
|
||||||
|
|
||||||
|
extension Message {
|
||||||
|
|
||||||
|
static var length: Int {
|
||||||
|
SHA256Digest.byteCount + Content.length
|
||||||
|
}
|
||||||
|
|
||||||
|
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
||||||
|
let count = SHA256Digest.byteCount
|
||||||
|
self.mac = Data(data.prefix(count))
|
||||||
|
self.content = .init(decodeFrom: Array(data.dropFirst(count)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isValid(using key: SymmetricKey) -> Bool {
|
||||||
|
HMAC<SHA256>.isValidAuthenticationCode(mac, authenticating: content.encoded, using: key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Message.Content {
|
||||||
|
|
||||||
|
func authenticate(using key: SymmetricKey) -> Message {
|
||||||
|
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
|
||||||
|
return .init(mac: Data(mac.map { $0 }), content: self)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authenticateAndSerialize(using key: SymmetricKey) -> Data {
|
||||||
|
let encoded = self.encoded
|
||||||
|
let mac = HMAC<SHA256>.authenticationCode(for: encoded, using: key)
|
||||||
|
return Data(mac.map { $0 }) + encoded
|
||||||
|
}
|
||||||
|
}
|
78
Sesame/Message.swift
Normal file
78
Sesame/Message.swift
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
import Foundation
|
||||||
|
import NIOCore
|
||||||
|
|
||||||
|
struct Message: Equatable, Hashable {
|
||||||
|
|
||||||
|
struct Content: Equatable, Hashable {
|
||||||
|
|
||||||
|
let time: UInt32
|
||||||
|
|
||||||
|
let id: UInt32
|
||||||
|
|
||||||
|
init(time: UInt32, id: UInt32) {
|
||||||
|
self.time = time
|
||||||
|
self.id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
init<T: Sequence>(decodeFrom data: T) where T.Element == UInt8 {
|
||||||
|
self.time = UInt32(data: data.prefix(4))
|
||||||
|
self.id = UInt32(data: data.dropFirst(4))
|
||||||
|
}
|
||||||
|
|
||||||
|
static var length: Int {
|
||||||
|
MemoryLayout<UInt32>.size * 2
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoded: Data {
|
||||||
|
time.encoded + id.encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes: [UInt8] {
|
||||||
|
time.bytes + id.bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mac: Data
|
||||||
|
|
||||||
|
let content: Content
|
||||||
|
|
||||||
|
init(mac: Data, content: Content) {
|
||||||
|
self.mac = mac
|
||||||
|
self.content = content
|
||||||
|
}
|
||||||
|
|
||||||
|
init?(decodeFrom buffer: ByteBuffer) {
|
||||||
|
guard let data = buffer.getBytes(at: 0, length: Message.length) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
self.init(decodeFrom: data)
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoded: Data {
|
||||||
|
mac + content.encoded
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes: [UInt8] {
|
||||||
|
Array(mac) + content.bytes
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension UInt32 {
|
||||||
|
|
||||||
|
init<T: Sequence>(data: T) where T.Element == UInt8 {
|
||||||
|
self = data
|
||||||
|
.enumerated()
|
||||||
|
.map { UInt32($0.element) << ($0.offset * 8) }
|
||||||
|
.reduce(0, +)
|
||||||
|
}
|
||||||
|
|
||||||
|
var encoded: Data {
|
||||||
|
.init(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes: [UInt8] {
|
||||||
|
(0..<4).map {
|
||||||
|
UInt8((self >> ($0*8)) & 0xFF)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
Sesame/MessageResult.swift
Normal file
71
Sesame/MessageResult.swift
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import Foundation
|
||||||
|
|
||||||
|
/**
|
||||||
|
A result from sending a key to the device.
|
||||||
|
*/
|
||||||
|
enum MessageResult: UInt8 {
|
||||||
|
|
||||||
|
/// Text content was received, although binary data was expected
|
||||||
|
case textReceived = 1
|
||||||
|
|
||||||
|
/// A socket event on the device was unexpected (not binary data)
|
||||||
|
case unexpectedSocketEvent = 2
|
||||||
|
|
||||||
|
/// The size of the payload (i.e. message) was invalid, or the data could not be read
|
||||||
|
case invalidMessageData = 3
|
||||||
|
|
||||||
|
/// The transmitted message could not be authenticated using the key
|
||||||
|
case messageAuthenticationFailed = 4
|
||||||
|
|
||||||
|
/// The message time was not within the acceptable bounds
|
||||||
|
case messageTimeMismatch = 5
|
||||||
|
|
||||||
|
/// A later key has been used, invalidating this key (to prevent replay attacks after blocked communication)
|
||||||
|
case messageCounterInvalid = 6
|
||||||
|
|
||||||
|
/// The key was accepted by the device, and the door will be opened
|
||||||
|
case messageAccepted = 7
|
||||||
|
|
||||||
|
|
||||||
|
/// The request did not contain body data with the key
|
||||||
|
case noBodyData = 10
|
||||||
|
|
||||||
|
/// The device is not connected
|
||||||
|
case deviceNotConnected = 12
|
||||||
|
|
||||||
|
/// The device did not respond within the timeout
|
||||||
|
case deviceTimedOut = 13
|
||||||
|
|
||||||
|
/// Another message is being processed by the device
|
||||||
|
case operationInProgress = 14
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MessageResult: CustomStringConvertible {
|
||||||
|
|
||||||
|
var description: String {
|
||||||
|
switch self {
|
||||||
|
case .textReceived:
|
||||||
|
return "The device received unexpected text"
|
||||||
|
case .unexpectedSocketEvent:
|
||||||
|
return "Unexpected socket event for the device"
|
||||||
|
case .invalidMessageData:
|
||||||
|
return "Invalid message data"
|
||||||
|
case .messageAuthenticationFailed:
|
||||||
|
return "Message authentication failed"
|
||||||
|
case .messageTimeMismatch:
|
||||||
|
return "Message time invalid"
|
||||||
|
case .messageCounterInvalid:
|
||||||
|
return "Message counter invalid"
|
||||||
|
case .messageAccepted:
|
||||||
|
return "Message accepted"
|
||||||
|
case .noBodyData:
|
||||||
|
return "No body data included in the request"
|
||||||
|
case .deviceNotConnected:
|
||||||
|
return "Device not connected"
|
||||||
|
case .deviceTimedOut:
|
||||||
|
return "The device did not respond"
|
||||||
|
case .operationInProgress:
|
||||||
|
return "Another operation is in progress"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,80 +0,0 @@
|
|||||||
import Foundation
|
|
||||||
|
|
||||||
/**
|
|
||||||
A result from sending a key to the device.
|
|
||||||
*/
|
|
||||||
enum KeyResult: UInt8 {
|
|
||||||
|
|
||||||
/// Text content was received, although binary data was expected
|
|
||||||
case textReceived = 1
|
|
||||||
|
|
||||||
/// A socket event on the device was unexpected (not binary data)
|
|
||||||
case unexpectedSocketEvent = 2
|
|
||||||
|
|
||||||
/// The size of the payload (key id + key data, or just key) was invalid
|
|
||||||
case invalidPayloadSize = 3
|
|
||||||
|
|
||||||
/// The index of the key was out of bounds
|
|
||||||
case invalidKeyIndex = 4
|
|
||||||
|
|
||||||
/// The transmitted key data did not match the expected key
|
|
||||||
case invalidKey = 5
|
|
||||||
|
|
||||||
/// The key has been previously used and is no longer valid
|
|
||||||
case keyAlreadyUsed = 6
|
|
||||||
|
|
||||||
/// A later key has been used, invalidating this key (to prevent replay attacks after blocked communication)
|
|
||||||
case keyWasSkipped = 7
|
|
||||||
|
|
||||||
/// The key was accepted by the device, and the door will be opened
|
|
||||||
case keyAccepted = 8
|
|
||||||
|
|
||||||
/// The device produced an unknown error
|
|
||||||
case unknownDeviceError = 9
|
|
||||||
|
|
||||||
/// The request did not contain body data with the key
|
|
||||||
case noBodyData = 10
|
|
||||||
|
|
||||||
/// The body data could not be read
|
|
||||||
case corruptkeyData = 11
|
|
||||||
|
|
||||||
/// The device is not connected
|
|
||||||
case deviceNotConnected = 12
|
|
||||||
|
|
||||||
/// The device did not respond within the timeout
|
|
||||||
case deviceTimedOut = 13
|
|
||||||
}
|
|
||||||
|
|
||||||
extension KeyResult: CustomStringConvertible {
|
|
||||||
|
|
||||||
var description: String {
|
|
||||||
switch self {
|
|
||||||
case .invalidKeyIndex:
|
|
||||||
return "Invalid key id (too large)"
|
|
||||||
case .noBodyData:
|
|
||||||
return "No body data included in the request"
|
|
||||||
case .invalidPayloadSize:
|
|
||||||
return "Invalid key size"
|
|
||||||
case .corruptkeyData:
|
|
||||||
return "Key data corrupted"
|
|
||||||
case .deviceNotConnected:
|
|
||||||
return "Device not connected"
|
|
||||||
case .textReceived:
|
|
||||||
return "The device received unexpected text"
|
|
||||||
case .unexpectedSocketEvent:
|
|
||||||
return "Unexpected socket event for the device"
|
|
||||||
case .invalidKey:
|
|
||||||
return "The transmitted key was not correct"
|
|
||||||
case .keyAlreadyUsed:
|
|
||||||
return "The transmitted key was already used"
|
|
||||||
case .keyWasSkipped:
|
|
||||||
return "A newer key was already used"
|
|
||||||
case .keyAccepted:
|
|
||||||
return "Key successfully sent"
|
|
||||||
case .unknownDeviceError:
|
|
||||||
return "The device experienced an unknown error"
|
|
||||||
case .deviceTimedOut:
|
|
||||||
return "The device did not respond"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user