iris-chat 0.1.35

Iris Chat command line client and shared encrypted chat core
Documentation
#[test]
fn device_sync_bootstraps_missing_chats_groups_and_post_roster_messages_once() {
    let owner = Keys::generate();
    let local_device = Keys::generate();
    let sibling_device = Keys::generate();
    let peer = Keys::generate();
    let peer_device = Keys::generate();
    let group_member = Keys::generate();
    let group_member_device = Keys::generate();
    let (mut core, _updates, _temp_dir) =
        logged_in_test_core_with_updates("device-sync", &owner, &local_device);
    let owner_hex = owner.public_key().to_hex();
    let sibling_hex = sibling_device.public_key().to_hex();
    let peer_hex = peer.public_key().to_hex();
    let group_member_hex = group_member.public_key().to_hex();
    core.app_keys.insert(
        owner_hex.clone(),
        KnownAppKeys {
            owner_pubkey_hex: owner_hex.clone(),
            created_at_secs: 100,
            devices: vec![
                KnownAppKeyDevice {
                    identity_pubkey_hex: local_device.public_key().to_hex(),
                    created_at_secs: 1,
                    device_label: None,
                    client_label: None,
                    label_updated_at_secs: 0,
                },
                KnownAppKeyDevice {
                    identity_pubkey_hex: sibling_hex.clone(),
                    created_at_secs: 100,
                    device_label: None,
                    client_label: None,
                    label_updated_at_secs: 0,
                },
            ],
        },
    );

    let data = serde_json::to_vec(&serde_json::json!({
        "type": "snapshot",
        "v": 1,
        "rosterAt": 102,
        "chats": [{ "id": peer_hex, "updatedAt": 90 }],
        "appKeys": [
            {
                "ownerPubkey": peer_hex,
                "createdAt": 80,
                "devices": [{
                    "identityPubkey": peer_device.public_key().to_hex(),
                    "createdAt": 70
                }]
            },
            {
                "ownerPubkey": group_member_hex,
                "createdAt": 81,
                "devices": [{
                    "identityPubkey": group_member_device.public_key().to_hex(),
                    "createdAt": 71
                }]
            }
        ],
        "groups": [{
            "id": "friends",
            "name": "Friends",
            "description": "Good people",
            "createdBy": owner_hex,
            "members": [owner_hex, peer_hex, group_member_hex],
            "admins": [owner_hex],
            "protocol": "pairwise_fanout_v1",
            "revision": 4,
            "createdAt": 50,
            "updatedAt": 90,
            "accepted": true
        }],
        "messages": [
            {
                "chatId": peer_hex,
                "id": "at-cutoff",
                "body": "old",
                "author": owner_hex,
                "createdAt": 100
            },
            {
                "chatId": peer_hex,
                "id": "after-cutoff",
                "body": "still old",
                "author": owner_hex,
                "createdAt": 101
            },
            {
                "chatId": peer_hex,
                "id": "after-both-cutoffs",
                "body": "new",
                "author": owner_hex,
                "createdAt": 103
            }
        ]
    }))
    .unwrap();

    core.handle_device_sync_packet(&Keys::generate().public_key().to_hex(), 7369, &data);
    assert!(
        core.threads.is_empty(),
        "unregistered devices cannot inject state"
    );
    assert!(!core.app_keys.contains_key(&peer_hex));
    assert!(!core.app_keys.contains_key(&group_member_hex));

    core.handle_device_sync_packet(&sibling_hex, 7369, &data);
    core.handle_device_sync_packet(&sibling_hex, 7369, &data);

    let newer_group = serde_json::to_vec(&serde_json::json!({
        "type": "snapshot",
        "v": 1,
        "rosterAt": 100,
        "chats": [],
        "groups": [{
            "id": "friends",
            "name": "Best friends",
            "createdBy": owner_hex,
            "members": [owner_hex, peer_hex, group_member_hex],
            "admins": [owner_hex],
            "revision": 5,
            "createdAt": 50,
            "updatedAt": 102
        }],
        "messages": []
    }))
    .unwrap();
    core.handle_device_sync_packet(&sibling_hex, 7369, &newer_group);

    assert!(core.threads.contains_key(&peer_hex));
    assert!(core.threads.contains_key("group:friends"));
    assert_eq!(core.groups.get("friends").unwrap().name, "Best friends");
    let messages = &core.threads.get(&peer_hex).unwrap().messages;
    assert_eq!(messages.len(), 1);
    assert_eq!(messages[0].id, "after-both-cutoffs");
    assert!(core.preferences.accepted_owner_pubkeys.contains(&peer_hex));
    assert_eq!(core.app_keys[&peer_hex].created_at_secs, 80);
    assert_eq!(core.app_keys[&group_member_hex].created_at_secs, 81);
    assert!(core
        .protocol_engine
        .as_ref()
        .unwrap()
        .has_device_roster_entry_for_owner(peer.public_key(), peer_device.public_key()));

    let unrelated = Keys::generate();
    let unrelated_hex = unrelated.public_key().to_hex();
    core.app_keys.insert(
        unrelated_hex.clone(),
        KnownAppKeys {
            owner_pubkey_hex: unrelated_hex.clone(),
            created_at_secs: 82,
            devices: vec![KnownAppKeyDevice {
                identity_pubkey_hex: Keys::generate().public_key().to_hex(),
                created_at_secs: 72,
                device_label: None,
                client_label: None,
                label_updated_at_secs: 0,
            }],
        },
    );
    let packets = core.build_device_sync_packets_for_test(100, false);
    let mut synced_owners = packets
        .iter()
        .flat_map(|packet| {
            serde_json::from_slice::<serde_json::Value>(packet).unwrap()["appKeys"]
                .as_array()
                .unwrap()
                .iter()
                .map(|roster| roster["ownerPubkey"].as_str().unwrap().to_string())
                .collect::<Vec<_>>()
        })
        .collect::<Vec<_>>();
    synced_owners.sort();
    let mut expected_owners = vec![
        owner_hex.clone(),
        peer_hex.clone(),
        group_member_hex.clone(),
    ];
    expected_owners.sort();
    assert_eq!(synced_owners, expected_owners);
    assert!(packets.iter().all(|packet| {
        serde_json::from_slice::<serde_json::Value>(packet).unwrap()["messages"]
            .as_array()
            .unwrap()
            .is_empty()
    }));

    let (mut linked, _updates, _temp) =
        logged_in_test_core_with_updates("device-sync-linked", &owner, &sibling_device);
    linked
        .app_keys
        .insert(owner_hex.clone(), core.app_keys[&owner_hex].clone());
    for packet in core.build_device_sync_packets_for_test(100, true) {
        linked.handle_device_sync_packet(&local_device.public_key().to_hex(), 7369, &packet);
    }
    assert_eq!(linked.threads[&peer_hex].messages[0].id, "after-both-cutoffs");
    assert_eq!(linked.groups["friends"].name, "Best friends");
    assert_eq!(linked.groups["friends"].members.len(), 3);
    assert!(linked.threads.contains_key("group:friends"));
    assert_eq!(linked.app_keys[&peer_hex].created_at_secs, 80);
    assert_eq!(linked.app_keys[&group_member_hex].created_at_secs, 81);
    assert!(!linked.app_keys.contains_key(&unrelated_hex));
}

#[test]
fn device_sync_app_keys_use_canonical_freshness_and_preserve_retained_labels() {
    let owner = Keys::generate();
    let local_device = Keys::generate();
    let sibling_device = Keys::generate();
    let peer = Keys::generate();
    let device_a = Keys::generate();
    let device_b = Keys::generate();
    let device_c = Keys::generate();
    let (mut core, _updates, _temp_dir) =
        logged_in_test_core_with_updates("device-sync-app-keys", &owner, &local_device);
    let owner_hex = owner.public_key().to_hex();
    let sibling_hex = sibling_device.public_key().to_hex();
    let peer_hex = peer.public_key().to_hex();
    core.app_keys.insert(
        owner_hex.clone(),
        KnownAppKeys {
            owner_pubkey_hex: owner_hex,
            created_at_secs: 100,
            devices: vec![
                KnownAppKeyDevice {
                    identity_pubkey_hex: local_device.public_key().to_hex(),
                    created_at_secs: 1,
                    device_label: None,
                    client_label: None,
                    label_updated_at_secs: 0,
                },
                KnownAppKeyDevice {
                    identity_pubkey_hex: sibling_hex.clone(),
                    created_at_secs: 2,
                    device_label: None,
                    client_label: None,
                    label_updated_at_secs: 0,
                },
            ],
        },
    );

    let snapshot = |created_at: u64, devices: &[&Keys]| {
        serde_json::to_vec(&serde_json::json!({
            "type": "snapshot",
            "v": 1,
            "rosterAt": 100,
            "chats": [{ "id": peer_hex, "updatedAt": 90 }],
            "appKeys": [{
                "ownerPubkey": peer_hex,
                "createdAt": created_at,
                "devices": devices.iter().map(|device| serde_json::json!({
                    "identityPubkey": device.public_key().to_hex(),
                    "createdAt": 10
                })).collect::<Vec<_>>()
            }],
            "groups": [],
            "messages": []
        }))
        .unwrap()
    };

    core.handle_device_sync_packet(&sibling_hex, 7369, &snapshot(20, &[&device_a]));
    core.app_keys
        .get_mut(&peer_hex)
        .unwrap()
        .devices[0]
        .device_label = Some("Kept locally".to_string());
    core.handle_device_sync_packet(&sibling_hex, 7369, &snapshot(19, &[&device_b]));
    assert_eq!(core.app_keys[&peer_hex].devices.len(), 1);
    assert_eq!(
        core.app_keys[&peer_hex].devices[0].identity_pubkey_hex,
        device_a.public_key().to_hex()
    );

    core.handle_device_sync_packet(&sibling_hex, 7369, &snapshot(20, &[&device_b]));
    assert_eq!(core.app_keys[&peer_hex].devices.len(), 2);

    core.handle_device_sync_packet(
        &sibling_hex,
        7369,
        &snapshot(21, &[&device_a, &device_c]),
    );
    let roster = &core.app_keys[&peer_hex];
    assert_eq!(roster.created_at_secs, 21);
    assert_eq!(roster.devices.len(), 2);
    assert!(roster
        .devices
        .iter()
        .any(|device| device.identity_pubkey_hex == device_c.public_key().to_hex()));
    assert_eq!(
        roster
            .devices
            .iter()
            .find(|device| device.identity_pubkey_hex == device_a.public_key().to_hex())
            .and_then(|device| device.device_label.as_deref()),
        Some("Kept locally")
    );
    assert!(!roster
        .devices
        .iter()
        .any(|device| device.identity_pubkey_hex == device_b.public_key().to_hex()));
}

#[test]
fn device_sync_rejects_malformed_app_keys_rosters() {
    let owner = Keys::generate();
    let local_device = Keys::generate();
    let sibling_device = Keys::generate();
    let peer = Keys::generate();
    let (mut core, _updates, _temp_dir) =
        logged_in_test_core_with_updates("device-sync-invalid-app-keys", &owner, &local_device);
    let owner_hex = owner.public_key().to_hex();
    let sibling_hex = sibling_device.public_key().to_hex();
    let peer_hex = peer.public_key().to_hex();
    core.app_keys.insert(
        owner_hex.clone(),
        KnownAppKeys {
            owner_pubkey_hex: owner_hex,
            created_at_secs: 100,
            devices: vec![
                KnownAppKeyDevice {
                    identity_pubkey_hex: local_device.public_key().to_hex(),
                    created_at_secs: 1,
                    device_label: None,
                    client_label: None,
                    label_updated_at_secs: 0,
                },
                KnownAppKeyDevice {
                    identity_pubkey_hex: sibling_hex.clone(),
                    created_at_secs: 2,
                    device_label: None,
                    client_label: None,
                    label_updated_at_secs: 0,
                },
            ],
        },
    );
    let data = serde_json::to_vec(&serde_json::json!({
        "type": "snapshot",
        "v": 1,
        "rosterAt": 100,
        "chats": [],
        "appKeys": [{
            "ownerPubkey": peer_hex,
            "createdAt": 20,
            "devices": [{ "identityPubkey": "not-a-key", "createdAt": 10 }]
        }],
        "groups": [],
        "messages": []
    }))
    .unwrap();

    core.handle_device_sync_packet(&sibling_hex, 7369, &data);
    assert!(!core.app_keys.contains_key(&peer_hex));

    let duplicate_owner = Keys::generate();
    let duplicate_owner_hex = duplicate_owner.public_key().to_hex();
    let duplicate_device_hex = Keys::generate().public_key().to_hex();
    let duplicates = serde_json::to_vec(&serde_json::json!({
        "type": "snapshot",
        "v": 1,
        "rosterAt": 100,
        "chats": [],
        "appKeys": [{
            "ownerPubkey": duplicate_owner_hex,
            "createdAt": 20,
            "devices": [
                { "identityPubkey": duplicate_device_hex, "createdAt": 10 },
                { "identityPubkey": duplicate_device_hex, "createdAt": 11 }
            ]
        }],
        "groups": [],
        "messages": []
    }))
    .unwrap();
    core.handle_device_sync_packet(&sibling_hex, 7369, &duplicates);
    assert!(!core.app_keys.contains_key(&duplicate_owner_hex));
}