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 */; };
|
||||
884A45CB27A464C000D6E650 /* SymmetricKey+Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.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 */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
@ -30,7 +35,11 @@
|
||||
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>"; };
|
||||
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 */
|
||||
|
||||
/* Begin PBXFrameworksBuildPhase section */
|
||||
@ -38,6 +47,7 @@
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
E24EE77727FF95C00011CFD2 /* NIOCore in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
@ -64,15 +74,19 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
884A45B6279F48C100D6E650 /* SesameApp.swift */,
|
||||
E24EE77827FF95E00011CFD2 /* Message.swift */,
|
||||
E24EE77A280058240011CFD2 /* Message+Extensions.swift */,
|
||||
884A45B8279F48C100D6E650 /* ContentView.swift */,
|
||||
884A45CC27A465F500D6E650 /* Client.swift */,
|
||||
884A45CE27A5402D00D6E650 /* Response.swift */,
|
||||
884A45CE27A5402D00D6E650 /* MessageResult.swift */,
|
||||
884A45C827A43D7900D6E650 /* ClientState.swift */,
|
||||
884A45C627A429EF00D6E650 /* ShareSheet.swift */,
|
||||
884A45C4279F4BBE00D6E650 /* KeyManagement.swift */,
|
||||
E24EE77327FF95920011CFD2 /* DeviceResponse.swift */,
|
||||
884A45CA27A464C000D6E650 /* SymmetricKey+Extensions.swift */,
|
||||
884A45BA279F48C300D6E650 /* Assets.xcassets */,
|
||||
884A45BC279F48C300D6E650 /* Preview Content */,
|
||||
E24EE77127FDCCC00011CFD2 /* Data+Extensions.swift */,
|
||||
);
|
||||
path = Sesame;
|
||||
sourceTree = "<group>";
|
||||
@ -101,6 +115,9 @@
|
||||
dependencies = (
|
||||
);
|
||||
name = Sesame;
|
||||
packageProductDependencies = (
|
||||
E24EE77627FF95C00011CFD2 /* NIOCore */,
|
||||
);
|
||||
productName = Sesame;
|
||||
productReference = 884A45B3279F48C100D6E650 /* Sesame.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
@ -129,6 +146,9 @@
|
||||
Base,
|
||||
);
|
||||
mainGroup = 884A45AA279F48C100D6E650;
|
||||
packageReferences = (
|
||||
E24EE77527FF95C00011CFD2 /* XCRemoteSwiftPackageReference "swift-nio" */,
|
||||
);
|
||||
productRefGroup = 884A45B4279F48C100D6E650 /* Products */;
|
||||
projectDirPath = "";
|
||||
projectRoot = "";
|
||||
@ -155,10 +175,14 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
884A45CF27A5402D00D6E650 /* Response.swift in Sources */,
|
||||
884A45CF27A5402D00D6E650 /* MessageResult.swift in Sources */,
|
||||
884A45B9279F48C100D6E650 /* ContentView.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 */,
|
||||
E24EE77927FF95E00011CFD2 /* Message.swift in Sources */,
|
||||
884A45C927A43D7900D6E650 /* ClientState.swift in Sources */,
|
||||
884A45B7279F48C100D6E650 /* SesameApp.swift in Sources */,
|
||||
884A45C5279F4BBE00D6E650 /* KeyManagement.swift in Sources */,
|
||||
@ -367,6 +391,25 @@
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* 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 */;
|
||||
}
|
||||
|
@ -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>
|
||||
<key>SchemeUserState</key>
|
||||
<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>
|
||||
<dict>
|
||||
<key>orderHint</key>
|
||||
<integer>0</integer>
|
||||
<integer>1</integer>
|
||||
</dict>
|
||||
</dict>
|
||||
</dict>
|
||||
|
@ -13,86 +13,88 @@ struct Client {
|
||||
|
||||
private enum RequestReponse: Error {
|
||||
case requestFailed
|
||||
case unknownResponse
|
||||
case unknownResponseData(Data)
|
||||
case unknownResponseString(String)
|
||||
case success(UInt8)
|
||||
}
|
||||
|
||||
func deviceStatus() async throws -> ClientState {
|
||||
func deviceStatus() async -> ClientState {
|
||||
let url = server.appendingPathComponent("status")
|
||||
let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData)
|
||||
let response = await integerReponse(to: request)
|
||||
switch response {
|
||||
case .requestFailed:
|
||||
return .statusRequestFailed
|
||||
case .unknownResponse:
|
||||
return .unknownDeviceStatus
|
||||
return .deviceNotAvailable(.serverNotReached)
|
||||
case .unknownResponseData(let data):
|
||||
return .internalError("Unknown status (\(data.count) bytes)")
|
||||
case .unknownResponseString(let string):
|
||||
return .internalError("Unknown status (\(string.prefix(15)))")
|
||||
case .success(let int):
|
||||
switch int {
|
||||
case 0:
|
||||
return .deviceDisconnected
|
||||
return .deviceNotAvailable(.deviceDisconnected)
|
||||
case 1:
|
||||
return .deviceConnected
|
||||
return .ready
|
||||
default:
|
||||
print("Unexpected device status '\(int)'")
|
||||
return .unknownDeviceStatus
|
||||
return .internalError("Invalid status: \(int)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func keyResponse(key: SymmetricKey, id: Int) async throws -> ClientState {
|
||||
let url = server.appendingPathComponent("key/\(id)")
|
||||
func send(_ message: Message) async throws -> (state: ClientState, response: Message?) {
|
||||
let url = server.appendingPathComponent("message")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpBody = key.data
|
||||
request.httpBody = message.encoded
|
||||
request.httpMethod = "POST"
|
||||
let response = await integerReponse(to: request)
|
||||
switch response {
|
||||
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 data = await fulfill(request) else {
|
||||
return (.deviceNotAvailable(.serverNotReached), nil)
|
||||
}
|
||||
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 {
|
||||
let (data, response) = try await URLSession.shared.data(for: request)
|
||||
guard let code = (response as? HTTPURLResponse)?.statusCode else {
|
||||
print("No response from server")
|
||||
return .failure(.requestFailed)
|
||||
return nil
|
||||
}
|
||||
guard code == 200 else {
|
||||
print("Invalid server response \(code)")
|
||||
return .failure(.requestFailed)
|
||||
return nil
|
||||
}
|
||||
return .success(data)
|
||||
return data
|
||||
} catch {
|
||||
print("Request failed: \(error)")
|
||||
return .failure(.requestFailed)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
private func integerReponse(to request: URLRequest) async -> RequestReponse {
|
||||
let response = await fulfill(request)
|
||||
switch response {
|
||||
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 data = await fulfill(request) else {
|
||||
return .requestFailed
|
||||
}
|
||||
guard let string = String(data: data, encoding: .utf8) else {
|
||||
print("Unexpected device status data: \([UInt8](data))")
|
||||
return .unknownResponseData(data)
|
||||
}
|
||||
guard let int = UInt8(string) else {
|
||||
print("Unexpected device status '\(string)'")
|
||||
return .unknownResponseString(string)
|
||||
}
|
||||
return .success(int)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,137 +1,156 @@
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
enum ClientState {
|
||||
|
||||
/// 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)
|
||||
enum ConnectionError {
|
||||
case serverNotReached
|
||||
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
|
||||
case deviceConnected
|
||||
|
||||
/// The key is being transmitted and a response is awaited
|
||||
/// The message is being transmitted and a response is awaited
|
||||
case waitingForResponse
|
||||
|
||||
/// The transmitted key was rejected (multiple possible reasons)
|
||||
case keyRejected
|
||||
|
||||
/// Internal errors with the implementation
|
||||
case internalError
|
||||
|
||||
/// The configuration of the devices doesn't match
|
||||
case configurationError
|
||||
/// The transmitted message was rejected (multiple possible reasons)
|
||||
case messageRejected(RejectionCause)
|
||||
|
||||
/// The device responded that the opening action was started
|
||||
case openSesame
|
||||
|
||||
/// All keys have been used
|
||||
case allKeysUsed
|
||||
|
||||
case internalError(String)
|
||||
|
||||
var canSendKey: Bool {
|
||||
switch self {
|
||||
case .deviceConnected, .openSesame, .keyRejected:
|
||||
case .ready, .openSesame, .messageRejected:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init(keyResult: KeyResult) {
|
||||
init(keyResult: MessageResult) {
|
||||
switch keyResult {
|
||||
case .textReceived, .unexpectedSocketEvent, .unknownDeviceError:
|
||||
self = .unknownDeviceStatus
|
||||
case .invalidPayloadSize, .invalidKeyIndex, .invalidKey:
|
||||
self = .configurationError
|
||||
case .keyAlreadyUsed, .keyWasSkipped:
|
||||
self = .keyRejected
|
||||
case .keyAccepted:
|
||||
case .messageAuthenticationFailed:
|
||||
self = .messageRejected(.invalidAuthentication)
|
||||
case .messageTimeMismatch:
|
||||
self = .messageRejected(.invalidTime)
|
||||
case .messageCounterInvalid:
|
||||
self = .messageRejected(.invalidCounter)
|
||||
case .deviceTimedOut:
|
||||
self = .messageRejected(.timeout)
|
||||
case .messageAccepted:
|
||||
self = .openSesame
|
||||
case .noBodyData, .corruptkeyData:
|
||||
self = .internalError
|
||||
case .deviceNotConnected, .deviceTimedOut:
|
||||
self = .deviceDisconnected
|
||||
case .noBodyData, .invalidMessageData, .textReceived, .unexpectedSocketEvent:
|
||||
self = .internalError(keyResult.description)
|
||||
case .deviceNotConnected:
|
||||
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 {
|
||||
switch self {
|
||||
case .initial, .statusRequestFailed, .unknownDeviceStatus, .deviceDisconnected, .newKeysGenerated, .configurationError, .internalError:
|
||||
return "Connect"
|
||||
case .allKeysUsed, .noKeysAvailable:
|
||||
return "Disabled"
|
||||
case .deviceConnected, .keyRejected, .openSesame:
|
||||
case .noKeyAvailable:
|
||||
return "Create key"
|
||||
default:
|
||||
return "Unlock"
|
||||
case .waitingForResponse:
|
||||
return "Unlocking..."
|
||||
}
|
||||
}
|
||||
|
||||
var openButtonColor: Color {
|
||||
switch self {
|
||||
case .initial, .newKeysGenerated, .statusRequestFailed, .waitingForResponse:
|
||||
case .noKeyAvailable, .requestingStatus:
|
||||
return .yellow
|
||||
case .noKeysAvailable, .allKeysUsed, .deviceDisconnected, .unknownDeviceStatus, .keyRejected, .configurationError, .internalError:
|
||||
case .deviceNotAvailable, .messageRejected, .internalError:
|
||||
return .red
|
||||
case .deviceConnected, .openSesame:
|
||||
case .ready, .waitingForResponse, .openSesame:
|
||||
return .green
|
||||
}
|
||||
}
|
||||
|
||||
var openActionIsEnabled: Bool {
|
||||
var openButtonIsEnabled: Bool {
|
||||
switch self {
|
||||
case .allKeysUsed, .noKeysAvailable, .waitingForResponse:
|
||||
case .requestingStatus, .deviceNotAvailable, .waitingForResponse:
|
||||
return false
|
||||
default:
|
||||
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 CryptoKit
|
||||
|
||||
let keyManager = try! KeyManagement()
|
||||
let server = Client(server: URL(string: "https://christophhagen.de/sesame/")!)
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@AppStorage("counter")
|
||||
var nextMessageCounter: Int = 0
|
||||
|
||||
@State var state: ClientState = .initial
|
||||
|
||||
var canShareKey = false
|
||||
|
||||
@State var showNewKeyWarning = false
|
||||
|
||||
@State var showKeyGenerationFailedWarning = false
|
||||
|
||||
@State var showShareSheetForNewKeys = false
|
||||
|
||||
@State var activeRequestCount = 0
|
||||
|
||||
@State
|
||||
var state: ClientState = .noKeyAvailable
|
||||
|
||||
@State
|
||||
private var timer: Timer?
|
||||
|
||||
@State
|
||||
private var hasActiveRequest = false
|
||||
|
||||
@State
|
||||
private var responseTime: Date? = nil
|
||||
|
||||
var isPerformingRequests: Bool {
|
||||
activeRequestCount > 0
|
||||
hasActiveRequest ||
|
||||
state == .waitingForResponse
|
||||
}
|
||||
|
||||
var keyText: String {
|
||||
let totalKeys = keyManager.numberOfKeys
|
||||
guard totalKeys > 0 else {
|
||||
return "No keys available"
|
||||
}
|
||||
let unusedKeys = keyManager.unusedKeyCount
|
||||
guard unusedKeys > 0 else {
|
||||
return "All keys used"
|
||||
}
|
||||
return "\(totalKeys - unusedKeys) / \(totalKeys) keys used"
|
||||
|
||||
var buttonBackground: Color {
|
||||
state.openButtonIsEnabled ?
|
||||
.white.opacity(0.2) :
|
||||
.gray.opacity(0.2)
|
||||
}
|
||||
|
||||
|
||||
let buttonBorderWidth: CGFloat = 3
|
||||
|
||||
var buttonColor: Color {
|
||||
state.openButtonIsEnabled ? .white : .gray
|
||||
}
|
||||
|
||||
private let buttonWidth: CGFloat = 200
|
||||
|
||||
|
||||
private let topButtonHeight: CGFloat = 60
|
||||
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
Text(keyText)
|
||||
Button("Generate new keys", action: {
|
||||
showNewKeyWarning = true
|
||||
print("Key regeneration requested")
|
||||
})
|
||||
.padding()
|
||||
.frame(width: buttonWidth, height: topButtonHeight)
|
||||
.background(.blue)
|
||||
.foregroundColor(.white)
|
||||
.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())
|
||||
GeometryReader { geo in
|
||||
VStack(spacing: 20) {
|
||||
Spacer()
|
||||
HStack {
|
||||
if isPerformingRequests {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle())
|
||||
}
|
||||
Text(state.description)
|
||||
.padding()
|
||||
}
|
||||
Text(state.description)
|
||||
.padding()
|
||||
Button(state.openButtonText, action: mainButtonPressed)
|
||||
.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)
|
||||
.frame(width: buttonWidth, height: 80, alignment: .center)
|
||||
.background(state.openButtonColor)
|
||||
.cornerRadius(100)
|
||||
.foregroundColor(.white)
|
||||
.font(.title2)
|
||||
.disabled(!state.openActionIsEnabled)
|
||||
.onAppear {
|
||||
if KeyManagement.hasKey {
|
||||
state = .requestingStatus
|
||||
}
|
||||
startRegularStatusUpdates()
|
||||
}
|
||||
.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() {
|
||||
print("Main button pressed")
|
||||
if state.canSendKey {
|
||||
sendKey()
|
||||
} else {
|
||||
checkInitialDeviceStatus()
|
||||
}
|
||||
}
|
||||
|
||||
func sendKey() {
|
||||
guard let key = keyManager.useNextKey() else {
|
||||
state = .allKeysUsed
|
||||
guard let key = KeyManagement.key?.remote else {
|
||||
generateKey()
|
||||
return
|
||||
}
|
||||
state = .waitingForResponse
|
||||
activeRequestCount += 1
|
||||
print("Sending key \(key.id)")
|
||||
Task {
|
||||
let newState = try await server.keyResponse(key: key.key, id: key.id)
|
||||
activeRequestCount -= 1
|
||||
state = newState
|
||||
}
|
||||
sendMessage(using: key)
|
||||
}
|
||||
|
||||
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")
|
||||
Task {
|
||||
do {
|
||||
activeRequestCount += 1
|
||||
let newState = try await server.deviceStatus()
|
||||
activeRequestCount -= 1
|
||||
print("Device status: \(newState)")
|
||||
switch newState {
|
||||
case .noKeysAvailable, .allKeysUsed:
|
||||
let newState = await server.deviceStatus()
|
||||
hasActiveRequest = false
|
||||
switch state {
|
||||
case .noKeyAvailable:
|
||||
return
|
||||
case .requestingStatus, .deviceNotAvailable, .ready:
|
||||
state = newState
|
||||
case .waitingForResponse:
|
||||
return
|
||||
case .messageRejected, .openSesame, .internalError:
|
||||
guard let time = responseTime else {
|
||||
state = newState
|
||||
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
|
||||
}
|
||||
} catch {
|
||||
print("Failed to get device status: \(error)")
|
||||
state = .statusRequestFailed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func regenerateKeys() {
|
||||
print("Regenerate keys")
|
||||
do {
|
||||
try keyManager.regenerateKeys()
|
||||
state = .newKeysGenerated
|
||||
showKeyGenerationFailedWarning = false
|
||||
showShareSheetForNewKeys = true
|
||||
checkInitialDeviceStatus()
|
||||
} catch {
|
||||
state = .noKeysAvailable
|
||||
showKeyGenerationFailedWarning = true
|
||||
showShareSheetForNewKeys = false
|
||||
}
|
||||
func generateKey() {
|
||||
print("Regenerate key")
|
||||
KeyManagement.generateNewKeys()
|
||||
state = .requestingStatus
|
||||
}
|
||||
|
||||
func shareKey() {
|
||||
@ -162,3 +197,14 @@ struct ContentView_Previews: PreviewProvider {
|
||||
.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
|
||||
|
||||
final class KeyManagement {
|
||||
|
||||
static let securityKeySize: SymmetricKeySize = .bits128
|
||||
|
||||
enum KeyError: Error {
|
||||
/// Keys which are already in use can't be exported
|
||||
case exportAttemptOfUsedKeys
|
||||
}
|
||||
|
||||
static var documentsDirectory: URL {
|
||||
let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
||||
return paths[0]
|
||||
}
|
||||
|
||||
private let keyFile = KeyManagement.documentsDirectory.appendingPathComponent("keys")
|
||||
|
||||
let exportFile = KeyManagement.documentsDirectory.appendingPathComponent("export.cpp")
|
||||
|
||||
private var keys: [(key: SymmetricKey, used: Bool)] {
|
||||
didSet {
|
||||
do {
|
||||
try saveKeys()
|
||||
} catch {
|
||||
print("Failed to save changed keys: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
static let tag = "com.ch.sesame.key".data(using: .utf8)!
|
||||
|
||||
private static let label = "sesame"
|
||||
|
||||
private static let keyType = kSecAttrKeyTypeEC
|
||||
|
||||
private static let keyClass = kSecAttrKeyClassSymmetric
|
||||
|
||||
private static let query: [String: Any] = [
|
||||
kSecClass as String: kSecClassInternetPassword,
|
||||
kSecAttrAccount as String: "account",
|
||||
kSecAttrServer as String: "christophhagen.de",
|
||||
]//kSecAttrLabel as String: "sesame"]
|
||||
|
||||
private static func loadKeys() -> Data? {
|
||||
var query = query
|
||||
query[kSecReturnData as String] = kCFBooleanTrue
|
||||
|
||||
var item: CFTypeRef?
|
||||
let status = SecItemCopyMatching(query as CFDictionary, &item)
|
||||
guard status == errSecSuccess else {
|
||||
print("Failed to get key: \(status)")
|
||||
return nil
|
||||
}
|
||||
return index
|
||||
let key = item as! CFData
|
||||
print("Key loaded from keychain")
|
||||
return key as Data
|
||||
}
|
||||
|
||||
init() throws {
|
||||
guard FileManager.default.fileExists(atPath: keyFile.path) else {
|
||||
self.keys = []
|
||||
|
||||
private static func deleteKeys() {
|
||||
let status = SecItemDelete(query as CFDictionary)
|
||||
guard status == errSecSuccess || status == errSecItemNotFound else {
|
||||
print("Failed to remove key: \(status)")
|
||||
return
|
||||
}
|
||||
let content = try String(contentsOf: keyFile)
|
||||
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")
|
||||
print("Key removed from keychain")
|
||||
}
|
||||
|
||||
func useNextKey() -> (key: SymmetricKey, id: Int)? {
|
||||
guard let index = nextKeyId else {
|
||||
return nil
|
||||
|
||||
private static func saveKeys(_ data: Data) {
|
||||
var query = query
|
||||
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
|
||||
keys[index].used = true
|
||||
return (key, index)
|
||||
print("Key saved to keychain")
|
||||
}
|
||||
|
||||
func regenerateKeys(count: Int = 100) throws {
|
||||
self.keys = Self.generateKeys(count: count)
|
||||
.map { ($0, false) }
|
||||
let keyString = keys.map { $0.key.codeString }.joined(separator: "\n")
|
||||
try keyString.write(to: exportFile, atomically: false, encoding: .utf8)
|
||||
}
|
||||
|
||||
private func saveKeys() throws {
|
||||
let content = keys.map { key, used -> String in
|
||||
let keyString = key.withUnsafeBytes {
|
||||
return Data(Array($0)).base64EncodedString()
|
||||
|
||||
private static var keyData: Data? = loadKeys() {
|
||||
didSet {
|
||||
guard let data = keyData else {
|
||||
deleteKeys()
|
||||
return
|
||||
}
|
||||
return keyString + ":" + (used ? "1" : "0")
|
||||
}.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)
|
||||
saveKeys(data)
|
||||
}
|
||||
}
|
||||
|
||||
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