diff --git a/irc/channel.go b/irc/channel.go index 5e79163b..90306380 100644 --- a/irc/channel.go +++ b/irc/channel.go @@ -777,12 +777,6 @@ func stripMaskFromNick(nickMask string) (nick string) { return nickMask[0:index] } -// munge the msgid corresponding to a replayable event, -// yielding a consistent msgid for the fake PRIVMSG from HistServ -func mungeMsgidForHistserv(token string) (result string) { - return fmt.Sprintf("_%s", token) -} - func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.Item, autoreplay bool) { chname := channel.Name() client := rb.target @@ -823,7 +817,7 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I } else { message = fmt.Sprintf(client.t("%[1]s [account: %[2]s] joined the channel"), nick, item.AccountName) } - rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) } case history.Part: if eventPlayback { @@ -833,14 +827,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I continue // #474 } message := fmt.Sprintf(client.t("%[1]s left the channel (%[2]s)"), nick, item.Message.Message) - rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) } case history.Kick: if eventPlayback { rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "KICK", chname, item.Params[0], item.Message.Message) } else { message := fmt.Sprintf(client.t("%[1]s kicked %[2]s (%[3]s)"), nick, item.Params[0], item.Message.Message) - rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) } case history.Quit: if eventPlayback { @@ -850,14 +844,14 @@ func (channel *Channel) replayHistoryItems(rb *ResponseBuffer, items []history.I continue // #474 } message := fmt.Sprintf(client.t("%[1]s quit (%[2]s)"), nick, item.Message.Message) - rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) } case history.Nick: if eventPlayback { rb.AddFromClient(item.Message.Time, item.Message.Msgid, item.Nick, item.AccountName, nil, "HEVENT", "NICK", item.Params[0]) } else { message := fmt.Sprintf(client.t("%[1]s changed nick to %[2]s"), nick, item.Params[0]) - rb.AddFromClient(item.Message.Time, mungeMsgidForHistserv(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) + rb.AddFromClient(item.Message.Time, utils.MungeSecretToken(item.Message.Msgid), "HistServ", "*", nil, "PRIVMSG", chname, message) } } } diff --git a/irc/utils/crypto.go b/irc/utils/crypto.go index 010b3401..a725b547 100644 --- a/irc/utils/crypto.go +++ b/irc/utils/crypto.go @@ -28,6 +28,29 @@ func GenerateSecretToken() string { return B32Encoder.EncodeToString(buf[:]) } +// "munge" a secret token to a new value. requirements: +// 1. MUST be roughly as unlikely to collide with `GenerateSecretToken` outputs +// as those outputs are with each other +// 2. SHOULD be deterministic (motivation: if a JOIN line has msgid x, +// create a deterministic msgid y for the fake HistServ PRIVMSG that "replays" it) +// 3. SHOULD be in the same "namespace" as `GenerateSecretToken` outputs +// (same length and character set) +func MungeSecretToken(token string) (result string) { + bytes, err := B32Encoder.DecodeString(token) + if err != nil { + // this should never happen + return GenerateSecretToken() + } + // add 1 with carrying + for i := len(bytes) - 1; 0 <= i; i -= 1 { + bytes[i] += 1 + if bytes[i] != 0 { + break + } // else: overflow, carry to the next place + } + return B32Encoder.EncodeToString(bytes) +} + // securely check if a supplied token matches a stored token func SecretTokensMatch(storedToken string, suppliedToken string) bool { // XXX fix a potential gotcha: if the stored token is uninitialized, diff --git a/irc/utils/crypto_test.go b/irc/utils/crypto_test.go index a5e60254..c00ebad3 100644 --- a/irc/utils/crypto_test.go +++ b/irc/utils/crypto_test.go @@ -47,8 +47,37 @@ func TestTokenCompare(t *testing.T) { } } +func TestMunging(t *testing.T) { + count := 131072 + set := make(map[string]bool) + var token string + for i := 0; i < count; i++ { + token = GenerateSecretToken() + set[token] = true + } + // all tokens generated thus far should be unique + assertEqual(len(set), count, t) + + // iteratively munge the last generated token an additional `count` times + mungedToken := token + for i := 0; i < count; i++ { + mungedToken = MungeSecretToken(mungedToken) + assertEqual(len(mungedToken), len(token), t) + set[mungedToken] = true + } + // munged tokens should not collide with generated tokens, or each other + assertEqual(len(set), count*2, t) +} + func BenchmarkGenerateSecretToken(b *testing.B) { for i := 0; i < b.N; i++ { GenerateSecretToken() } } + +func BenchmarkMungeSecretToken(b *testing.B) { + t := GenerateSecretToken() + for i := 0; i < b.N; i++ { + t = MungeSecretToken(t) + } +}