Initial version
This commit is contained in:
11
Sesame/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
11
Sesame/Assets.xcassets/AccentColor.colorset/Contents.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
98
Sesame/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
98
Sesame/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@ -0,0 +1,98 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "2x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "iphone",
|
||||
"scale" : "3x",
|
||||
"size" : "60x60"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "20x20"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "29x29"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "40x40"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "1x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "76x76"
|
||||
},
|
||||
{
|
||||
"idiom" : "ipad",
|
||||
"scale" : "2x",
|
||||
"size" : "83.5x83.5"
|
||||
},
|
||||
{
|
||||
"idiom" : "ios-marketing",
|
||||
"scale" : "1x",
|
||||
"size" : "1024x1024"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
6
Sesame/Assets.xcassets/Contents.json
Normal file
6
Sesame/Assets.xcassets/Contents.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
105
Sesame/Client.swift
Normal file
105
Sesame/Client.swift
Normal file
@ -0,0 +1,105 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
struct Client {
|
||||
|
||||
let server: URL
|
||||
|
||||
private let delegate = NeverCacheDelegate()
|
||||
|
||||
init(server: URL) {
|
||||
self.server = server
|
||||
}
|
||||
|
||||
private enum RequestReponse: Error {
|
||||
case requestFailed
|
||||
case unknownResponse
|
||||
case success(UInt8)
|
||||
}
|
||||
|
||||
func deviceStatus() async throws -> 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
|
||||
case .success(let int):
|
||||
switch int {
|
||||
case 0:
|
||||
return .deviceDisconnected
|
||||
case 1:
|
||||
return .deviceConnected
|
||||
default:
|
||||
print("Unexpected device status '\(int)'")
|
||||
return .unknownDeviceStatus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func keyResponse(key: SymmetricKey, id: Int) async throws -> ClientState {
|
||||
let url = server.appendingPathComponent("key/\(id)")
|
||||
var request = URLRequest(url: url)
|
||||
request.httpBody = key.data
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private func fulfill(_ request: URLRequest) async -> Result<Data, RequestReponse> {
|
||||
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)
|
||||
}
|
||||
guard code == 200 else {
|
||||
print("Invalid server response \(code)")
|
||||
return .failure(.requestFailed)
|
||||
}
|
||||
return .success(data)
|
||||
} catch {
|
||||
print("Request failed: \(error)")
|
||||
return .failure(.requestFailed)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class NeverCacheDelegate: NSObject, NSURLConnectionDataDelegate {
|
||||
|
||||
func connection(_ connection: NSURLConnection, willCacheResponse cachedResponse: CachedURLResponse) -> CachedURLResponse? {
|
||||
return nil
|
||||
}
|
||||
}
|
137
Sesame/ClientState.swift
Normal file
137
Sesame/ClientState.swift
Normal file
@ -0,0 +1,137 @@
|
||||
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)
|
||||
case deviceDisconnected
|
||||
|
||||
/// The device is connected and ready to receive a key
|
||||
case deviceConnected
|
||||
|
||||
/// The key 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 device responded that the opening action was started
|
||||
case openSesame
|
||||
|
||||
/// All keys have been used
|
||||
case allKeysUsed
|
||||
|
||||
var canSendKey: Bool {
|
||||
switch self {
|
||||
case .deviceConnected, .openSesame, .keyRejected:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
init(keyResult: KeyResult) {
|
||||
switch keyResult {
|
||||
case .textReceived, .unexpectedSocketEvent, .unknownDeviceError:
|
||||
self = .unknownDeviceStatus
|
||||
case .invalidPayloadSize, .invalidKeyIndex, .invalidKey:
|
||||
self = .configurationError
|
||||
case .keyAlreadyUsed, .keyWasSkipped:
|
||||
self = .keyRejected
|
||||
case .keyAccepted:
|
||||
self = .openSesame
|
||||
case .noBodyData, .corruptkeyData:
|
||||
self = .internalError
|
||||
case .deviceNotConnected, .deviceTimedOut:
|
||||
self = .deviceDisconnected
|
||||
}
|
||||
}
|
||||
|
||||
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:
|
||||
return "Unlock"
|
||||
case .waitingForResponse:
|
||||
return "Unlocking..."
|
||||
}
|
||||
}
|
||||
|
||||
var openButtonColor: Color {
|
||||
switch self {
|
||||
case .initial, .newKeysGenerated, .statusRequestFailed, .waitingForResponse:
|
||||
return .yellow
|
||||
case .noKeysAvailable, .allKeysUsed, .deviceDisconnected, .unknownDeviceStatus, .keyRejected, .configurationError, .internalError:
|
||||
return .red
|
||||
case .deviceConnected, .openSesame:
|
||||
return .green
|
||||
}
|
||||
}
|
||||
|
||||
var openActionIsEnabled: Bool {
|
||||
switch self {
|
||||
case .allKeysUsed, .noKeysAvailable, .waitingForResponse:
|
||||
return false
|
||||
default:
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
164
Sesame/ContentView.swift
Normal file
164
Sesame/ContentView.swift
Normal file
@ -0,0 +1,164 @@
|
||||
import SwiftUI
|
||||
import CryptoKit
|
||||
|
||||
let keyManager = try! KeyManagement()
|
||||
let server = Client(server: URL(string: "https://christophhagen.de/sesame/")!)
|
||||
|
||||
struct ContentView: View {
|
||||
|
||||
@State var state: ClientState = .initial
|
||||
|
||||
var canShareKey = false
|
||||
|
||||
@State var showNewKeyWarning = false
|
||||
|
||||
@State var showKeyGenerationFailedWarning = false
|
||||
|
||||
@State var showShareSheetForNewKeys = false
|
||||
|
||||
@State var activeRequestCount = 0
|
||||
|
||||
var isPerformingRequests: Bool {
|
||||
activeRequestCount > 0
|
||||
}
|
||||
|
||||
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"
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
Text(state.description)
|
||||
.padding()
|
||||
}
|
||||
Button(state.openButtonText, action: mainButtonPressed)
|
||||
.frame(width: buttonWidth, height: 80, alignment: .center)
|
||||
.background(state.openButtonColor)
|
||||
.cornerRadius(100)
|
||||
.foregroundColor(.white)
|
||||
.font(.title2)
|
||||
.disabled(!state.openActionIsEnabled)
|
||||
}
|
||||
.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
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
func checkInitialDeviceStatus() {
|
||||
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:
|
||||
return
|
||||
default:
|
||||
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 shareKey() {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ContentView()
|
||||
.previewDevice("iPhone 8")
|
||||
}
|
||||
}
|
118
Sesame/KeyManagement.swift
Normal file
118
Sesame/KeyManagement.swift
Normal file
@ -0,0 +1,118 @@
|
||||
import Foundation
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
return index
|
||||
}
|
||||
|
||||
init() throws {
|
||||
guard FileManager.default.fileExists(atPath: keyFile.path) else {
|
||||
self.keys = []
|
||||
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")
|
||||
}
|
||||
|
||||
func useNextKey() -> (key: SymmetricKey, id: Int)? {
|
||||
guard let index = nextKeyId else {
|
||||
return nil
|
||||
}
|
||||
let key = keys[index].key
|
||||
keys[index].used = true
|
||||
return (key, index)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,6 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
80
Sesame/Response.swift
Normal file
80
Sesame/Response.swift
Normal file
@ -0,0 +1,80 @@
|
||||
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"
|
||||
}
|
||||
}
|
||||
}
|
17
Sesame/SesameApp.swift
Normal file
17
Sesame/SesameApp.swift
Normal file
@ -0,0 +1,17 @@
|
||||
//
|
||||
// SesameApp.swift
|
||||
// Sesame
|
||||
//
|
||||
// Created by iMac on 24.01.22.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
@main
|
||||
struct SesameApp: App {
|
||||
var body: some Scene {
|
||||
WindowGroup {
|
||||
ContentView()
|
||||
}
|
||||
}
|
||||
}
|
21
Sesame/ShareSheet.swift
Normal file
21
Sesame/ShareSheet.swift
Normal file
@ -0,0 +1,21 @@
|
||||
import SwiftUI
|
||||
|
||||
extension UIApplication {
|
||||
|
||||
static let keyWindow = keyWindowScene?.windows.filter(\.isKeyWindow).first
|
||||
static let keyWindowScene = shared.connectedScenes.first { $0.activationState == .foregroundActive } as? UIWindowScene
|
||||
|
||||
}
|
||||
|
||||
extension View {
|
||||
|
||||
func shareSheet(isPresented: Binding<Bool>, items: [Any]) -> some View {
|
||||
guard isPresented.wrappedValue else { return self }
|
||||
let activityViewController = UIActivityViewController(activityItems: items, applicationActivities: nil)
|
||||
let presentedViewController = UIApplication.keyWindow?.rootViewController?.presentedViewController ?? UIApplication.keyWindow?.rootViewController
|
||||
activityViewController.completionWithItemsHandler = { _, _, _, _ in isPresented.wrappedValue = false }
|
||||
presentedViewController?.present(activityViewController, animated: true)
|
||||
return self
|
||||
}
|
||||
|
||||
}
|
21
Sesame/SymmetricKey+Extensions.swift
Normal file
21
Sesame/SymmetricKey+Extensions.swift
Normal file
@ -0,0 +1,21 @@
|
||||
import Foundation
|
||||
import CryptoKit
|
||||
|
||||
extension SymmetricKey {
|
||||
|
||||
var data: Data {
|
||||
withUnsafeBytes { Data(Array($0)) }
|
||||
}
|
||||
|
||||
var base64: String {
|
||||
data.base64EncodedString()
|
||||
}
|
||||
|
||||
var codeString: String {
|
||||
" {" +
|
||||
withUnsafeBytes {
|
||||
return Data(Array($0))
|
||||
}.map(String.init).joined(separator: ", ") +
|
||||
"},"
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user