Changeset 95 in code for trunk


Ignore:
Timestamp:
Mar 13, 2020, 2:12:44 PM (5 years ago)
Author:
contact
Message:

Add support for SASL authentication

We now store SASL credentials in the database and automatically populate
them on NickServ REGISTER/IDENTIFY.

References: https://todo.sr.ht/~emersion/jounce/10

Location:
trunk
Files:
7 edited

Legend:

Unmodified
Added
Removed
  • trunk/db.go

    r93 r95  
    1111        Username string
    1212        Password string // hashed
     13}
     14
     15type SASL struct {
     16        Mechanism string
     17
     18        Plain struct {
     19                Username string
     20                Password string
     21        }
    1322}
    1423
     
    2029        Realname string
    2130        Pass     string
     31        SASL     SASL
    2232}
    2333
     
    4454        defer db.lock.Unlock()
    4555        return db.Close()
     56}
     57
     58func fromStringPtr(ptr *string) string {
     59        if ptr == nil {
     60                return ""
     61        }
     62        return *ptr
     63}
     64
     65func toStringPtr(s string) *string {
     66        if s == "" {
     67                return nil
     68        }
     69        return &s
    4670}
    4771
     
    6387                        return nil, err
    6488                }
    65                 if password != nil {
    66                         user.Password = *password
    67                 }
     89                user.Password = fromStringPtr(password)
    6890                users = append(users, user)
    6991        }
     
    79101        defer db.lock.Unlock()
    80102
    81         var password *string
    82         if user.Password != "" {
    83                 password = &user.Password
    84         }
     103        password := toStringPtr(user.Password)
    85104        _, err := db.db.Exec("INSERT INTO User(username, password) VALUES (?, ?)", user.Username, password)
    86105        return err
     
    91110        defer db.lock.RUnlock()
    92111
    93         rows, err := db.db.Query("SELECT id, addr, nick, username, realname, pass FROM Network WHERE user = ?", username)
     112        rows, err := db.db.Query(`SELECT id, addr, nick, username, realname, pass,
     113                        sasl_mechanism, sasl_plain_username, sasl_plain_password
     114                FROM Network
     115                WHERE user = ?`,
     116                username)
    94117        if err != nil {
    95118                return nil, err
     
    101124                var net Network
    102125                var username, realname, pass *string
    103                 if err := rows.Scan(&net.ID, &net.Addr, &net.Nick, &username, &realname, &pass); err != nil {
     126                var saslMechanism, saslPlainUsername, saslPlainPassword *string
     127                err := rows.Scan(&net.ID, &net.Addr, &net.Nick, &username, &realname,
     128                        &pass, &saslMechanism, &saslPlainUsername, &saslPlainPassword)
     129                if err != nil {
    104130                        return nil, err
    105131                }
    106                 if username != nil {
    107                         net.Username = *username
    108                 }
    109                 if realname != nil {
    110                         net.Realname = *realname
    111                 }
    112                 if pass != nil {
    113                         net.Pass = *pass
    114                 }
     132                net.Username = fromStringPtr(username)
     133                net.Realname = fromStringPtr(realname)
     134                net.Pass = fromStringPtr(pass)
     135                net.SASL.Mechanism = fromStringPtr(saslMechanism)
     136                net.SASL.Plain.Username = fromStringPtr(saslPlainUsername)
     137                net.SASL.Plain.Password = fromStringPtr(saslPlainPassword)
    115138                networks = append(networks, net)
    116139        }
     
    126149        defer db.lock.Unlock()
    127150
    128         var netUsername, realname, pass *string
    129         if network.Username != "" {
    130                 netUsername = &network.Username
    131         }
    132         if network.Realname != "" {
    133                 realname = &network.Realname
    134         }
    135         if network.Pass != "" {
    136                 pass = &network.Pass
     151        netUsername := toStringPtr(network.Username)
     152        realname := toStringPtr(network.Realname)
     153        pass := toStringPtr(network.Pass)
     154
     155        var saslMechanism, saslPlainUsername, saslPlainPassword *string
     156        if network.SASL.Mechanism != "" {
     157                saslMechanism = &network.SASL.Mechanism
     158                switch network.SASL.Mechanism {
     159                case "PLAIN":
     160                        saslPlainUsername = toStringPtr(network.SASL.Plain.Username)
     161                        saslPlainPassword = toStringPtr(network.SASL.Plain.Password)
     162                }
    137163        }
    138164
     
    140166        if network.ID != 0 {
    141167                _, err = db.db.Exec(`UPDATE Network
    142                         SET addr = ?, nick = ?, username = ?, realname = ?, pass = ?
     168                        SET addr = ?, nick = ?, username = ?, realname = ?, pass = ?,
     169                                sasl_mechanism = ?, sasl_plain_username = ?, sasl_plain_password = ?
    143170                        WHERE id = ?`,
    144                         network.Addr, network.Nick, netUsername, realname, pass, network.ID)
     171                        network.Addr, network.Nick, netUsername, realname, pass,
     172                        saslMechanism, saslPlainUsername, saslPlainPassword, network.ID)
    145173        } else {
    146174                var res sql.Result
    147175                res, err = db.db.Exec(`INSERT INTO Network(user, addr, nick, username,
    148                                 realname, pass)
    149                         VALUES (?, ?, ?, ?, ?, ?)`,
    150                         username, network.Addr, network.Nick, netUsername, realname, pass)
     176                                realname, pass, sasl_mechanism, sasl_plain_username,
     177                                sasl_plain_password)
     178                        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
     179                        username, network.Addr, network.Nick, netUsername, realname, pass,
     180                        saslMechanism, saslPlainUsername, saslPlainPassword)
    151181                if err != nil {
    152182                        return err
  • trunk/downstream.go

    r93 r95  
    382382                        }
    383383
    384                         dc.logger.Printf("trying to connect to new upstream server %q", addr)
     384                        dc.logger.Printf("trying to connect to new network %q", addr)
    385385                        if err := sanityCheckServer(addr); err != nil {
    386386                                dc.logger.Printf("failed to connect to %q: %v", addr, err)
     
    391391                        }
    392392
    393                         dc.logger.Printf("auto-adding network %q", networkName)
     393                        dc.logger.Printf("auto-saving network %q", networkName)
    394394                        network, err = u.createNetwork(networkName, dc.nick)
    395395                        if err != nil {
     
    619619                        }
    620620
     621                        if upstreamName == "NickServ" {
     622                                dc.handleNickServPRIVMSG(uc, text)
     623                        }
     624
    621625                        uc.SendMessage(&irc.Message{
    622626                                Command: "PRIVMSG",
     
    630634        return nil
    631635}
     636
     637func (dc *downstreamConn) handleNickServPRIVMSG(uc *upstreamConn, text string) {
     638        username, password, ok := parseNickServCredentials(text, uc.nick)
     639        if !ok {
     640                return
     641        }
     642
     643        dc.logger.Printf("auto-saving NickServ credentials with username %q", username)
     644        n := uc.network
     645        n.SASL.Mechanism = "PLAIN"
     646        n.SASL.Plain.Username = username
     647        n.SASL.Plain.Password = password
     648        if err := dc.srv.db.StoreNetwork(dc.user.Username, &n.Network); err != nil {
     649                dc.logger.Printf("failed to save NickServ credentials: %v", err)
     650        }
     651}
     652
     653func parseNickServCredentials(text, nick string) (username, password string, ok bool) {
     654        fields := strings.Fields(text)
     655        if len(fields) < 2 {
     656                return "", "", false
     657        }
     658        cmd := strings.ToUpper(fields[0])
     659        params := fields[1:]
     660        switch cmd {
     661        case "REGISTER":
     662                username = nick
     663                password = params[0]
     664        case "IDENTIFY":
     665                if len(params) == 1 {
     666                        username = nick
     667                } else {
     668                        username = params[0]
     669                }
     670                password = params[1]
     671        }
     672        return username, password, true
     673}
  • trunk/go.mod

    r84 r95  
    44
    55require (
     6        github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b
    67        github.com/mattn/go-sqlite3 v2.0.3+incompatible
    78        golang.org/x/crypto v0.0.0-20200311171314-f7b00557c8c4
  • trunk/go.sum

    r84 r95  
     1github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b h1:uhWtEWBHgop1rqEk2klKaxPAkVDCXexai6hSuRQ7Nvs=
     2github.com/emersion/go-sasl v0.0.0-20191210011802-430746ea8b9b/go.mod h1:G/dpzLu16WtQpBfQ/z3LYiYJn3ZhKSGWn83fyoyQe/k=
    13github.com/mattn/go-sqlite3 v1.13.0 h1:LnJI81JidiW9r7pS/hXe6cFeO5EXNq7KbfvoJLRI69c=
    24github.com/mattn/go-sqlite3 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
  • trunk/irc.go

    r43 r95  
    1212        rpl_globalusers  = "266"
    1313        rpl_topicwhotime = "333"
     14        rpl_loggedin     = "900"
     15        rpl_loggedout    = "901"
     16        err_nicklocked   = "902"
     17        rpl_saslsuccess  = "903"
     18        err_saslfail     = "904"
     19        err_sasltoolong  = "905"
     20        err_saslaborted  = "906"
     21        err_saslalready  = "907"
     22        rpl_saslmechs    = "908"
    1423)
    1524
  • trunk/schema.sql

    r94 r95  
    1212        realname VARCHAR(255),
    1313        pass VARCHAR(255),
     14        sasl_mechanism VARCHAR(255),
     15        sasl_plain_username VARCHAR(255),
     16        sasl_plain_password VARCHAR(255),
    1417        FOREIGN KEY(user) REFERENCES User(username),
    1518        UNIQUE(user, addr, nick)
  • trunk/upstream.go

    r93 r95  
    33import (
    44        "crypto/tls"
     5        "encoding/base64"
    56        "fmt"
    67        "io"
     
    1011        "time"
    1112
     13        "github.com/emersion/go-sasl"
    1214        "gopkg.in/irc.v3"
    1315)
     
    4951        history    map[string]uint64
    5052        caps       map[string]string
     53
     54        saslClient  sasl.Client
     55        saslStarted bool
    5156}
    5257
     
    170175                uc.logger.Print(msg)
    171176        case "CAP":
    172                 if len(msg.Params) < 2 {
    173                         return newNeedMoreParamsError(msg.Command)
    174                 }
    175                 caps := strings.Fields(msg.Params[len(msg.Params)-1])
    176                 more := msg.Params[len(msg.Params)-2] == "*"
    177 
    178                 for _, s := range caps {
    179                         kv := strings.SplitN(s, "=", 2)
    180                         k := strings.ToLower(kv[0])
    181                         var v string
    182                         if len(kv) >= 2 {
    183                                 v = kv[1]
    184                         }
    185                         uc.caps[k] = v
    186                 }
    187 
    188                 if !more {
     177                var subCmd string
     178                if err := parseMessageParams(msg, nil, &subCmd); err != nil {
     179                        return err
     180                }
     181                subCmd = strings.ToUpper(subCmd)
     182                subParams := msg.Params[2:]
     183                switch subCmd {
     184                case "LS":
     185                        if len(subParams) < 1 {
     186                                return newNeedMoreParamsError(msg.Command)
     187                        }
     188                        caps := strings.Fields(subParams[len(subParams)-1])
     189                        more := len(subParams) >= 2 && msg.Params[len(subParams)-2] == "*"
     190
     191                        for _, s := range caps {
     192                                kv := strings.SplitN(s, "=", 2)
     193                                k := strings.ToLower(kv[0])
     194                                var v string
     195                                if len(kv) == 2 {
     196                                        v = kv[1]
     197                                }
     198                                uc.caps[k] = v
     199                        }
     200
     201                        if more {
     202                                break // wait to receive all capabilities
     203                        }
     204
     205                        if uc.requestSASL() {
     206                                uc.SendMessage(&irc.Message{
     207                                        Command: "CAP",
     208                                        Params:  []string{"REQ", "sasl"},
     209                                })
     210                                break // we'll send CAP END after authentication is completed
     211                        }
     212
    189213                        uc.SendMessage(&irc.Message{
    190214                                Command: "CAP",
    191215                                Params:  []string{"END"},
    192216                        })
    193                 }
     217                case "ACK", "NAK":
     218                        if len(subParams) < 1 {
     219                                return newNeedMoreParamsError(msg.Command)
     220                        }
     221                        caps := strings.Fields(subParams[0])
     222
     223                        for _, name := range caps {
     224                                if err := uc.handleCapAck(strings.ToLower(name), subCmd == "ACK"); err != nil {
     225                                        return err
     226                                }
     227                        }
     228
     229                        if uc.saslClient == nil {
     230                                uc.SendMessage(&irc.Message{
     231                                        Command: "CAP",
     232                                        Params:  []string{"END"},
     233                                })
     234                        }
     235                default:
     236                        uc.logger.Printf("unhandled message: %v", msg)
     237                }
     238        case "AUTHENTICATE":
     239                if uc.saslClient == nil {
     240                        return fmt.Errorf("received unexpected AUTHENTICATE message")
     241                }
     242
     243                // TODO: if a challenge is 400 bytes long, buffer it
     244                var challengeStr string
     245                if err := parseMessageParams(msg, &challengeStr); err != nil {
     246                        uc.SendMessage(&irc.Message{
     247                                Command: "AUTHENTICATE",
     248                                Params:  []string{"*"},
     249                        })
     250                        return err
     251                }
     252
     253                var challenge []byte
     254                if challengeStr != "+" {
     255                        var err error
     256                        challenge, err = base64.StdEncoding.DecodeString(challengeStr)
     257                        if err != nil {
     258                                uc.SendMessage(&irc.Message{
     259                                        Command: "AUTHENTICATE",
     260                                        Params:  []string{"*"},
     261                                })
     262                                return err
     263                        }
     264                }
     265
     266                var resp []byte
     267                var err error
     268                if !uc.saslStarted {
     269                        _, resp, err = uc.saslClient.Start()
     270                        uc.saslStarted = true
     271                } else {
     272                        resp, err = uc.saslClient.Next(challenge)
     273                }
     274                if err != nil {
     275                        uc.SendMessage(&irc.Message{
     276                                Command: "AUTHENTICATE",
     277                                Params:  []string{"*"},
     278                        })
     279                        return err
     280                }
     281
     282                // TODO: send response in multiple chunks if >= 400 bytes
     283                var respStr = "+"
     284                if resp != nil {
     285                        respStr = base64.StdEncoding.EncodeToString(resp)
     286                }
     287
     288                uc.SendMessage(&irc.Message{
     289                        Command: "AUTHENTICATE",
     290                        Params:  []string{respStr},
     291                })
     292        case rpl_loggedin:
     293                var account string
     294                if err := parseMessageParams(msg, nil, nil, &account); err != nil {
     295                        return err
     296                }
     297                uc.logger.Printf("logged in with account %q", account)
     298        case rpl_loggedout:
     299                uc.logger.Printf("logged out")
     300        case err_nicklocked, rpl_saslsuccess, err_saslfail, err_sasltoolong, err_saslaborted:
     301                var info string
     302                if err := parseMessageParams(msg, nil, &info); err != nil {
     303                        return err
     304                }
     305                switch msg.Command {
     306                case err_nicklocked:
     307                        uc.logger.Printf("invalid nick used with SASL authentication: %v", info)
     308                case err_saslfail:
     309                        uc.logger.Printf("SASL authentication failed: %v", info)
     310                case err_sasltoolong:
     311                        uc.logger.Printf("SASL message too long: %v", info)
     312                }
     313
     314                uc.saslClient = nil
     315                uc.saslStarted = false
     316
     317                uc.SendMessage(&irc.Message{
     318                        Command: "CAP",
     319                        Params:  []string{"END"},
     320                })
    194321        case irc.RPL_WELCOME:
    195322                uc.registered = true
     
    440567                // Ignore
    441568        default:
    442                 uc.logger.Printf("unhandled upstream message: %v", msg)
     569                uc.logger.Printf("unhandled message: %v", msg)
    443570        }
    444571        return nil
     
    478605}
    479606
     607func (uc *upstreamConn) requestSASL() bool {
     608        if uc.network.SASL.Mechanism == "" {
     609                return false
     610        }
     611
     612        v, ok := uc.caps["sasl"]
     613        if !ok {
     614                return false
     615        }
     616        if v != "" {
     617                mechanisms := strings.Split(v, ",")
     618                found := false
     619                for _, mech := range mechanisms {
     620                        if strings.EqualFold(mech, uc.network.SASL.Mechanism) {
     621                                found = true
     622                                break
     623                        }
     624                }
     625                if !found {
     626                        return false
     627                }
     628        }
     629
     630        return true
     631}
     632
     633func (uc *upstreamConn) handleCapAck(name string, ok bool) error {
     634        auth := &uc.network.SASL
     635        switch name {
     636        case "sasl":
     637                if !ok {
     638                        uc.logger.Printf("server refused to acknowledge the SASL capability")
     639                        return nil
     640                }
     641
     642                switch auth.Mechanism {
     643                case "PLAIN":
     644                        uc.logger.Printf("starting SASL PLAIN authentication with username %q", auth.Plain.Username)
     645                        uc.saslClient = sasl.NewPlainClient("", auth.Plain.Username, auth.Plain.Password)
     646                default:
     647                        return fmt.Errorf("unsupported SASL mechanism %q", name)
     648                }
     649
     650                uc.SendMessage(&irc.Message{
     651                        Command: "AUTHENTICATE",
     652                        Params:  []string{auth.Mechanism},
     653                })
     654        }
     655        return nil
     656}
     657
    480658func (uc *upstreamConn) readMessages() error {
    481659        for {
Note: See TracChangeset for help on using the changeset viewer.