mirror of
https://gitlab.opencode.de/bwi/bundesmessenger/clients/bundesmessenger-ios.git
synced 2026-04-29 12:46:58 +02:00
Implement cross platform AES encryption support; add documentation
This commit is contained in:
committed by
Stefan Ceriu
parent
414c6dc34f
commit
b42e41957e
@@ -33,5 +33,6 @@ struct RendezvousTransportDetails: Codable {
|
||||
}
|
||||
|
||||
struct RendezvousMessage: Codable {
|
||||
var combined: String
|
||||
var iv: String
|
||||
var ciphertext: String
|
||||
}
|
||||
|
||||
@@ -26,14 +26,12 @@ enum RendezvousServiceError: Error {
|
||||
case transportError(RendezvousTransportError)
|
||||
}
|
||||
|
||||
enum RendezvousServiceCallback {
|
||||
case error(RendezvousServiceError)
|
||||
}
|
||||
|
||||
/// Algorithm name as per MSC3903
|
||||
enum RendezvousChannelAlgorithm: String {
|
||||
case ECDH_V1 = "m.rendezvous.v1.x25519-aes-sha256"
|
||||
case ECDH_V1 = "m.rendezvous.v1.curve25519-aes-sha256"
|
||||
}
|
||||
|
||||
/// Allows communication through a secure channel. Based on MSC3886 and MSC3903
|
||||
@MainActor
|
||||
class RendezvousService {
|
||||
private let transport: RendezvousTransportProtocol
|
||||
@@ -47,6 +45,7 @@ class RendezvousService {
|
||||
self.privateKey = Curve25519.KeyAgreement.PrivateKey()
|
||||
}
|
||||
|
||||
/// Creates a new rendezvous endpoint and publishes the creator's public key
|
||||
func createRendezvous() async -> Result<(), RendezvousServiceError> {
|
||||
let publicKeyString = self.privateKey.publicKey.rawRepresentation.base64EncodedString()
|
||||
let payload = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue,
|
||||
@@ -60,6 +59,8 @@ class RendezvousService {
|
||||
}
|
||||
}
|
||||
|
||||
/// After creation we need to wait for the pair to publish its public key as well
|
||||
/// At the end of this a symmetric key will be available for encryption
|
||||
func waitForInterlocutor() async -> Result<(), RendezvousServiceError> {
|
||||
switch await transport.get() {
|
||||
case .failure(let error):
|
||||
@@ -86,6 +87,8 @@ class RendezvousService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Joins an existing rendezvous and publishes the joiner's public key
|
||||
/// At the end of this a symmetric key will be available for encryption
|
||||
func joinRendezvous() async -> Result<(), RendezvousServiceError> {
|
||||
guard case let .success(data) = await transport.get() else {
|
||||
return .failure(.internalError)
|
||||
@@ -100,6 +103,8 @@ class RendezvousService {
|
||||
return .failure(.invalidInterlocutorKey)
|
||||
}
|
||||
|
||||
self.interlocutorPublicKey = interlocutorPublicKey
|
||||
|
||||
let publicKeyString = self.privateKey.publicKey.rawRepresentation.base64EncodedString()
|
||||
let payload = RendezvousDetails(algorithm: RendezvousChannelAlgorithm.ECDH_V1.rawValue,
|
||||
key: publicKeyString)
|
||||
@@ -118,17 +123,28 @@ class RendezvousService {
|
||||
return .success(())
|
||||
}
|
||||
|
||||
/// Send arbitrary data over the secure channel
|
||||
/// This will use the previously generated symmetric key to AES encrypt the payload
|
||||
/// - Parameter data: the data to be encrypted and sent
|
||||
/// - Returns: nothing if succeeded or a RendezvousServiceError failure
|
||||
func send(data: Data) async -> Result<(), RendezvousServiceError> {
|
||||
guard let symmetricKey = symmetricKey else {
|
||||
return .failure(.channelNotReady)
|
||||
}
|
||||
|
||||
guard let sealedBox = try? AES.GCM.seal(data, using: symmetricKey),
|
||||
let combinedData = sealedBox.combined else {
|
||||
|
||||
// Generate a custom random 256 bit nonce/iv as per MSC3903. The default one is 96 bit.
|
||||
guard let nonce = try? AES.GCM.Nonce(data: generateRandomData(ofLength: 32)),
|
||||
let sealedBox = try? AES.GCM.seal(data, using: symmetricKey, nonce: nonce) else {
|
||||
return .failure(.internalError)
|
||||
}
|
||||
|
||||
let body = RendezvousMessage(combined: combinedData.base64EncodedString())
|
||||
// The resulting cipher text needs to contain both the message and the authentication tag
|
||||
// in order to play nicely with other platforms
|
||||
var ciphertext = sealedBox.ciphertext
|
||||
ciphertext.append(contentsOf: sealedBox.tag)
|
||||
|
||||
let body = RendezvousMessage(iv: Data(nonce).base64EncodedString(),
|
||||
ciphertext: ciphertext.base64EncodedString())
|
||||
|
||||
switch await transport.send(body: body) {
|
||||
case .failure(let transportError):
|
||||
@@ -138,6 +154,9 @@ class RendezvousService {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Waits for and returns newly available rendezvous channel data
|
||||
/// - Returns: The unencrypted data or a RendezvousServiceError
|
||||
func receive() async -> Result<Data, RendezvousServiceError> {
|
||||
guard let symmetricKey = symmetricKey else {
|
||||
return .failure(.channelNotReady)
|
||||
@@ -151,8 +170,17 @@ class RendezvousService {
|
||||
return .failure(.decodingError)
|
||||
}
|
||||
|
||||
guard let combinedData = Data(base64Encoded: response.combined),
|
||||
let sealedBox = try? AES.GCM.SealedBox(combined: combinedData),
|
||||
guard let ciphertextData = Data(base64Encoded: response.ciphertext),
|
||||
let nonceData = Data(base64Encoded: response.iv),
|
||||
let nonce = try? AES.GCM.Nonce(data: nonceData) else {
|
||||
return .failure(.decodingError)
|
||||
}
|
||||
|
||||
// Split the ciphertext into the message and authentication tag data
|
||||
let messageData = ciphertextData.dropLast(16) // The last 16 bytes are the tag
|
||||
let tagData = ciphertextData.dropFirst(messageData.count)
|
||||
|
||||
guard let sealedBox = try? AES.GCM.SealedBox(nonce: nonce, ciphertext: messageData, tag: tagData),
|
||||
let messageData = try? AES.GCM.open(sealedBox, using: symmetricKey) else {
|
||||
return .failure(.decodingError)
|
||||
}
|
||||
@@ -164,7 +192,21 @@ class RendezvousService {
|
||||
// MARK: - Private
|
||||
|
||||
private func generateSymmetricKeyFrom(sharedSecret: SharedSecret) -> SymmetricKey {
|
||||
// MSC3903 asks for a 8 zero byte salt when deriving the keys
|
||||
let salt = Data(repeating: 0, count: 8)
|
||||
return sharedSecret.hkdfDerivedSymmetricKey(using: SHA256.self, salt: salt, sharedInfo: Data(), outputByteCount: 32)
|
||||
}
|
||||
|
||||
private func generateRandomData(ofLength length: Int) -> Data {
|
||||
var data = Data(count: length)
|
||||
_ = data.withUnsafeMutableBytes { pointer -> Int32 in
|
||||
if let baseAddress = pointer.baseAddress {
|
||||
return SecRandomCopyBytes(kSecRandomDefault, length, baseAddress)
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,15 +63,12 @@ class RendezvousTransport: RendezvousTransportProtocol {
|
||||
} else if httpURLResponse.statusCode == 304 {
|
||||
continuation.resume(returning: .success(nil))
|
||||
} else if httpURLResponse.statusCode == 200 {
|
||||
if httpURLResponse.allHeaderFields["Content-Type"] as? String != "application/json" {
|
||||
continuation.resume(returning: .success(nil))
|
||||
} else {
|
||||
if let etag = httpURLResponse.allHeaderFields["Etag"] as? String {
|
||||
self.currentEtag = etag
|
||||
}
|
||||
|
||||
continuation.resume(returning: .success(data))
|
||||
// The resouce changed, update the etag
|
||||
if let etag = httpURLResponse.allHeaderFields["Etag"] as? String {
|
||||
self.currentEtag = etag
|
||||
}
|
||||
|
||||
continuation.resume(returning: .success(data))
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
@@ -23,11 +23,21 @@ enum RendezvousTransportError: Error {
|
||||
case rendezvousCancelled
|
||||
}
|
||||
|
||||
/// HTTP based MSC3886 channel implementation
|
||||
@MainActor
|
||||
protocol RendezvousTransportProtocol {
|
||||
/// The current rendezvous endpoint.
|
||||
/// Automatically assigned after a successful creation
|
||||
var rendezvousURL: URL? { get }
|
||||
|
||||
/// Creates a new rendezvous point containing the body
|
||||
/// - Parameter body: arbitrary data to publish on the rendevous
|
||||
/// - Returns:a transport error in case of failure
|
||||
func create<T: Encodable>(body: T) async -> Result<(), RendezvousTransportError>
|
||||
|
||||
/// Waits for and returns newly availalbe rendezvous data
|
||||
func get() async -> Result<Data, RendezvousTransportError>
|
||||
|
||||
/// Publishes new rendezvous data
|
||||
func send<T: Encodable>(body: T) async -> Result<(), RendezvousTransportError>
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user