mirror of
https://github.com/signalapp/libsignal.git
synced 2026-04-26 01:35:22 +02:00
242 lines
10 KiB
Swift
242 lines
10 KiB
Swift
//
|
|
// Copyright 2025 Signal Messenger, LLC.
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import Foundation
|
|
import SignalFfi
|
|
|
|
public enum KeyTransparency {
|
|
/// Protocol for a local persistent key transparency data store.
|
|
///
|
|
/// Contents of the store are opaque to the client and are only supposed to be
|
|
/// used by the ``Client``.
|
|
///
|
|
public protocol Store {
|
|
func getLastDistinguishedTreeHead() async -> Data?
|
|
func setLastDistinguishedTreeHead(to: Data) async
|
|
func getAccountData(for aci: Aci) async -> Data?
|
|
func setAccountData(_ data: Data, for aci: Aci) async
|
|
}
|
|
|
|
/// ACI descriptor for key transparency requests.
|
|
public struct AciInfo {
|
|
let aci: Aci
|
|
let identityKey: IdentityKey
|
|
}
|
|
|
|
/// E.164 descriptor for key transparency requests.
|
|
public struct E164Info {
|
|
let e164: String
|
|
let unidentifiedAccessKey: Data
|
|
}
|
|
|
|
/// Mode of the monitor operation.
|
|
///
|
|
/// If the newer version of account data is found in the key transparency
|
|
/// log, self-monitor will terminate with an error, but monitor for other
|
|
/// account will fall back to a full search and update the locally stored
|
|
/// data.
|
|
public enum MonitorMode {
|
|
case `self`
|
|
case other
|
|
}
|
|
|
|
/// Typed API to access the key transparency subsystem using an existing
|
|
/// unauthenticated chat connection.
|
|
///
|
|
/// Unlike ``UnauthenticatedChatConnection``, the client does
|
|
/// not export "raw" send/receive APIs, and instead uses them internally to
|
|
/// implement high-level key transparency operations.
|
|
///
|
|
/// Instances should be obtained by using the
|
|
/// ``UnauthenticatedChatConnection/keyTransparencyClient`` property.
|
|
///
|
|
/// Example usage:
|
|
///
|
|
/// ```swift
|
|
/// let network = Net(
|
|
/// env: .staging,
|
|
/// userAgent: "key-transparency-example"
|
|
/// )
|
|
///
|
|
/// let chat = try await network.connectUnauthenticatedChat()
|
|
/// chat.start(listener: MyChatListener())
|
|
///
|
|
/// // Successful completion means the search succeeded with no further steps required.
|
|
/// try await chat.keyTransparencyClient.search(
|
|
/// account: myAciInfo,
|
|
/// e164: myE164Info,
|
|
/// store: store
|
|
/// )
|
|
/// ```
|
|
public class Client {
|
|
private let chatConnection: UnauthenticatedChatConnection
|
|
private let asyncContext: TokioAsyncContext
|
|
private let environment: Net.Environment
|
|
|
|
internal init(
|
|
chatConnection: UnauthenticatedChatConnection,
|
|
asyncContext: TokioAsyncContext,
|
|
environment: Net.Environment
|
|
) {
|
|
self.chatConnection = chatConnection
|
|
self.asyncContext = asyncContext
|
|
self.environment = environment
|
|
}
|
|
|
|
/// Search for account information in the key transparency tree.
|
|
///
|
|
/// - Parameters:
|
|
/// - aciInfo: ACI identifying information.
|
|
/// - e164Info: E.164 identifying information. Optional.
|
|
/// - usernameHash: Hash of the username. Optional.
|
|
/// - store: Local key transparency storage. It will be queried for both
|
|
/// the account data and the latest distinguished tree head before sending the
|
|
/// server request and, if the request succeeds, will be updated with the
|
|
/// search operation results.
|
|
/// - Throws:
|
|
/// - `SignalError.keyTransparencyError` for errors related to key transparency logic, which
|
|
/// includes missing required fields in the serialized data. Retrying the search without
|
|
/// changing any of the arguments (including the state of the store) is unlikely to yield a
|
|
/// different result.
|
|
/// - `SignalError.keyTransparencyVerificationFailed` when it fails to
|
|
/// verify the data in key transparency server response, such as an incorrect proof or a
|
|
/// wrong signature.
|
|
///
|
|
/// Completes successfully if the search succeeds and the local state has been
|
|
/// updated to reflect the latest changes. If the operation fails, the UI should
|
|
/// be updated to notify the user of the failure.
|
|
public func search(
|
|
account aciInfo: AciInfo,
|
|
e164 e164Info: E164Info? = nil,
|
|
usernameHash: Data? = nil,
|
|
store: some Store
|
|
) async throws {
|
|
let e164 = e164Info?.e164
|
|
let uak = e164Info?.unidentifiedAccessKey
|
|
|
|
let accountData = await store.getAccountData(for: aciInfo.aci)
|
|
let distinguished = try await self.updateDistinguished(store)
|
|
|
|
let bytes = try await self.asyncContext.invokeAsyncFunction { promise, tokioContext in
|
|
try! withAllBorrowed(
|
|
self.chatConnection,
|
|
aciInfo.aci,
|
|
aciInfo.identityKey.publicKey,
|
|
uak,
|
|
usernameHash,
|
|
accountData,
|
|
distinguished
|
|
) { chatHandle, aciBytes, identityKeyHandle, uakBytes, hashBytes, accDataBytes, distinguishedBytes in
|
|
signal_key_transparency_search(
|
|
promise,
|
|
tokioContext.const(),
|
|
self.environment.rawValue,
|
|
chatHandle.const(),
|
|
aciBytes,
|
|
identityKeyHandle.const(),
|
|
e164,
|
|
uakBytes,
|
|
hashBytes,
|
|
accDataBytes,
|
|
distinguishedBytes
|
|
)
|
|
}
|
|
}
|
|
await store.setAccountData(Data(consuming: bytes), for: aciInfo.aci)
|
|
}
|
|
|
|
/// Perform a monitor operation for an account previously searched for.
|
|
///
|
|
/// - Parameters:
|
|
/// - mode: Mode of the monitor operation. See ``MonitorMode``.
|
|
/// - aciInfo: ACI identifying information.
|
|
/// - e164Info: E.164 identifying information. Optional.
|
|
/// - usernameHash: Hash of the username. Optional.
|
|
/// - store: Local key transparency storage. It will be queried for both
|
|
/// the account data and the latest distinguished tree head before sending the
|
|
/// server request and, if the request succeeds, will be updated with the
|
|
/// search operation results.
|
|
/// - Throws:
|
|
/// - `SignalError.keyTransparencyError` for errors related to key transparency logic, which
|
|
/// includes missing required fields in the serialized data. Retrying the search without
|
|
/// changing any of the arguments (including the state of the store) is unlikely to yield a
|
|
/// different result.
|
|
/// - `SignalError.keyTransparencyVerificationFailed` when it fails to
|
|
/// verify the data in key transparency server response, such as an incorrect proof or a
|
|
/// wrong signature. This is also the error thrown when new version
|
|
/// of account data is found in the key transparency log when
|
|
/// self-monitoring. See ``MonitorMode``.
|
|
///
|
|
/// Completes successfully if the search succeeds and the local state has been
|
|
/// updated to reflect the latest changes. If the operation fails, the UI should
|
|
/// be updated to notify the user of the failure.
|
|
public func monitor(
|
|
for mode: MonitorMode,
|
|
account aciInfo: AciInfo,
|
|
e164 e164Info: E164Info? = nil,
|
|
usernameHash: Data? = nil,
|
|
store: some Store
|
|
) async throws {
|
|
let e164 = e164Info?.e164
|
|
let uak = e164Info?.unidentifiedAccessKey
|
|
|
|
let accountData = await store.getAccountData(for: aciInfo.aci)
|
|
let distinguished = try await self.updateDistinguished(store)
|
|
|
|
let bytes = try await self.asyncContext.invokeAsyncFunction { promise, tokioContext in
|
|
try! withAllBorrowed(
|
|
self.chatConnection,
|
|
aciInfo.aci,
|
|
aciInfo.identityKey.publicKey,
|
|
uak,
|
|
usernameHash,
|
|
accountData,
|
|
distinguished
|
|
) { chatHandle, aciBytes, identityKeyHandle, uakBytes, hashBytes, accDataBytes, distinguishedBytes in
|
|
signal_key_transparency_monitor(
|
|
promise,
|
|
tokioContext.const(),
|
|
self.environment.rawValue,
|
|
chatHandle.const(),
|
|
aciBytes,
|
|
identityKeyHandle.const(),
|
|
e164,
|
|
uakBytes,
|
|
hashBytes,
|
|
accDataBytes,
|
|
distinguishedBytes,
|
|
mode == .self
|
|
)
|
|
}
|
|
}
|
|
await store.setAccountData(Data(consuming: bytes), for: aciInfo.aci)
|
|
}
|
|
|
|
private func updateDistinguished(_ store: some Store) async throws -> Data {
|
|
let knownDistinguished = await store.getLastDistinguishedTreeHead()
|
|
let latestDistinguished = try await getDistinguished(knownDistinguished)
|
|
await store.setLastDistinguishedTreeHead(to: latestDistinguished)
|
|
return latestDistinguished
|
|
}
|
|
|
|
internal func getDistinguished(
|
|
_ distinguished: Data? = nil
|
|
) async throws -> Data {
|
|
let bytes = try await self.asyncContext.invokeAsyncFunction { promise, tokioContext in
|
|
try! withAllBorrowed(self.chatConnection, distinguished) { chatHandle, distinguishedBytes in
|
|
signal_key_transparency_distinguished(
|
|
promise,
|
|
tokioContext.const(),
|
|
self.environment.rawValue,
|
|
chatHandle.const(),
|
|
distinguishedBytes
|
|
)
|
|
}
|
|
}
|
|
return Data(consuming: bytes)
|
|
}
|
|
}
|
|
}
|