Skip to content

Commit 8a454df

Browse files
authored
feat: changes needed to implement cypher session renewal on client side (#8)
* feat: changes needed to implement cypher session renewal on client side * test: add test to cover a case with loosing session's changes
1 parent 934da7e commit 8a454df

4 files changed

Lines changed: 150 additions & 7 deletions

File tree

Sources/DXProtocol/DXError.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ public enum DXError: Error, Equatable {
7777
case invalidSenderKeySession(distributionId: UUID, message: String)
7878
/// The message has already been sent.
7979
case duplicatedMessage(String)
80-
/// The signature verification failed.
81-
case verificationFailed(String)
80+
/// The MAC verification failed.
81+
case messageVerificationFailed(String)
8282
/// An error occurred in a callback.
8383
case callbackError(String)
8484
/// An unknown error occurred.

Sources/DXProtocol/Session/Session.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -334,7 +334,6 @@ extension Session {
334334
identityStore: identityStore,
335335
preKeyStore: preKeyStore,
336336
signedPreKeyStore: signedPreKeyStore)
337-
// This is 'decrypt_message_with_record'
338337
let result = try session.decryptMessage(preKeyMessage.secureMessage)
339338

340339
try sessionStore.storeSession(session, for: address)
@@ -380,7 +379,6 @@ extension Session {
380379
throw DXError.untrustedIdentity("Abort decrypting PreKeyMessage for untrusted identity")
381380
}
382381

383-
// This is 'decrypt_message_with_record'
384382
let result = try session.decryptMessage(secureMessage)
385383

386384
try sessionStore.storeSession(session, for: address)
@@ -399,13 +397,16 @@ extension Session {
399397
/// - Returns: The decrypted data.
400398
private mutating func decryptMessage(_ message: SecureMessage) throws -> Data {
401399
var result = Data()
400+
var errors = [Error]()
402401

403402
do {
404403
// We MUST discard changes of 'state' if decryption of message fails
405404
var state = self.state
406405
result = try state.decryptMessage(message)
407406
self.state = state
408407
} catch let error {
408+
errors.append(error)
409+
409410
if case DXError.duplicatedMessage = error {
410411
// This message has been already processed.
411412
// Do not try previous states to decrypt it
@@ -423,6 +424,8 @@ extension Session {
423424
self.promotePreviousState(state: olderState, index: index)
424425
break
425426
} catch let error {
427+
errors.append(error)
428+
426429
// This code is not covered by tests
427430
if case DXError.duplicatedMessage = error {
428431
// This message has been already processed.
@@ -433,6 +436,12 @@ extension Session {
433436
}
434437
}
435438

439+
if result.isEmpty && !errors.isEmpty {
440+
let protocolErrors = errors.compactMap { $0 as? DXError }
441+
let error = protocolErrors.first ?? errors[0]
442+
throw error
443+
}
444+
436445
if result.isEmpty {
437446
throw DXError.invalidMessage("Failed to decrypt message")
438447
}
@@ -459,13 +468,13 @@ extension Session {
459468

460469
/// Checks if the session contains active current state.
461470
/// - Returns: True if session contains active current state. False otherwise
462-
func hasCurrentState() -> Bool {
471+
public func hasCurrentState() -> Bool {
463472
return !self.state.archived
464473
}
465474

466475
// TODO: - Add tests on error
467476
/// Archives the current active state. For example, this functionality might be used to manage stale devices
468-
mutating func archiveCurrentState() {
477+
public mutating func archiveCurrentState() {
469478
guard !self.state.archived else {
470479
return
471480
}

Sources/DXProtocol/Session/SessionState.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ struct SessionState: Codable {
235235
receiverIdentityKey: localIdentityKey,
236236
macKey: messageKeys.macKey)
237237
guard isMacValid else {
238-
throw DXError.invalidMessage("MAC verification failed")
238+
throw DXError.messageVerificationFailed("MAC verification failed")
239239
}
240240

241241
// Source - https://developer.apple.com/forums/thread/687212

Tests/DXProtocolTests/SessionTests/SessionTests.swift

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1455,6 +1455,140 @@ final class SessionTests: XCTestCase {
14551455
XCTFail("Invalid error type: \(error)")
14561456
}
14571457
}
1458+
1459+
func testInteractionWhenLostSessionChanges() throws {
1460+
let senderClient = try TestClient(userId: UUID()) // Alice
1461+
let recipientClient = try TestClient(userId: UUID()) // Bob
1462+
try initializeSession(senderClient: senderClient, recipientClient: recipientClient)
1463+
1464+
let aliceClient = senderClient
1465+
let bobClient = recipientClient
1466+
1467+
// Archive session state so that we have a non-empty list of previous states
1468+
var session = try bobClient.sessionStore.loadSession(for: aliceClient.protocolAddress)
1469+
session?.archiveCurrentState()
1470+
try bobClient.sessionStore.storeSession(
1471+
try XCTUnwrap(session),
1472+
for: aliceClient.protocolAddress)
1473+
1474+
// The code below will do the next ratchet step
1475+
// {
1476+
// Alice sends one more message
1477+
let data = try XCTUnwrap("From Alice".data(using: .utf8))
1478+
let message = try Session.encrypt(
1479+
data: data,
1480+
for: bobClient.protocolAddress,
1481+
sessionStore: aliceClient.sessionStore,
1482+
identityStore: aliceClient.identityKeyStore
1483+
)
1484+
let result = try Session.decrypt(
1485+
message: message,
1486+
from: aliceClient.protocolAddress,
1487+
sessionStore: bobClient.sessionStore,
1488+
identityStore: bobClient.identityKeyStore,
1489+
preKeyStore: bobClient.preKeyStore,
1490+
signedPreKeyStore: bobClient.signedPreKeyStore
1491+
)
1492+
XCTAssertEqual(result, data)
1493+
1494+
// Bob sends reply to Alice
1495+
let replyData = try XCTUnwrap("From Bob".data(using: .utf8))
1496+
let replyMessage = try Session.encrypt(
1497+
data: replyData,
1498+
for: aliceClient.protocolAddress,
1499+
sessionStore: bobClient.sessionStore,
1500+
identityStore: bobClient.identityKeyStore)
1501+
1502+
let decryptReplyMessage = try Session.decrypt(
1503+
message: replyMessage,
1504+
from: bobClient.protocolAddress,
1505+
sessionStore: aliceClient.sessionStore,
1506+
identityStore: aliceClient.identityKeyStore,
1507+
preKeyStore: aliceClient.preKeyStore,
1508+
signedPreKeyStore: aliceClient.signedPreKeyStore)
1509+
XCTAssertEqual(decryptReplyMessage, replyData)
1510+
// }
1511+
1512+
// We will use this session to imitate loosing of session's changes
1513+
let outdatedSession = try bobClient.sessionStore.loadSession(for: aliceClient.protocolAddress)
1514+
1515+
// The code below will do further ratchet step (session's changes)
1516+
// {
1517+
let secondData = try XCTUnwrap("Alice second message".data(using: .utf8))
1518+
let secondMessage = try Session.encrypt(
1519+
data: secondData,
1520+
for: bobClient.protocolAddress,
1521+
sessionStore: aliceClient.sessionStore,
1522+
identityStore: aliceClient.identityKeyStore)
1523+
let decryptSecondResult = try Session.decrypt(
1524+
message: secondMessage,
1525+
from: aliceClient.protocolAddress,
1526+
sessionStore: bobClient.sessionStore,
1527+
identityStore: bobClient.identityKeyStore,
1528+
preKeyStore: bobClient.preKeyStore,
1529+
signedPreKeyStore: bobClient.signedPreKeyStore)
1530+
XCTAssertEqual(decryptSecondResult, secondData)
1531+
1532+
let replySecondData = try XCTUnwrap("Bob reply on second message".data(using: .utf8))
1533+
let replySecondMessage = try Session.encrypt(
1534+
data: replySecondData,
1535+
for: aliceClient.protocolAddress,
1536+
sessionStore: bobClient.sessionStore,
1537+
identityStore: bobClient.identityKeyStore)
1538+
let decryptReplySecondResult = try Session.decrypt(
1539+
message: replySecondMessage,
1540+
from: bobClient.protocolAddress,
1541+
sessionStore: aliceClient.sessionStore,
1542+
identityStore: aliceClient.identityKeyStore,
1543+
preKeyStore: aliceClient.preKeyStore,
1544+
signedPreKeyStore: aliceClient.signedPreKeyStore)
1545+
XCTAssertEqual(decryptReplySecondResult, replySecondData)
1546+
1547+
// Actually test
1548+
1549+
// Imitate like we lost last changes made to session
1550+
try bobClient.sessionStore.storeSession(
1551+
try XCTUnwrap(outdatedSession),
1552+
for: aliceClient.protocolAddress)
1553+
1554+
// Test Bob can send message to Alice after lost session changes
1555+
let bobDataAfterLost = try XCTUnwrap("Message From Bob after lost".data(using: .utf8))
1556+
let bobMessageAfterLost = try Session.encrypt(
1557+
data: bobDataAfterLost,
1558+
for: aliceClient.protocolAddress,
1559+
sessionStore: bobClient.sessionStore,
1560+
identityStore: bobClient.identityKeyStore)
1561+
let aliceDecryptAfterLost = try Session.decrypt(
1562+
message: bobMessageAfterLost,
1563+
from: bobClient.protocolAddress,
1564+
sessionStore: aliceClient.sessionStore,
1565+
identityStore: aliceClient.identityKeyStore,
1566+
preKeyStore: aliceClient.preKeyStore,
1567+
signedPreKeyStore: aliceClient.signedPreKeyStore)
1568+
XCTAssertEqual(aliceDecryptAfterLost, bobDataAfterLost)
1569+
1570+
// Test Bob can't receive message from Alice
1571+
let aliceThirdMessageData = try XCTUnwrap("Alice third message".data(using: .utf8))
1572+
let aliceThirdMessage = try Session.encrypt(
1573+
data: aliceThirdMessageData,
1574+
for: bobClient.protocolAddress,
1575+
sessionStore: aliceClient.sessionStore,
1576+
identityStore: aliceClient.identityKeyStore)
1577+
do {
1578+
let decryptThirdMessageResult = try Session.decrypt(
1579+
message: aliceThirdMessage,
1580+
from: aliceClient.protocolAddress,
1581+
sessionStore: bobClient.sessionStore,
1582+
identityStore: bobClient.identityKeyStore,
1583+
preKeyStore: bobClient.preKeyStore,
1584+
signedPreKeyStore: bobClient.signedPreKeyStore)
1585+
XCTFail("Third message should not be decrypted due loosing session changes")
1586+
XCTAssertEqual(decryptThirdMessageResult, aliceThirdMessageData)
1587+
} catch DXError.messageVerificationFailed {
1588+
} catch {
1589+
XCTFail("Incorrect error thrown")
1590+
}
1591+
}
14581592
}
14591593

14601594
extension SessionTests {

0 commit comments

Comments
 (0)