diff --git a/irc/chanserv.go b/irc/chanserv.go index 6fb748e5..9817ccc2 100644 --- a/irc/chanserv.go +++ b/irc/chanserv.go @@ -4,17 +4,15 @@ package irc import ( - "bytes" "fmt" - "hash/crc32" "sort" - "strconv" "strings" "time" "github.com/goshuirc/irc-go/ircfmt" "github.com/oragono/oragono/irc/modes" "github.com/oragono/oragono/irc/sno" + "github.com/oragono/oragono/irc/utils" ) const chanservHelp = `ChanServ lets you register and manage channels.` @@ -352,7 +350,7 @@ func csUnregisterHandler(server *Server, client *Client, command string, params } info := channel.ExportRegistration(0) - expectedCode := unregisterConfirmationCode(info.Name, info.RegisteredAt) + expectedCode := utils.ConfirmationCode(info.Name, info.RegisteredAt) if expectedCode != verificationCode { csNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this channel will remove all stored channel attributes.$b"))) csNotice(rb, fmt.Sprintf(client.t("To confirm channel unregistration, type: /CS UNREGISTER %[1]s %[2]s"), channelKey, expectedCode)) @@ -363,14 +361,6 @@ func csUnregisterHandler(server *Server, client *Client, command string, params csNotice(rb, fmt.Sprintf(client.t("Channel %s is now unregistered"), channelKey)) } -// deterministically generates a confirmation code for unregistering a channel / account -func unregisterConfirmationCode(name string, registeredAt time.Time) (code string) { - var codeInput bytes.Buffer - codeInput.WriteString(name) - codeInput.WriteString(strconv.FormatInt(registeredAt.Unix(), 16)) - return strconv.Itoa(int(crc32.ChecksumIEEE(codeInput.Bytes()))) -} - func csClearHandler(server *Server, client *Client, command string, params []string, rb *ResponseBuffer) { channel := server.channels.Get(params[0]) if channel == nil { @@ -426,7 +416,7 @@ func csTransferHandler(server *Server, client *Client, command string, params [] return } if targetAccount.NameCasefolded != account { - expectedCode := unregisterConfirmationCode(regInfo.Name, regInfo.RegisteredAt) + expectedCode := utils.ConfirmationCode(regInfo.Name, regInfo.RegisteredAt) codeValidated := 2 < len(params) && params[2] == expectedCode if !codeValidated { csNotice(rb, ircfmt.Unescape(client.t("$bWarning: you are about to transfer control of your channel to another user.$b"))) diff --git a/irc/handlers.go b/irc/handlers.go index a14cc7ed..0a0f2329 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -810,7 +810,7 @@ func debugHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Res rb.Notice(client.t("You must have rehash permissions in order to execute DEBUG CRASHSERVER")) return false } - code := unregisterConfirmationCode(server.name, server.ctime) + code := utils.ConfirmationCode(server.name, server.ctime) if len(msg.Params) == 1 || msg.Params[1] != code { rb.Notice(fmt.Sprintf(client.t("To crash the server, issue the following command: /DEBUG CRASHSERVER %s"), code)) return false diff --git a/irc/nickserv.go b/irc/nickserv.go index 16177c75..39db7590 100644 --- a/irc/nickserv.go +++ b/irc/nickserv.go @@ -732,7 +732,7 @@ func nsUnregisterHandler(server *Server, client *Client, command string, params return } - expectedCode := unregisterConfirmationCode(account.Name, account.RegisteredAt) + expectedCode := utils.ConfirmationCode(account.Name, account.RegisteredAt) if expectedCode != verificationCode { nsNotice(rb, ircfmt.Unescape(client.t("$bWarning: unregistering this account will remove its stored privileges.$b"))) nsNotice(rb, fmt.Sprintf(client.t("To confirm account unregistration, type: /NS UNREGISTER %[1]s %[2]s"), cfname, expectedCode)) diff --git a/irc/utils/confirmation.go b/irc/utils/confirmation.go new file mode 100644 index 00000000..5d984a16 --- /dev/null +++ b/irc/utils/confirmation.go @@ -0,0 +1,22 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + +package utils + +import ( + "crypto/sha256" + "encoding/binary" + "time" +) + +// Deterministically generates a confirmation code for some destructive activity; +// `name` is typically the name of the identity being destroyed (a channel being +// unregistered, or the server being crashed) and `createdAt` means a different +// value is required each time. +func ConfirmationCode(name string, createdAt time.Time) (code string) { + buf := make([]byte, len(name)+8) + binary.BigEndian.PutUint64(buf, uint64(createdAt.UnixNano())) + copy(buf[8:], name[:]) + out := sha256.Sum256(buf) + return B32Encoder.EncodeToString(out[:3]) +} diff --git a/irc/utils/confirmation_test.go b/irc/utils/confirmation_test.go new file mode 100644 index 00000000..01954dac --- /dev/null +++ b/irc/utils/confirmation_test.go @@ -0,0 +1,36 @@ +// Copyright (c) 2020 Shivaram Lingamneni +// released under the MIT license + +package utils + +import ( + "testing" + "time" +) + +func easyParse(timestamp string) time.Time { + result, err := time.Parse("2006-01-02 15:04:05Z", timestamp) + if err != nil { + panic(err) + } + return result +} + +func TestConfirmation(t *testing.T) { + set := make(map[string]struct{}) + + set[ConfirmationCode("#darwin", easyParse("2006-01-01 00:00:00Z"))] = struct{}{} + set[ConfirmationCode("#darwin", easyParse("2006-01-02 00:00:00Z"))] = struct{}{} + set[ConfirmationCode("#xelpers", easyParse("2006-01-01 00:00:00Z"))] = struct{}{} + set[ConfirmationCode("#xelpers", easyParse("2006-01-02 00:00:00Z"))] = struct{}{} + + if len(set) != 4 { + t.Error("confirmation codes are not unique") + } + + for code := range set { + if len(code) <= 2 || len(code) >= 8 { + t.Errorf("bad code: %s", code) + } + } +}