Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Track and reset KX with long offline users #116

Merged
merged 14 commits into from Feb 14, 2023
7 changes: 7 additions & 0 deletions .github/workflows/go.yml
Expand Up @@ -26,3 +26,10 @@ jobs:
- name: Test
run: |
./goclean.sh
- name: Upload logs if tests failed
if: failure()
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
with:
name: ${{matrix.go}}-test-logs
path: /tmp/br*

37 changes: 37 additions & 0 deletions brclient/appstate.go
Expand Up @@ -1379,6 +1379,37 @@ func (as *appState) requestRatchetReset(cw *chatWindow) {
as.repaintIfActive(cw)
}

func (as *appState) resetAllOldRatchets(interval time.Duration) error {
intervalStr := interval.String()
if interval > time.Hour*24*3 {
intervalStr = fmt.Sprintf("%d days",
interval/(time.Hour*24))
}
progrChan := make(chan clientintf.UserID)
go func() {
for uid := range progrChan {
nick, _ := as.c.UserNick(uid)
as.diagMsg("Requested ratchet reset with %s (%s)",
strescape.Nick(nick), uid)
}
}()

as.cwHelpMsg("Starting to reset ratchets older than %s", intervalStr)
go func() {
res, err := as.c.ResetAllOldRatchets(interval, progrChan)
as.manyDiagMsgsCb(func(pf printf) {
if len(res) == 0 && err == nil {
pf("No old ratchets in need of starting reset")
} else if err != nil {
pf("Unable to complete old ratchet reset: %v", err)
}
})
time.Sleep(time.Second)
close(progrChan)
}()
return nil
}

func (as *appState) requestMediateID(cw *chatWindow, target clientintf.UserID) {
m := cw.newInternalMsg(fmt.Sprintf("Requesting mediate identity with %s", target))
as.repaintIfActive(cw)
Expand Down Expand Up @@ -2283,6 +2314,12 @@ func newAppState(sendMsg func(tea.Msg), lndLogLines *sloglinesbuffer.Buffer,
}
}))

ntfns.Register(client.OnLocalClientOfflineTooLong(func(oldConnDate time.Time) {
as.diagMsg("The local client has been offline since %s which is before "+
"the limit date imposed by the server message retention policy. "+
"Resetting all KXs", oldConnDate.Format(ISO8601DateTime))
}))

// Initialize client config.
cfg := client.Config{
DB: db,
Expand Down
86 changes: 85 additions & 1 deletion brclient/commands.go
Expand Up @@ -468,6 +468,26 @@ var listCommands = []tuicmd{
}
})

return nil
},
}, {
cmd: "userslastmsgtime",
descr: "List the timestamp of the last message received for every user",
usableOffline: true,
handler: func(args []string, as *appState) error {
users, err := as.c.ListUsersLastReceivedTime()
if err != nil {
return nil
}
as.cwHelpMsgs(func(pf printf) {
pf("")
pf("Last received message time from users (most recent first)")
for _, user := range users {
nick, _ := as.c.UserNick(user.UID)
pf("%s - %s - %s", user.LastDecrypted.Format(ISO8601DateTime),
strescape.Nick(nick), user.UID)
}
})
return nil
},
},
Expand Down Expand Up @@ -2114,10 +2134,41 @@ var commands = []tuicmd{
},
}, {
cmd: "addressbook",
usage: "[<user>]",
usableOffline: true,
aliases: []string{"ab"},
descr: "Show the address book of known peers",
descr: "Show the address book of known remote users (or a user profile)",
handler: func(args []string, as *appState) error {
if len(args) > 0 {
ru, err := as.c.UserByNick(args[0])
if err != nil {
return err
}

as.cwHelpMsgs(func(pf printf) {
pii := ru.PublicIdentity()
r := ru.RatchetDebugInfo()
pf("")
pf("Info for user %s", strescape.Nick(ru.Nick()))
pf(" UID: %s", ru.ID())
pf(" Name: %s", strescape.Content(pii.Name))
pf(" Ignored: %v", ru.IsIgnored())
pf("Last Encrypt Time: %s", r.LastEncTime.Format(ISO8601DateTimeMs))
pf("Last Decrypt Time: %s", r.LastDecTime.Format(ISO8601DateTimeMs))
pf(" Send RV: %s (%s...)",
r.SendRVPlain, r.SendRV.ShortLogID())
pf(" Recv RV: %s (%s...)",
r.RecvRVPlain, r.RecvRV.ShortLogID())
pf(" Drain RV: %s (%s...)",
r.DrainRVPlain, r.DrainRV.ShortLogID())
pf(" My Reset RV: %s", r.MyResetRV)
pf(" Their Reset RV: %s", r.TheirResetRV)
pf(" Saved Keys: %d", r.NbSavedKeys)
pf(" Will Ratchet: %v", r.WillRatchet)
})
return nil
}

ab := as.c.AddressBook()
var maxNickLen int
for i := range ab {
Expand Down Expand Up @@ -2325,6 +2376,39 @@ var commands = []tuicmd{
go as.requestRatchetReset(cw)
return nil
},
}, {
cmd: "rresetold",
usage: "[age]",
descr: "Request a ratchet reset with contacts with no messages since [age] ago",
long: []string{"Request a ratchet reset with every remote user:",
"1. From which no message has been received since [age] ago.",
"2. No other KX reset procedure has been done since [age] ago.",
"This command is useful to restablish comms after the local client has been offline for a long time (greater than the time the data lives on the server), on which case it's likely that many ratchets have been broken",
"If [age] is not specified, it defaults to the ExpiryDays setting of the server.",
"[age] may be specified either in days (without any suffix) or as a Go time.Duration string (with a time suffix)"},

handler: func(args []string, as *appState) error {
var interval time.Duration
if len(args) > 0 {
var err error
interval, err = time.ParseDuration(args[0])
if err != nil {
d, err := strconv.ParseInt(args[0], 10, 64)
if err != nil {
return fmt.Errorf("arg %q is not a valid age",
args[0])
}
interval = time.Duration(d) * 24 * time.Hour
}
} else {
as.connectedMtx.Lock()
expiry := as.expirationDays
as.connectedMtx.Unlock()
interval = time.Duration(expiry) * 24 * time.Hour
}

return as.resetAllOldRatchets(interval)
},
}, {
cmd: "mediateid",
aliases: []string{"mi"},
Expand Down
5 changes: 3 additions & 2 deletions brclient/util.go
Expand Up @@ -20,8 +20,9 @@ import (
)

const (
ISO8601DateTime = "2006-01-02 15:04:00"
ISO8601Date = "2006-01-02"
ISO8601DateTimeMs = "2006-01-02 15:04:05.000"
ISO8601DateTime = "2006-01-02 15:04:05"
ISO8601Date = "2006-01-02"
)

// Helper mixin to avoid having to add an Init() function everywhere.
Expand Down
21 changes: 21 additions & 0 deletions bruig/flutterui/bruig/lib/components/chats_list.dart
@@ -1,5 +1,6 @@
import 'package:bruig/models/client.dart';
import 'package:bruig/models/menus.dart';
import 'package:bruig/screens/contacts_msg_times.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:bruig/screens/feed/feed_posts.dart';
Expand Down Expand Up @@ -142,6 +143,11 @@ Future<void> loadInvite(BuildContext context) async {
.pushNamed('/verifyInvite', arguments: invite);
}

void gotoContactsLastMsgTimeScreen(BuildContext context) {
Navigator.of(context, rootNavigator: true)
.pushNamed(ContactsLastMsgTimesScreen.routeName);
}

class _ChatsList extends StatefulWidget {
final ClientModel chats;
final FocusNode editLineFocusNode;
Expand Down Expand Up @@ -283,6 +289,21 @@ class _ChatsListState extends State<_ChatsList> {
tooltip: "Load Invite",
onPressed: () => loadInvite(context),
icon: Icon(size: 15, color: darkTextColor, Icons.add)))),
Positioned(
bottom: 5,
left: 30,
child: Material(
color: selectedBackgroundColor.withOpacity(0),
child: IconButton(
hoverColor: selectedBackgroundColor,
splashRadius: 15,
iconSize: 15,
tooltip: "List last received message time",
onPressed: () => gotoContactsLastMsgTimeScreen(context),
icon: Icon(
size: 15,
color: darkTextColor,
Icons.list_rounded)))),
Positioned(
bottom: 5,
left: 5,
Expand Down