237 lines
8.7 KiB
Swift
237 lines
8.7 KiB
Swift
//
|
|
// Copyright 2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
//
|
|
|
|
import LibSignalClient
|
|
|
|
private func validate(_ condition: @autoclosure () -> Bool,
|
|
file: String = #file,
|
|
function: String = #function,
|
|
line: Int = #line) {
|
|
#if DEBUG
|
|
// TESTABLE_BUILD is too broad; a large test case takes 0.22s to serialize with validation and 0.13s without.
|
|
owsAssertDebug(condition(), file: file, function: function, line: line)
|
|
#endif
|
|
}
|
|
|
|
private func prependKeyType(to data: Data) -> Data {
|
|
validate(data.count == 32)
|
|
return [PublicKey.Constants.keyTypeDJB] + data
|
|
}
|
|
|
|
private func removeKeyType(from data: Data) -> Data {
|
|
validate((try? PublicKey(data)) != nil)
|
|
return data.dropFirst()
|
|
}
|
|
|
|
extension LegacyChainKey {
|
|
fileprivate func buildProto() -> SessionRecordProtos_SessionStructure.Chain.ChainKey {
|
|
var result = SessionRecordProtos_SessionStructure.Chain.ChainKey()
|
|
|
|
result.index = UInt32(index)
|
|
result.key = key
|
|
|
|
return result
|
|
}
|
|
|
|
fileprivate convenience init(_ proto: SessionRecordProtos_SessionStructure.Chain.ChainKey) {
|
|
validate(proto.unknownFields.data.isEmpty)
|
|
|
|
self.init(data: proto.key, index: Int32(proto.index))
|
|
}
|
|
}
|
|
|
|
extension LegacyMessageKeys {
|
|
fileprivate func buildProto() -> SessionRecordProtos_SessionStructure.Chain.MessageKey {
|
|
validate(cipherKey.count == 32)
|
|
validate(macKey.count == 32)
|
|
validate(iv.count == 16)
|
|
|
|
var result = SessionRecordProtos_SessionStructure.Chain.MessageKey()
|
|
|
|
result.index = UInt32(index)
|
|
result.cipherKey = cipherKey
|
|
result.macKey = macKey
|
|
result.iv = iv
|
|
|
|
return result
|
|
}
|
|
|
|
fileprivate convenience init(_ proto: SessionRecordProtos_SessionStructure.Chain.MessageKey) {
|
|
validate(proto.unknownFields.data.isEmpty)
|
|
validate(proto.cipherKey.count == 32)
|
|
validate(proto.macKey.count == 32)
|
|
validate(proto.iv.count == 16)
|
|
|
|
self.init(cipherKey: proto.cipherKey, macKey: proto.macKey, iv: proto.iv, index: Int32(proto.index))
|
|
}
|
|
}
|
|
|
|
extension LegacyReceivingChain {
|
|
fileprivate func buildProto() -> SessionRecordProtos_SessionStructure.Chain {
|
|
validate(senderRatchetKey.count == 32)
|
|
|
|
var result = SessionRecordProtos_SessionStructure.Chain()
|
|
|
|
result.senderRatchetKey = prependKeyType(to: senderRatchetKey)
|
|
result.chainKey = chainKey.buildProto()
|
|
for messageKeys in messageKeysList {
|
|
let messageKeys = messageKeys as! LegacyMessageKeys
|
|
result.messageKeys.append(messageKeys.buildProto())
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
fileprivate convenience init(_ proto: SessionRecordProtos_SessionStructure.Chain) {
|
|
validate(proto.unknownFields.data.isEmpty)
|
|
|
|
self.init(chainKey: LegacyChainKey(proto.chainKey), senderRatchetKey: removeKeyType(from: proto.senderRatchetKey))
|
|
for messageKeysProto in proto.messageKeys {
|
|
messageKeysList.add(LegacyMessageKeys(messageKeysProto))
|
|
}
|
|
}
|
|
}
|
|
|
|
extension LegacyPendingPreKey {
|
|
fileprivate func buildProto() -> SessionRecordProtos_SessionStructure.PendingPreKey {
|
|
var result = SessionRecordProtos_SessionStructure.PendingPreKey()
|
|
|
|
// AxolotlKit uses -1 to represent "no pre-key ID".
|
|
if preKeyId >= 0 {
|
|
result.preKeyID = UInt32(preKeyId)
|
|
}
|
|
result.signedPreKeyID = signedPreKeyId
|
|
result.baseKey = prependKeyType(to: baseKey)
|
|
|
|
return result
|
|
}
|
|
}
|
|
|
|
extension LegacySessionState {
|
|
private func buildSenderChain() -> SessionRecordProtos_SessionStructure.Chain? {
|
|
guard let ratchetKeyPair = senderRatchetKeyPair() else {
|
|
return nil
|
|
}
|
|
|
|
var result = SessionRecordProtos_SessionStructure.Chain()
|
|
|
|
result.senderRatchetKey = Data(ratchetKeyPair.identityKeyPair.publicKey.serialize())
|
|
result.senderRatchetKeyPrivate = Data(ratchetKeyPair.identityKeyPair.privateKey.serialize())
|
|
result.chainKey = senderChainKey().buildProto()
|
|
|
|
return result
|
|
}
|
|
|
|
fileprivate func buildProto() -> SessionRecordProtos_SessionStructure? {
|
|
guard let rootKey = self.rootKey else { return nil }
|
|
|
|
var result = SessionRecordProtos_SessionStructure()
|
|
|
|
result.sessionVersion = UInt32(version)
|
|
|
|
if let localIdentityKey = self.localIdentityKey {
|
|
result.localIdentityPublic = prependKeyType(to: localIdentityKey)
|
|
}
|
|
if let remoteIdentityKey = self.remoteIdentityKey {
|
|
result.remoteIdentityPublic = prependKeyType(to: remoteIdentityKey)
|
|
}
|
|
result.rootKey = rootKey.keyData
|
|
result.previousCounter = UInt32(previousCounter)
|
|
if let chain = buildSenderChain() {
|
|
result.senderChain = chain
|
|
}
|
|
for receivingChain in receivingChains {
|
|
result.receiverChains.append(receivingChain.buildProto())
|
|
}
|
|
if let pendingPreKey = unacknowledgedPreKeyMessageItems() {
|
|
result.pendingPreKey = pendingPreKey.buildProto()
|
|
}
|
|
result.remoteRegistrationID = UInt32(bitPattern: remoteRegistrationId)
|
|
result.localRegistrationID = UInt32(bitPattern: localRegistrationId)
|
|
if let aliceBaseKey = self.aliceBaseKey {
|
|
result.aliceBaseKey = prependKeyType(to: aliceBaseKey)
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
fileprivate convenience init(_ proto: SessionRecordProtos_SessionStructure) {
|
|
validate(proto.unknownFields.data.isEmpty)
|
|
|
|
self.init()
|
|
version = Int32(proto.sessionVersion)
|
|
localIdentityKey = removeKeyType(from: proto.localIdentityPublic)
|
|
remoteIdentityKey = removeKeyType(from: proto.remoteIdentityPublic)
|
|
rootKey = LegacyRootKey(data: proto.rootKey)
|
|
previousCounter = Int32(proto.previousCounter)
|
|
if proto.hasSenderChain {
|
|
validate(proto.senderChain.unknownFields.data.isEmpty)
|
|
|
|
let senderRatchetKey = try? PublicKey(proto.senderChain.senderRatchetKey)
|
|
validate(senderRatchetKey != nil)
|
|
let senderRatchetKeyPrivate = try? PrivateKey(proto.senderChain.senderRatchetKeyPrivate)
|
|
validate(senderRatchetKeyPrivate != nil)
|
|
let senderRatchetKeyPair = ECKeyPair(IdentityKeyPair(publicKey: senderRatchetKey!,
|
|
privateKey: senderRatchetKeyPrivate!))
|
|
setSenderChain(senderRatchetKeyPair,
|
|
chainKey: LegacyChainKey(proto.senderChain.chainKey))
|
|
}
|
|
receivingChains = proto.receiverChains.map { LegacyReceivingChain($0) }
|
|
if proto.hasPendingPreKey {
|
|
validate(proto.pendingPreKey.unknownFields.data.isEmpty)
|
|
|
|
// AxolotlKit uses -1 to represent "no pre-key ID".
|
|
let preKeyID = proto.pendingPreKey.hasPreKeyID ? Int32(proto.pendingPreKey.preKeyID) : -1
|
|
setUnacknowledgedPreKeyMessage(preKeyID,
|
|
signedPreKey: Int32(proto.pendingPreKey.signedPreKeyID),
|
|
baseKey: removeKeyType(from: proto.pendingPreKey.baseKey))
|
|
}
|
|
remoteRegistrationId = Int32(bitPattern: proto.remoteRegistrationID)
|
|
localRegistrationId = Int32(bitPattern: proto.localRegistrationID)
|
|
if proto.hasAliceBaseKey {
|
|
aliceBaseKey = removeKeyType(from: proto.aliceBaseKey)
|
|
}
|
|
}
|
|
}
|
|
|
|
extension LegacySessionRecord {
|
|
fileprivate func buildProto() -> SessionRecordProtos_RecordStructure {
|
|
var result = SessionRecordProtos_RecordStructure()
|
|
|
|
if let currentSessionProto = sessionState().buildProto() {
|
|
result.currentSession = currentSessionProto
|
|
} else {
|
|
let noPreviousSessions = previousSessionStates()?.isEmpty ?? true
|
|
owsAssertDebug(!noPreviousSessions, "should not try to archive uninitialized sessions")
|
|
}
|
|
|
|
for previousSession in previousSessionStates() {
|
|
if let sessionProto = previousSession.buildProto() {
|
|
result.previousSessions.append(sessionProto)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
public func serializeProto() throws -> Data {
|
|
return try buildProto().serializedData()
|
|
}
|
|
|
|
public convenience init(serializedProto: Data) throws {
|
|
let deserialized = try SessionRecordProtos_RecordStructure(serializedData: serializedProto)
|
|
validate(deserialized.unknownFields.data.isEmpty)
|
|
|
|
self.init()
|
|
for stateProto in deserialized.previousSessions.reversed() {
|
|
self.setState(LegacySessionState(stateProto))
|
|
self.archiveCurrentState()
|
|
}
|
|
if deserialized.hasCurrentSession {
|
|
self.setState(LegacySessionState(deserialized.currentSession))
|
|
}
|
|
}
|
|
}
|