Signal-iOS/SignalServiceKit/Messages/ValidatedIncomingEnvelope.swift
2026-04-01 16:34:13 -05:00

157 lines
5.5 KiB
Swift

//
// Copyright 2023 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import LibSignalClient
/// Represents an incoming envelope that's been validated but not decrypted.
///
/// If your method accepts this type, then it can rely on the validation
/// performed in the initializer because that's the ONLY way to produce an
/// object of this type.
///
/// This type also converts values it validates into new types with
/// stricter, compile-time guarantees. For example, the destination of the
/// envelope is converted to the two-case `localIdentity` enum rather than
/// an arbitrary `ServiceId`. Similarly, the envelope's type is converted to
/// a `Kind` enum; this enum eliminates the need to repeatedly consider all
/// possible envelope types in downstream code.
class ValidatedIncomingEnvelope {
let timestamp: UInt64
let serverTimestamp: UInt64
let envelope: SSKProtoEnvelope
let kind: Kind
let localIdentity: OWSIdentity
let localServiceId: ServiceId
init(_ envelope: SSKProtoEnvelope, localIdentifiers: LocalIdentifiers) throws {
self.envelope = envelope
guard envelope.timestamp >= 1, SDS.fitsInInt64(envelope.timestamp) else {
throw OWSAssertionError("Invalid timestamp.")
}
self.timestamp = envelope.timestamp
guard envelope.hasServerTimestamp, SDS.fitsInInt64(envelope.serverTimestamp) else {
throw OWSAssertionError("Invalid serverTimestamp.")
}
self.serverTimestamp = envelope.serverTimestamp
let kind: Kind
switch envelope.type {
case .receipt:
kind = .serverReceipt
case .ciphertext:
kind = .identifiedSender(.whisper)
case .prekeyBundle:
kind = .identifiedSender(.preKey)
case .plaintextContent:
kind = .identifiedSender(.plaintext)
case .unidentifiedSender:
kind = .unidentifiedSender
case .unknown, .none:
throw OWSGenericError("Unsupported type.")
}
self.kind = kind
let localServiceId = try Self.localServiceId(for: envelope, localIdentifiers: localIdentifiers)
self.localServiceId = localServiceId
self.localIdentity = { () -> OWSIdentity in
switch localServiceId.kind {
case .aci: return .aci
case .pni: return .pni
}
}()
try Self.validateEnvelopeKind(kind, for: localIdentity)
}
enum Kind {
case serverReceipt
case identifiedSender(CiphertextMessage.MessageType)
case unidentifiedSender
}
// MARK: - GUID
var serverGuid: UUID? {
return Self.parseServerGuid(fromEnvelope: self.envelope)
}
static func parseServerGuid(fromEnvelope envelope: SSKProtoEnvelope) -> UUID? {
if let serverGuidBinary = envelope.serverGuidBinary {
return UUID(data: serverGuidBinary)
}
if let serverGuidString = envelope.serverGuid {
return UUID(uuidString: serverGuidString)
}
return nil
}
// MARK: - Source
func validateSource<T: ServiceId>(_ type: T.Type) throws -> (T, DeviceId) {
guard
let sourceServiceId = T.parseFrom(
serviceIdBinary: envelope.sourceServiceIDBinary,
serviceIdString: envelope.sourceServiceID,
)
else {
throw OWSAssertionError("Invalid source.")
}
guard envelope.hasSourceDevice, let sourceDevice = DeviceId(validating: envelope.sourceDevice) else {
throw OWSAssertionError("Invalid source device.")
}
return (sourceServiceId, sourceDevice)
}
// MARK: - Destination
private static func localServiceId(
for envelope: SSKProtoEnvelope,
localIdentifiers: LocalIdentifiers,
) throws -> ServiceId {
let destinationServiceId: ServiceId
if envelope.destinationServiceID?.nilIfEmpty != nil || envelope.hasDestinationServiceIDBinary {
destinationServiceId = try ServiceId.parseFrom(
serviceIdBinary: envelope.destinationServiceIDBinary,
serviceIdString: envelope.destinationServiceID,
) ?? { () throws -> ServiceId in throw OWSGenericError("Couldn't parse destination.") }()
} else {
// Old, locally-persisted envelopes may not have a destination specified.
destinationServiceId = localIdentifiers.aci
}
switch destinationServiceId {
case localIdentifiers.aci, localIdentifiers.pni:
return destinationServiceId
default:
throw MessageProcessingError.wrongDestinationUuid
}
}
private static func validateEnvelopeKind(_ kind: Kind, for localIdentity: OWSIdentity) throws {
let canBeReceived: Bool = {
switch kind {
case .serverReceipt:
return true
case .identifiedSender(.whisper):
return localIdentity == .aci
case .identifiedSender(.preKey):
return true
case .identifiedSender(.senderKey):
return localIdentity == .aci
case .identifiedSender(.plaintext):
return localIdentity == .aci
case .identifiedSender:
return false
case .unidentifiedSender:
return localIdentity == .aci
}
}()
guard canBeReceived else {
throw MessageProcessingError.invalidMessageTypeForDestinationUuid
}
}
}