2022-01-29 18:59:42 +01:00
|
|
|
import Foundation
|
|
|
|
import CryptoKit
|
|
|
|
|
2023-04-11 18:18:31 +02:00
|
|
|
final class Client {
|
2022-01-29 18:59:42 +01:00
|
|
|
|
2023-04-11 18:18:31 +02:00
|
|
|
init() {}
|
2023-12-12 17:33:42 +01:00
|
|
|
|
2024-04-22 12:58:49 +02:00
|
|
|
func send(_ message: Message, to url: String, port: UInt16, through route: TransmissionType, using keys: KeySet) async -> ServerResponse {
|
2023-12-12 17:33:42 +01:00
|
|
|
let sentTime = Date.now
|
|
|
|
let signedMessage = message.authenticate(using: keys.remote)
|
|
|
|
let response: Message
|
|
|
|
switch route {
|
|
|
|
case .throughServer:
|
|
|
|
response = await send(signedMessage, toServerUrl: url, authenticateWith: keys.server, verifyUsing: keys.device)
|
|
|
|
|
|
|
|
case .overLocalWifi:
|
2024-04-22 12:58:49 +02:00
|
|
|
response = await send(signedMessage, toLocalDevice: url, port: port, verifyUsing: keys.device)
|
2023-12-12 17:33:42 +01:00
|
|
|
}
|
|
|
|
let receivedTime = Date.now
|
|
|
|
// Create best guess for creation of challenge.
|
|
|
|
let roundTripTime = receivedTime.timeIntervalSince(sentTime)
|
|
|
|
let serverChallenge = ServerChallenge(
|
|
|
|
creationDate: sentTime.addingTimeInterval(roundTripTime / 2),
|
|
|
|
message: response)
|
|
|
|
|
|
|
|
// Validate message content
|
|
|
|
guard response.result == .messageAccepted else {
|
|
|
|
print("Failure: \(response)")
|
|
|
|
return (response, nil)
|
|
|
|
}
|
|
|
|
|
|
|
|
guard response.clientChallenge == message.clientChallenge else {
|
|
|
|
print("Invalid client challenge: \(response)")
|
|
|
|
return (response.with(result: .invalidClientChallengeFromDevice), nil)
|
|
|
|
}
|
|
|
|
return (response, serverChallenge)
|
2023-04-11 18:18:31 +02:00
|
|
|
}
|
|
|
|
|
2024-04-22 12:58:49 +02:00
|
|
|
private func send(_ message: SignedMessage, toLocalDevice host: String, port: UInt16, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
|
|
|
let client = UDPClient(host: host, port: port)
|
|
|
|
let response: Data? = await withCheckedContinuation { continuation in
|
|
|
|
client.begin()
|
|
|
|
client.send(message: message.encoded) { res in
|
|
|
|
continuation.resume(returning: res)
|
|
|
|
}
|
2023-04-11 18:18:31 +02:00
|
|
|
}
|
2024-04-22 12:58:49 +02:00
|
|
|
guard let data = response else {
|
|
|
|
return message.message.with(result: .deviceNotConnected)
|
|
|
|
}
|
|
|
|
return decode(data, inResponseTo: message.message, verifyUsing: deviceKey)
|
2023-04-11 18:18:31 +02:00
|
|
|
}
|
|
|
|
|
2023-12-12 17:33:42 +01:00
|
|
|
private func send(_ message: SignedMessage, toServerUrl server: String, authenticateWith authToken: Data, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
|
|
|
guard let url = URL(string: server)?.appendingPathComponent(SesameRoute.postMessage.rawValue) else {
|
|
|
|
return message.message.with(result: .serverUrlInvalid)
|
2023-04-11 18:18:31 +02:00
|
|
|
}
|
2023-12-12 17:33:42 +01:00
|
|
|
|
2022-01-29 18:59:42 +01:00
|
|
|
var request = URLRequest(url: url)
|
2023-12-12 17:33:42 +01:00
|
|
|
request.httpBody = message.encoded
|
2022-01-29 18:59:42 +01:00
|
|
|
request.httpMethod = "POST"
|
2023-08-09 16:27:34 +02:00
|
|
|
request.timeoutInterval = 10
|
2023-12-12 17:33:42 +01:00
|
|
|
request.addValue(authToken.hexEncoded, forHTTPHeaderField: SesameHeader.authenticationHeader)
|
|
|
|
return await perform(request, inResponseTo: message.message, verifyUsing: deviceKey)
|
2023-04-11 18:18:31 +02:00
|
|
|
}
|
|
|
|
|
2023-12-12 17:33:42 +01:00
|
|
|
private func perform(_ request: URLRequest, inResponseTo message: Message, verifyUsing deviceKey: SymmetricKey) async -> Message {
|
|
|
|
let (response, responseData) = await fulfill(request)
|
|
|
|
guard response == .messageAccepted, let data = responseData else {
|
|
|
|
return message.with(result: response)
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
2024-04-22 12:58:49 +02:00
|
|
|
return decode(data, inResponseTo: message, verifyUsing: deviceKey)
|
|
|
|
}
|
|
|
|
|
|
|
|
private func decode(_ data: Data, inResponseTo message: Message, verifyUsing deviceKey: SymmetricKey) -> Message {
|
2023-12-12 17:33:42 +01:00
|
|
|
guard data.count == SignedMessage.size else {
|
|
|
|
print("[WARN] Received message with \(data.count) bytes (\(Array(data)))")
|
|
|
|
return message.with(result: .invalidMessageSizeFromDevice)
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
2023-12-12 17:33:42 +01:00
|
|
|
let decodedMessage: SignedMessage
|
|
|
|
do {
|
|
|
|
decodedMessage = try SignedMessage(decodeFrom: data)
|
|
|
|
} catch {
|
|
|
|
return message.with(result: error as! MessageResult)
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
2023-12-12 17:33:42 +01:00
|
|
|
guard decodedMessage.isValid(using: deviceKey) else {
|
|
|
|
return message.with(result: .invalidSignatureFromDevice)
|
2022-04-09 17:43:33 +02:00
|
|
|
}
|
2023-12-12 17:33:42 +01:00
|
|
|
return decodedMessage.message
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
|
|
|
|
2023-12-12 17:33:42 +01:00
|
|
|
private func fulfill(_ request: URLRequest) async -> (response: MessageResult, data: Data?) {
|
2022-01-29 18:59:42 +01:00
|
|
|
do {
|
|
|
|
let (data, response) = try await URLSession.shared.data(for: request)
|
|
|
|
guard let code = (response as? HTTPURLResponse)?.statusCode else {
|
2023-12-12 17:33:42 +01:00
|
|
|
return (.unexpectedUrlResponseType, nil)
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
2023-12-12 17:33:42 +01:00
|
|
|
return (.init(httpCode: code), data)
|
2022-01-29 18:59:42 +01:00
|
|
|
} catch {
|
|
|
|
print("Request failed: \(error)")
|
2023-12-12 17:33:42 +01:00
|
|
|
return (.deviceTimedOut, nil)
|
2022-01-29 18:59:42 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|