update resume support to draft/resume-0.3

This commit is contained in:
Shivaram Lingamneni 2019-02-12 00:27:57 -05:00
parent cf2445abe7
commit afe94d43c3
10 changed files with 121 additions and 47 deletions

@ -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",
),

@ -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",

@ -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()

@ -231,7 +231,7 @@ func init() {
"RESUME": {
handler: resumeHandler,
usablePreReg: true,
minParams: 2,
minParams: 1,
},
"SAJOIN": {
handler: sajoinHandler,

@ -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 {

@ -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 <oldnick> <token> [timestamp]
// RESUME <token> [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,
}

87
irc/resume.go Normal file

@ -0,0 +1,87 @@
// Copyright (c) 2019 Shivaram Lingamneni <slingamn@cs.stanford.edu>
// 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)
}
}

@ -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
}

@ -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:

@ -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)
}
}