From afe94d43c3f615a812297c7cd04e53c42b9e3e6b Mon Sep 17 00:00:00 2001 From: Shivaram Lingamneni Date: Tue, 12 Feb 2019 00:27:57 -0500 Subject: [PATCH] update resume support to draft/resume-0.3 --- gencapdefs.py | 2 +- irc/caps/defs.go | 4 +- irc/client.go | 36 +++-------------- irc/commands.go | 2 +- irc/getters.go | 10 ++++- irc/handlers.go | 18 ++++----- irc/resume.go | 87 ++++++++++++++++++++++++++++++++++++++++ irc/server.go | 3 ++ irc/utils/crypto.go | 4 ++ irc/utils/crypto_test.go | 2 +- 10 files changed, 121 insertions(+), 47 deletions(-) create mode 100644 irc/resume.go diff --git a/gencapdefs.py b/gencapdefs.py index 207e6aac..a0dcd0f4 100644 --- a/gencapdefs.py +++ b/gencapdefs.py @@ -107,7 +107,7 @@ CAPDEFS = [ ), CapDef( identifier="Resume", - name="draft/resume-0.2", + name="draft/resume-0.3", url="https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md", standard="proposed IRCv3", ), diff --git a/irc/caps/defs.go b/irc/caps/defs.go index 5f5aac14..74ff1de6 100644 --- a/irc/caps/defs.go +++ b/irc/caps/defs.go @@ -73,7 +73,7 @@ const ( // https://github.com/SaberUK/ircv3-specifications/blob/rename/extensions/rename.md Rename Capability = iota - // Resume is the proposed IRCv3 capability named "draft/resume-0.2": + // Resume is the proposed IRCv3 capability named "draft/resume-0.3": // https://github.com/DanielOaks/ircv3-specifications/blob/master+resume/extensions/resume.md Resume Capability = iota @@ -112,7 +112,7 @@ var ( "draft/message-tags-0.2", "multi-prefix", "draft/rename", - "draft/resume-0.2", + "draft/resume-0.3", "sasl", "server-time", "sts", diff --git a/irc/client.go b/irc/client.go index 2f1e88c5..8ebb2a5b 100644 --- a/irc/client.go +++ b/irc/client.go @@ -37,8 +37,6 @@ const ( // when completing the registration, and when rejoining channels. type ResumeDetails struct { OldClient *Client - OldNick string - OldNickMask string PresentedToken string Timestamp time.Time ResumedAt time.Time @@ -86,7 +84,7 @@ type Client struct { realIP net.IP registered bool resumeDetails *ResumeDetails - resumeToken string + resumeID string saslInProgress bool saslMechanism string saslValue string @@ -385,16 +383,15 @@ func (client *Client) tryResume() (success bool) { } }() - oldnick := client.resumeDetails.OldNick timestamp := client.resumeDetails.Timestamp var timestampString string if !timestamp.IsZero() { timestampString = timestamp.UTC().Format(IRCv3TimestampFormat) } - oldClient := server.clients.Get(oldnick) + oldClient := server.resumeManager.VerifyToken(client.resumeDetails.PresentedToken) if oldClient == nil { - client.Send(nil, server.name, "RESUME", "ERR", oldnick, client.t("Cannot resume connection, old client not found")) + client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, token is not valid")) return } oldNick := oldClient.Nick() @@ -402,13 +399,7 @@ func (client *Client) tryResume() (success bool) { resumeAllowed := config.Server.AllowPlaintextResume || (oldClient.HasMode(modes.TLS) && client.HasMode(modes.TLS)) if !resumeAllowed { - client.Send(nil, server.name, "RESUME", "ERR", oldnick, client.t("Cannot resume connection, old and new clients must have TLS")) - return - } - - oldResumeToken := oldClient.ResumeToken() - if oldResumeToken == "" || !utils.SecretTokensMatch(oldResumeToken, client.resumeDetails.PresentedToken) { - client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, invalid resume token")) + client.Send(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, old and new clients must have TLS")) return } @@ -896,6 +887,8 @@ func (client *Client) destroy(beingResumed bool) { client.server.connectionLimiter.RemoveClient(ipaddr) } + client.server.resumeManager.Delete(client) + // alert monitors client.server.monitorManager.AlertAbout(client, false) // clean up monitor state @@ -1120,23 +1113,6 @@ func (client *Client) removeChannel(channel *Channel) { client.stateMutex.Unlock() } -// Ensures the client has a cryptographically secure resume token, and returns -// its value. An error is returned if a token was previously assigned. -func (client *Client) generateResumeToken() (token string, err error) { - newToken := utils.GenerateSecretToken() - - client.stateMutex.Lock() - defer client.stateMutex.Unlock() - - if client.resumeToken == "" { - client.resumeToken = newToken - } else { - err = errResumeTokenAlreadySet - } - - return client.resumeToken, err -} - // Records that the client has been invited to join an invite-only channel func (client *Client) Invite(casefoldedChannel string) { client.stateMutex.Lock() diff --git a/irc/commands.go b/irc/commands.go index 5ff86088..fce1b5df 100644 --- a/irc/commands.go +++ b/irc/commands.go @@ -231,7 +231,7 @@ func init() { "RESUME": { handler: resumeHandler, usablePreReg: true, - minParams: 2, + minParams: 1, }, "SAJOIN": { handler: sajoinHandler, diff --git a/irc/getters.go b/irc/getters.go index 3ae77959..7eb3e546 100644 --- a/irc/getters.go +++ b/irc/getters.go @@ -109,10 +109,16 @@ func (client *Client) uniqueIdentifiers() (nickCasefolded string, skeleton strin return client.nickCasefolded, client.skeleton } -func (client *Client) ResumeToken() string { +func (client *Client) ResumeID() string { client.stateMutex.RLock() defer client.stateMutex.RUnlock() - return client.resumeToken + return client.resumeID +} + +func (client *Client) SetResumeID(id string) { + client.stateMutex.Lock() + defer client.stateMutex.Unlock() + client.resumeID = id } func (client *Client) Oper() *Oper { diff --git a/irc/handlers.go b/irc/handlers.go index bbb77afa..78e47e45 100644 --- a/irc/handlers.go +++ b/irc/handlers.go @@ -508,8 +508,8 @@ func capHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Respo // if this is the first time the client is requesting a resume token, // send it to them if toAdd.Has(caps.Resume) { - token, err := client.generateResumeToken() - if err == nil { + token := server.resumeManager.GenerateToken(client) + if token != "" { rb.Add(nil, server.name, "RESUME", "TOKEN", token) } } @@ -2258,28 +2258,26 @@ func renameHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *Re return false } -// RESUME [timestamp] +// RESUME [timestamp] func resumeHandler(server *Server, client *Client, msg ircmsg.IrcMessage, rb *ResponseBuffer) bool { - oldnick := msg.Params[0] - token := msg.Params[1] + token := msg.Params[0] if client.registered { - rb.Add(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Cannot resume connection, connection registration has already been completed")) + rb.Add(nil, server.name, "RESUME", "ERR", client.t("Cannot resume connection, connection registration has already been completed")) return false } var timestamp time.Time - if 2 < len(msg.Params) { - ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[2]) + if 1 < len(msg.Params) { + ts, err := time.Parse(IRCv3TimestampFormat, msg.Params[1]) if err == nil { timestamp = ts } else { - rb.Add(nil, server.name, ERR_CANNOT_RESUME, oldnick, client.t("Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it")) + rb.Add(nil, server.name, "RESUME", "ERR", client.t("Timestamp is not in 2006-01-02T15:04:05.999Z format, ignoring it")) } } client.resumeDetails = &ResumeDetails{ - OldNick: oldnick, Timestamp: timestamp, PresentedToken: token, } diff --git a/irc/resume.go b/irc/resume.go new file mode 100644 index 00000000..efb2baa9 --- /dev/null +++ b/irc/resume.go @@ -0,0 +1,87 @@ +// Copyright (c) 2019 Shivaram Lingamneni +// released under the MIT license + +package irc + +import ( + "sync" + + "github.com/oragono/oragono/irc/utils" +) + +// implements draft/resume-0.3, in particular the issuing, management, and verification +// of resume tokens with two components: a unique ID and a secret key + +type resumeTokenPair struct { + client *Client + secret string +} + +type ResumeManager struct { + sync.RWMutex // level 2 + + resumeIDtoCreds map[string]resumeTokenPair + server *Server +} + +func (rm *ResumeManager) Initialize(server *Server) { + rm.resumeIDtoCreds = make(map[string]resumeTokenPair) + rm.server = server +} + +// GenerateToken generates a resume token for a client. If the client has +// already been assigned one, it returns "". +func (rm *ResumeManager) GenerateToken(client *Client) (token string) { + id := utils.GenerateSecretToken() + secret := utils.GenerateSecretToken() + + rm.Lock() + defer rm.Unlock() + + if client.ResumeID() != "" { + return + } + + client.SetResumeID(id) + rm.resumeIDtoCreds[id] = resumeTokenPair{ + client: client, + secret: secret, + } + + return id + secret +} + +// VerifyToken looks up the client corresponding to a resume token, returning +// nil if there is no such client or the token is invalid. +func (rm *ResumeManager) VerifyToken(token string) (client *Client) { + if len(token) != 2*utils.SecretTokenLength { + return + } + + rm.RLock() + defer rm.RUnlock() + + id := token[:utils.SecretTokenLength] + pair, ok := rm.resumeIDtoCreds[id] + if ok { + if utils.SecretTokensMatch(pair.secret, token[utils.SecretTokenLength:]) { + // disallow resume of an unregistered client; this prevents the use of + // resume as an auth bypass + if pair.client.Registered() { + return pair.client + } + } + } + return +} + +// Delete stops tracking a client's resume token. +func (rm *ResumeManager) Delete(client *Client) { + rm.Lock() + defer rm.Unlock() + + currentID := client.ResumeID() + if currentID != "" { + delete(rm.resumeIDtoCreds, currentID) + } +} diff --git a/irc/server.go b/irc/server.go index 67cf027d..c478afdb 100644 --- a/irc/server.go +++ b/irc/server.go @@ -88,6 +88,7 @@ type Server struct { rehashMutex sync.Mutex // tier 4 rehashSignal chan os.Signal pprofServer *http.Server + resumeManager ResumeManager signals chan os.Signal snomasks *SnoManager store *buntdb.DB @@ -130,6 +131,8 @@ func NewServer(config *Config, logger *logger.Manager) (*Server, error) { semaphores: NewServerSemaphores(), } + server.resumeManager.Initialize(server) + if err := server.applyConfig(config, true); err != nil { return nil, err } diff --git a/irc/utils/crypto.go b/irc/utils/crypto.go index c467974a..d332a355 100644 --- a/irc/utils/crypto.go +++ b/irc/utils/crypto.go @@ -14,6 +14,10 @@ var ( b32encoder = base32.NewEncoding("abcdefghijklmnopqrstuvwxyz234567").WithPadding(base32.NoPadding) ) +const ( + SecretTokenLength = 26 +) + // generate a secret token that cannot be brute-forced via online attacks func GenerateSecretToken() string { // 128 bits of entropy are enough to resist any online attack: diff --git a/irc/utils/crypto_test.go b/irc/utils/crypto_test.go index ed312e08..a5e60254 100644 --- a/irc/utils/crypto_test.go +++ b/irc/utils/crypto_test.go @@ -16,7 +16,7 @@ const ( func TestGenerateSecretToken(t *testing.T) { token := GenerateSecretToken() - if len(token) < 22 { + if len(token) != SecretTokenLength { t.Errorf("bad token: %v", token) } }