Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add Oblivious HTTP (OHTTP) client for iOS
Using the ohttp-rs and bhttp-rs Rust libraries to perform underlying encoding and encryption. This is exposed to Swift using URLRequest/Response types that are typical for the platform. Note that only iOS is targetted since Gecko projects have a builtin OHTTP solution within Necko.
- Loading branch information
1 parent
9049484
commit 53c80d2
Showing
14 changed files
with
1,150 additions
and
4 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
[package] | ||
name = "as-ohttp-client" | ||
version = "0.1.0" | ||
edition = "2021" | ||
authors = ["Ted Campbell <tcampbell@mozilla.com>"] | ||
description = "An Oblivious HTTP client for iOS applications" | ||
license = "MPL-2.0" | ||
|
||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html | ||
|
||
[dependencies] | ||
uniffi = "0.24.1" | ||
thiserror = "1.0" | ||
bhttp = "0.3" | ||
ohttp = { version = "0.3", default-features = false, features = ["client", "server", "rust-hpke"]} | ||
|
||
[build-dependencies] | ||
uniffi = { version = "0.24.1", features=["build"]} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
fn main() { | ||
uniffi::generate_scaffolding("./src/as_ohttp_client.udl").unwrap(); | ||
} |
107 changes: 107 additions & 0 deletions
107
components/as-ohttp-client/ios/ASOhttpClient/OhttpManager.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
/* This Source Code Form is subject to the terms of the Mozilla Public | ||
* License, v. 2.0. If a copy of the MPL was not distributed with this | ||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */ | ||
|
||
import Foundation | ||
|
||
class OhttpManager { | ||
// The OhttpManager communicates with the relay and key server using | ||
// URLSession.shared.data unless an alternative networking method is | ||
// provided with this signature. | ||
typealias NetworkFunction = (_: URLRequest) async throws -> (Data, URLResponse) | ||
|
||
// Global cache to caching Gateway encryption keys. Stale entries are | ||
// ignored and on Gateway errors the key used should be purged and retrieved | ||
// again next at next network attempt. | ||
static var keyCache = [URL: ([UInt8], Date)]() | ||
|
||
private var configUrl: String | ||
private var relayUrl: String | ||
private var network: NetworkFunction | ||
|
||
init(configUrl: String, | ||
relayUrl: String, | ||
network: @escaping NetworkFunction = URLSession.shared.data) | ||
{ | ||
self.configUrl = configUrl | ||
self.relayUrl = relayUrl | ||
self.network = network | ||
} | ||
|
||
private func fetchKey(url: URL) async throws -> [UInt8] { | ||
let request = URLRequest(url: url) | ||
if let (data, response) = try? await network(request) { | ||
if (response as! HTTPURLResponse).statusCode == 200 { | ||
return [UInt8](data) | ||
} | ||
} | ||
|
||
throw OhttpError.KeyFetchFailed(message: "Failed to fetch encryption key") | ||
} | ||
|
||
private func keyForGateway(gatewayConfigUrl: URL, ttl: TimeInterval) async throws -> [UInt8] { | ||
if let (data, timestamp) = Self.keyCache[gatewayConfigUrl] { | ||
if Date.now < timestamp + ttl { | ||
// Cache Hit! | ||
return data | ||
} | ||
|
||
Self.keyCache.removeValue(forKey: gatewayConfigUrl) | ||
} | ||
|
||
let data = try await fetchKey(url: gatewayConfigUrl) | ||
Self.keyCache[gatewayConfigUrl] = (data, Date.now) | ||
|
||
return data | ||
} | ||
|
||
private func invalidateKey() { | ||
Self.keyCache.removeValue(forKey: URL(string: configUrl)!) | ||
} | ||
|
||
func data(for request: URLRequest) async throws -> (Data, HTTPURLResponse) { | ||
// Get the encryption keys for Gateway | ||
let config = try await keyForGateway(gatewayConfigUrl: URL(string: configUrl)!, | ||
ttl: TimeInterval(3600)) | ||
|
||
// Create an encryption session for a request-response round-trip | ||
let session = try OhttpSession(config: config) | ||
|
||
// Encapsulate the URLRequest for the Target | ||
let encoded = try session.encapsulate(method: request.httpMethod ?? "GET", | ||
scheme: request.url!.scheme!, | ||
server: request.url!.host!, | ||
endpoint: request.url!.path, | ||
headers: request.allHTTPHeaderFields ?? [:], | ||
payload: [UInt8](request.httpBody ?? Data())) | ||
|
||
// Request from Client to Relay | ||
var request = URLRequest(url: URL(string: relayUrl)!) | ||
request.httpMethod = "POST" | ||
request.setValue("message/ohttp-req", forHTTPHeaderField: "Content-Type") | ||
request.httpBody = Data(encoded) | ||
|
||
let (data, response) = try await network(request) | ||
let httpResponse = response as! HTTPURLResponse | ||
|
||
// Decapsulation failures have these codes, so invalidate any cached | ||
// keys in case the gateway has changed them. | ||
if httpResponse.statusCode == 400 || | ||
httpResponse.statusCode == 401 | ||
{ | ||
invalidateKey() | ||
} | ||
|
||
guard httpResponse.statusCode == 200 else { | ||
throw OhttpError.RelayFailed(message: "Network errors communicating with Relay / Gateway") | ||
} | ||
|
||
// Decapsulate the Target response into a HTTPURLResponse | ||
let message = try session.decapsulate(encoded: [UInt8](data)) | ||
return (Data(message.payload), | ||
HTTPURLResponse(url: request.url!, | ||
statusCode: Int(message.statusCode), | ||
httpVersion: "HTTP/1.1", | ||
headerFields: message.headers)!) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
namespace as_ohttp_client { | ||
}; | ||
|
||
[Error] | ||
enum OhttpError { | ||
"KeyFetchFailed", | ||
"MalformedKeyConfig", | ||
"UnsupportedKeyConfig", | ||
"InvalidSession", | ||
"RelayFailed", | ||
"CannotEncodeMessage", | ||
"MalformedMessage", | ||
"DuplicateHeaders", | ||
}; | ||
|
||
// The decrypted response from the Gateway/Target | ||
dictionary OhttpResponse { | ||
u16 status_code; | ||
record<string, string> headers; | ||
sequence<u8> payload; | ||
}; | ||
|
||
// Each OHTTP request-reply exchange needs to create an OhttpSession | ||
// object to manage encryption state. | ||
interface OhttpSession { | ||
// Initialize encryption state based on specific Gateway key config | ||
[Throws=OhttpError] | ||
constructor([ByRef] sequence<u8> config); | ||
|
||
// Encapsulate an HTTP request as Binary HTTP and then encrypt that | ||
// payload using HPKE. The caller is reponsible for sending the | ||
// resulting message to the Relay. | ||
[Throws=OhttpError] | ||
sequence<u8> encapsulate([ByRef] string method, | ||
[ByRef] string scheme, | ||
[ByRef] string server, | ||
[ByRef] string endpoint, | ||
record<string, string> headers, | ||
[ByRef] sequence<u8> payload); | ||
|
||
// Decypt and unpack the response from the Relay for the previously | ||
// encapsulated request. You must use the same OhttpSession that | ||
// generated the request message. | ||
[Throws=OhttpError] | ||
OhttpResponse decapsulate([ByRef] sequence<u8> encoded); | ||
}; | ||
|
||
dictionary TestServerRequest { | ||
string method; | ||
string scheme; | ||
string server; | ||
string endpoint; | ||
record<string, string> headers; | ||
sequence<u8> payload; | ||
}; | ||
|
||
// A testing interface for decrypting and responding to OHTTP messages. This | ||
// will on malformed data and should only be used for testing. | ||
interface OhttpTestServer { | ||
constructor(); | ||
|
||
sequence<u8> get_config(); | ||
|
||
[Throws=OhttpError] | ||
TestServerRequest receive([ByRef] sequence<u8> message); | ||
|
||
[Throws=OhttpError] | ||
sequence<u8> respond(OhttpResponse response); | ||
}; |
Oops, something went wrong.