From e52c9d36a6bfadc210d1832c463820fb0d682a45 Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Sun, 5 Feb 2023 16:29:03 +0100 Subject: [PATCH] support cram-md5 authentication for imap and smtp and change thunderbird autoconfiguration to use it. unfortunately, for microsoft autodiscover, there appears to be no way to request secure password negotiation. so it will default to plain text auth. cram-md5 is less secure than scram-sha-*, but thunderbird does not yet support scram auth. it currently chooses "plain", sending the literal password over the connection (which is TLS-protected, but we don't want to receive clear text passwords). in short, cram-md5 is better than nothing... for cram-md5 to work, a new set of derived credentials need to be stored in the database. so you need to save your password again to make it work. this was also the case with the scram-sha-1 addition, but i forgot to mention it then. --- http/autoconf.go | 23 +++++--- imapserver/authenticate_test.go | 50 ++++++++++++++++++ imapserver/server.go | 70 ++++++++++++++++++++++++- main.go | 8 +-- metrics/auth.go | 2 +- mox-/rand.go | 5 +- rfc/index.md | 2 + smtpserver/server.go | 75 ++++++++++++++++++++++++-- store/account.go | 93 +++++++++++++++++++++++++++++++-- 9 files changed, 306 insertions(+), 22 deletions(-) diff --git a/http/autoconf.go b/http/autoconf.go index 66c8d5a..d46d9bd 100644 --- a/http/autoconf.go +++ b/http/autoconf.go @@ -103,14 +103,14 @@ func autoconfHandle(l config.Listener) http.HandlerFunc { log.Error("autoconfig: no imap configured?") } - // todo: specify SCRAM-SHA256 once thunderbird and autoconfig supports it. we could implement CRAM-MD5 and use it. + // todo: specify SCRAM-SHA-256 once thunderbird and autoconfig supports it. or perhaps that will fall under "password-encrypted" by then. resp.EmailProvider.IncomingServer.Type = "imap" resp.EmailProvider.IncomingServer.Hostname = hostname.ASCII resp.EmailProvider.IncomingServer.Port = imapPort resp.EmailProvider.IncomingServer.SocketType = imapSocket resp.EmailProvider.IncomingServer.Username = email - resp.EmailProvider.IncomingServer.Authentication = "password-cleartext" + resp.EmailProvider.IncomingServer.Authentication = "password-encrypted" var smtpPort int var smtpSocket string @@ -133,7 +133,7 @@ func autoconfHandle(l config.Listener) http.HandlerFunc { resp.EmailProvider.OutgoingServer.Port = smtpPort resp.EmailProvider.OutgoingServer.SocketType = smtpSocket resp.EmailProvider.OutgoingServer.Username = email - resp.EmailProvider.OutgoingServer.Authentication = "password-cleartext" + resp.EmailProvider.OutgoingServer.Authentication = "password-encrypted" // todo: should we put the email address in the URL? resp.ClientConfigUpdate.URL = fmt.Sprintf("https://%s/mail/config-v1.1.xml", hostname.ASCII) @@ -150,10 +150,14 @@ func autoconfHandle(l config.Listener) http.HandlerFunc { // Autodiscover from Microsoft, also used by Thunderbird. // User should create a DNS record: _autodiscover._tcp. IN SRV 0 0 443 > -// In practice, autodiscover does not seem to work (any more). A connectivity test -// tool for outlook is available on https://testconnectivity.microsoft.com/, it has -// an option to do "Autodiscover to detect server settings". Incoming TLS -// connections are all failing, with various errors. +// +// In practice, autodiscover does not seem to work wit microsoft clients. A +// connectivity test tool for outlook is available on +// https://testconnectivity.microsoft.com/, it has an option to do "Autodiscover to +// detect server settings". Incoming TLS connections are all failing, with various +// errors. +// +// Thunderbird does understand autodiscover. func autodiscoverHandle(l config.Listener) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { log := xlog.WithContext(r.Context()) @@ -197,7 +201,10 @@ func autodiscoverHandle(l config.Listener) http.HandlerFunc { // High-level starting point, https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/78530279-d042-4eb0-a1f4-03b18143cd19 // Request: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/2096fab2-9c3c-40b9-b123-edf6e8d55a9b // Response, protocol: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/f4238db6-a983-435c-807a-b4b4a624c65b - // It appears autodiscover does not allow specifying SCRAM-SHA256 as authentication method. See https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/21fd2dd5-c4ee-485b-94fb-e7db5da93726 + // It appears autodiscover does not allow specifying SCRAM-SHA-256 as + // authentication method, or any authentication method that real clients actually + // use. See + // https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-oxdscli/21fd2dd5-c4ee-485b-94fb-e7db5da93726 var imapPort int imapSSL := "off" diff --git a/imapserver/authenticate_test.go b/imapserver/authenticate_test.go index 0dae24f..15c8b15 100644 --- a/imapserver/authenticate_test.go +++ b/imapserver/authenticate_test.go @@ -1,10 +1,13 @@ package imapserver import ( + "crypto/hmac" + "crypto/md5" "crypto/sha1" "crypto/sha256" "encoding/base64" "errors" + "fmt" "hash" "strings" "testing" @@ -124,3 +127,50 @@ func testAuthenticateSCRAM(t *testing.T, method string, h func() hash.Hash) { tc.transactf("bad", "authenticate %s %s", method, base64.StdEncoding.EncodeToString([]byte("bad data"))) tc.close() } + +func TestAuthenticateCRAMMD5(t *testing.T) { + tc := start(t) + + tc.transactf("no", "authenticate bogus ") + tc.transactf("bad", "authenticate CRAM-MD5 not base64...") + tc.transactf("bad", "authenticate CRAM-MD5 %s", base64.StdEncoding.EncodeToString([]byte("baddata"))) + tc.transactf("bad", "authenticate CRAM-MD5 %s", base64.StdEncoding.EncodeToString([]byte("bad data"))) + + auth := func(status string, username, password string) { + t.Helper() + + tc.client.LastTag = "x001" + tc.writelinef("%s authenticate CRAM-MD5", tc.client.LastTag) + + xreadContinuation := func() []byte { + line, _, result, rerr := tc.client.ReadContinuation() + tc.check(rerr, "read continuation") + if result.Status != "" { + tc.t.Fatalf("expected continuation") + } + buf, err := base64.StdEncoding.DecodeString(line) + tc.check(err, "parsing base64 from remote") + return buf + } + + chal := xreadContinuation() + h := hmac.New(md5.New, []byte(password)) + h.Write([]byte(chal)) + resp := fmt.Sprintf("%s %x", username, h.Sum(nil)) + tc.writelinef("%s", base64.StdEncoding.EncodeToString([]byte(resp))) + + _, result, err := tc.client.Response() + tc.check(err, "read response") + if string(result.Status) != strings.ToUpper(status) { + tc.t.Fatalf("got status %q, expected %q", result.Status, strings.ToUpper(status)) + } + } + + auth("no", "mjl@mox.example", "badpass") + auth("no", "mjl@mox.example", "") + auth("no", "other@mox.example", "testtest") + + auth("ok", "mjl@mox.example", "testtest") + + tc.close() +} diff --git a/imapserver/server.go b/imapserver/server.go index 1d42bed..30315a6 100644 --- a/imapserver/server.go +++ b/imapserver/server.go @@ -39,6 +39,7 @@ import ( "bufio" "bytes" "context" + "crypto/md5" "crypto/sha1" "crypto/sha256" "crypto/tls" @@ -118,8 +119,9 @@ var ( // ID: ../rfc/2971 // AUTH=SCRAM-SHA-256: ../rfc/7677 ../rfc/5802 // AUTH=SCRAM-SHA-1: ../rfc/5802 +// AUTH=CRAM-MD5: ../rfc/2195 // APPENDLIMIT, we support the max possible size, 1<<63 - 1: ../rfc/7889:129 -const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ONLY LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1 ID APPENDLIMIT=9223372036854775807" +const serverCapabilities = "IMAP4rev2 IMAP4rev1 ENABLE LITERAL+ IDLE SASL-IR BINARY UNSELECT UIDPLUS ESEARCH SEARCHRES MOVE UTF8=ONLY LIST-EXTENDED SPECIAL-USE LIST-STATUS AUTH=SCRAM-SHA-256 AUTH=SCRAM-SHA-1 AUTH=CRAM-MD5 ID APPENDLIMIT=9223372036854775807" type conn struct { cid int64 @@ -1392,6 +1394,71 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) { c.username = authc authResult = "ok" + case "CRAM-MD5": + authVariant = strings.ToLower(authType) + + // ../rfc/9051:1462 + p.xempty() + + // ../rfc/2195:82 + chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII) + c.writelinef("+ %s", base64.StdEncoding.EncodeToString([]byte(chal))) + + resp := xreadContinuation() + t := strings.Split(string(resp), " ") + if len(t) != 2 || len(t[1]) != 2*md5.Size { + xsyntaxErrorf("malformed cram-md5 response") + } + addr := t[0] + c.log.Info("cram-md5 auth", mlog.Field("address", addr)) + acc, _, err := store.OpenEmail(addr) + if err != nil { + if errors.Is(err, store.ErrUnknownCredentials) { + xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials") + } + xserverErrorf("looking up address: %v", err) + } + defer func() { + if acc != nil { + err := acc.Close() + c.xsanity(err, "close account") + } + }() + var ipadhash, opadhash hash.Hash + acc.WithRLock(func() { + err := acc.DB.Read(func(tx *bstore.Tx) error { + password, err := bstore.QueryTx[store.Password](tx).Get() + if err == bstore.ErrAbsent { + xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials") + } + if err != nil { + return err + } + + ipadhash = password.CRAMMD5.Ipad + opadhash = password.CRAMMD5.Opad + return nil + }) + xcheckf(err, "tx read") + }) + if ipadhash == nil || opadhash == nil { + c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", mlog.Field("address", addr)) + xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials") + } + + // ../rfc/2195:138 ../rfc/2104:142 + ipadhash.Write([]byte(chal)) + opadhash.Write(ipadhash.Sum(nil)) + digest := fmt.Sprintf("%x", opadhash.Sum(nil)) + if digest != t[1] { + xusercodeErrorf("AUTHENTICATIONFAILED", "bad credentials") + } + + c.account = acc + acc = nil // Cancel cleanup. + c.username = addr + authResult = "ok" + case "SCRAM-SHA-1", "SCRAM-SHA-256": // todo: improve handling of errors during scram. e.g. invalid parameters. should we abort the imap command, or continue until the end and respond with a scram-level error? // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go @@ -1438,6 +1505,7 @@ func (c *conn) cmdAuthenticate(tag, cmd string, p *parser) { xscram = password.SCRAMSHA256 } if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) { + c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", mlog.Field("address", ss.Authentication)) xuserErrorf("scram not possible") } xcheckf(err, "fetching credentials") diff --git a/main.go b/main.go index 0a2d275..55b8f00 100644 --- a/main.go +++ b/main.go @@ -876,8 +876,10 @@ func cmdSetaccountpassword(c *cmd) { c.params = "address" c.help = `Set new password an account. -The password is read from stdin. Its bcrypt hash and SCRAM-SHA-256 derivations -are stored in the accounts database. +The password is read from stdin. Secrets derived from the password, but not the +password itself, are stored in the account database. The stored secrets are for +authentication with: scram-sha-256, scram-sha-1, cram-md5, plain text (bcrypt +hash). Any email address configured for the account can be used. ` @@ -1958,7 +1960,7 @@ binary should be setgid that group: if !submitconf.STARTTLS { tlsMode = smtpclient.TLSSkip } - // todo: should have more auth options, scram-sha-256 at least. + // todo: should have more auth options, scram-sha-256 at least, perhaps cram-md5 for compatibility as well. authLine := fmt.Sprintf("AUTH PLAIN %s", base64.StdEncoding.EncodeToString([]byte(fmt.Sprintf("\u0000%s\u0000%s", submitconf.Username, submitconf.Password)))) mox.Conf.Static.HostnameDomain.ASCII = submitconf.LocalHostname client, err := smtpclient.New(ctx, mlog.New("sendmail"), conn, tlsMode, submitconf.Host, authLine) diff --git a/metrics/auth.go b/metrics/auth.go index baf3d53..9c7ba46 100644 --- a/metrics/auth.go +++ b/metrics/auth.go @@ -13,7 +13,7 @@ var ( }, []string{ "kind", // submission, imap, httpaccount, httpadmin - "variant", // login, plain, scram-sha-256, httpbasic + "variant", // login, plain, scram-sha-256, scram-sha-1, cram-md5, httpbasic // todo: we currently only use badcreds, but known baduser can be helpful "result", // ok, baduser, badpassword, badcreds, error, aborted }, diff --git a/mox-/rand.go b/mox-/rand.go index ec088a9..4fd3ee9 100644 --- a/mox-/rand.go +++ b/mox-/rand.go @@ -9,10 +9,11 @@ import ( // NewRand returns a new PRNG seeded with random bytes from crypto/rand. func NewRand() *mathrand.Rand { - return mathrand.New(mathrand.NewSource(cryptoRandInt())) + return mathrand.New(mathrand.NewSource(CryptoRandInt())) } -func cryptoRandInt() int64 { +// CryptoRandInt returns a cryptographically random number. +func CryptoRandInt() int64 { buf := make([]byte, 8) _, err := cryptorand.Read(buf) if err != nil { diff --git a/rfc/index.md b/rfc/index.md index 8e0ee07..e21a926 100644 --- a/rfc/index.md +++ b/rfc/index.md @@ -226,6 +226,8 @@ and many more, see http://sieve.info/documents # SASL +2104 HMAC: Keyed-Hashing for Message Authentication +2195 IMAP/POP AUTHorize Extension for Simple Challenge/Response 4013 (obsoleted by RFC 7613) SASLprep: Stringprep Profile for User Names and Passwords 4422 Simple Authentication and Security Layer (SASL) 4505 Anonymous Simple Authentication and Security Layer (SASL) Mechanism diff --git a/smtpserver/server.go b/smtpserver/server.go index be76222..3253262 100644 --- a/smtpserver/server.go +++ b/smtpserver/server.go @@ -5,6 +5,7 @@ import ( "bufio" "bytes" "context" + "crypto/md5" "crypto/rsa" "crypto/sha1" "crypto/sha256" @@ -682,7 +683,7 @@ func (c *conn) cmdHello(p *parser, ehlo bool) { if c.submission { // ../rfc/4954:123 if c.tls || !c.requireTLSForAuth { - c.bwritelinef("250-AUTH PLAIN SCRAM-SHA-256 SCRAM-SHA-1") + c.bwritelinef("250-AUTH PLAIN SCRAM-SHA-256 SCRAM-SHA-1 CRAM-MD5") } else { c.bwritelinef("250-AUTH ") } @@ -864,6 +865,75 @@ func (c *conn) cmdAuth(p *parser) { // ../rfc/4954:276 c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil) + case "CRAM-MD5": + authVariant = strings.ToLower(mech) + + p.xempty() + + // ../rfc/2195:82 + chal := fmt.Sprintf("<%d.%d@%s>", uint64(mox.CryptoRandInt()), time.Now().UnixNano(), mox.Conf.Static.HostnameDomain.ASCII) + c.writelinef("%d %s", smtp.C334ContinueAuth, base64.StdEncoding.EncodeToString([]byte(chal))) + + resp := xreadContinuation() + t := strings.Split(string(resp), " ") + if len(t) != 2 || len(t[1]) != 2*md5.Size { + xsmtpUserErrorf(smtp.C501BadParamSyntax, smtp.SeProto5BadParams4, "malformed cram-md5 response") + } + addr := t[0] + c.log.Info("cram-md5 auth", mlog.Field("address", addr)) + acc, _, err := store.OpenEmail(addr) + if err != nil { + if errors.Is(err, store.ErrUnknownCredentials) { + xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass") + } + } + xcheckf(err, "looking up address") + defer func() { + if acc != nil { + err := acc.Close() + if err != nil { + c.log.Errorx("closing account", err) + } + } + }() + var ipadhash, opadhash hash.Hash + acc.WithRLock(func() { + err := acc.DB.Read(func(tx *bstore.Tx) error { + password, err := bstore.QueryTx[store.Password](tx).Get() + if err == bstore.ErrAbsent { + xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass") + } + if err != nil { + return err + } + + ipadhash = password.CRAMMD5.Ipad + opadhash = password.CRAMMD5.Opad + return nil + }) + xcheckf(err, "tx read") + }) + if ipadhash == nil || opadhash == nil { + c.log.Info("cram-md5 auth attempt without derived secrets set, save password again to store secrets", mlog.Field("address", addr)) + xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass") + } + + // ../rfc/2195:138 ../rfc/2104:142 + ipadhash.Write([]byte(chal)) + opadhash.Write(ipadhash.Sum(nil)) + digest := fmt.Sprintf("%x", opadhash.Sum(nil)) + if digest != t[1] { + xsmtpUserErrorf(smtp.C535AuthBadCreds, smtp.SePol7AuthBadCreds8, "bad user/pass") + } + + authResult = "ok" + c.authFailed = 0 + c.account = acc + acc = nil // Cancel cleanup. + c.username = addr + // ../rfc/4954:276 + c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil) + case "SCRAM-SHA-1", "SCRAM-SHA-256": // todo: improve handling of errors during scram. e.g. invalid parameters. should we abort the imap command, or continue until the end and respond with a scram-level error? // todo: use single implementation between ../imapserver/server.go and ../smtpserver/server.go @@ -910,6 +980,7 @@ func (c *conn) cmdAuth(p *parser) { xscram = password.SCRAMSHA256 } if err == bstore.ErrAbsent || err == nil && (len(xscram.Salt) == 0 || xscram.Iterations == 0 || len(xscram.SaltedPassword) == 0) { + c.log.Info("scram auth attempt without derived secrets set, save password again to store secrets", mlog.Field("address", ss.Authentication)) xsmtpUserErrorf(smtp.C454TempAuthFail, smtp.SeSys3Other0, "scram not possible") } xcheckf(err, "fetching credentials") @@ -947,8 +1018,6 @@ func (c *conn) cmdAuth(p *parser) { c.writecodeline(smtp.C235AuthSuccess, smtp.SePol7Other0, "nice", nil) default: - // todo future: implement scram-sha-256 ../rfc/7677 - // todo future: possibly implement cram-md5, at least where we allow PLAIN. ../rfc/4954:348 // ../rfc/4954:176 xsmtpUserErrorf(smtp.C504ParamNotImpl, smtp.SeProto5BadParams4, "mechanism %s not supported", mech) } diff --git a/store/account.go b/store/account.go index 146eacc..c89c30f 100644 --- a/store/account.go +++ b/store/account.go @@ -24,11 +24,14 @@ package store import ( "context" + "crypto/md5" "crypto/sha1" "crypto/sha256" + "encoding" "encoding/json" "errors" "fmt" + "hash" "io" "os" "path/filepath" @@ -70,11 +73,68 @@ type SCRAM struct { SaltedPassword []byte } -// Password holds a bcrypt hash for logging in with SMTP/IMAP/admin. +// CRAMMD5 holds HMAC ipad and opad hashes that are initialized with the first +// block with (a derivation of) the key/password, so we don't store the password in plain +// text. +type CRAMMD5 struct { + Ipad hash.Hash + Opad hash.Hash +} + +// BinaryMarshal is used by bstore to store the ipad/opad hash states. +func (c CRAMMD5) MarshalBinary() ([]byte, error) { + if c.Ipad == nil || c.Opad == nil { + return nil, nil + } + + ipad, err := c.Ipad.(encoding.BinaryMarshaler).MarshalBinary() + if err != nil { + return nil, fmt.Errorf("marshal ipad: %v", err) + } + opad, err := c.Opad.(encoding.BinaryMarshaler).MarshalBinary() + if err != nil { + return nil, fmt.Errorf("marshal opad: %v", err) + } + buf := make([]byte, 2+len(ipad)+len(opad)) + ipadlen := uint16(len(ipad)) + buf[0] = byte(ipadlen >> 8) + buf[1] = byte(ipadlen >> 0) + copy(buf[2:], ipad) + copy(buf[2+len(ipad):], opad) + return buf, nil +} + +// BinaryUnmarshal is used by bstore to restore the ipad/opad hash states. +func (c *CRAMMD5) UnmarshalBinary(buf []byte) error { + if len(buf) == 0 { + *c = CRAMMD5{} + return nil + } + if len(buf) < 2 { + return fmt.Errorf("short buffer") + } + ipadlen := int(uint16(buf[0])<<8 | uint16(buf[1])<<0) + if len(buf) < 2+ipadlen { + return fmt.Errorf("buffer too short for ipadlen") + } + ipad := md5.New() + opad := md5.New() + if err := ipad.(encoding.BinaryUnmarshaler).UnmarshalBinary(buf[2 : 2+ipadlen]); err != nil { + return fmt.Errorf("unmarshal ipad: %v", err) + } + if err := opad.(encoding.BinaryUnmarshaler).UnmarshalBinary(buf[2+ipadlen:]); err != nil { + return fmt.Errorf("unmarshal opad: %v", err) + } + *c = CRAMMD5{ipad, opad} + return nil +} + +// Password holds credentials in various forms, for logging in with SMTP/IMAP. type Password struct { - Hash string - SCRAMSHA1 SCRAM - SCRAMSHA256 SCRAM + Hash string // bcrypt hash for IMAP LOGIN and SASL PLAIN authentication. + CRAMMD5 CRAMMD5 // For SASL CRAM-MD5. + SCRAMSHA1 SCRAM // For SASL SCRAM-SHA-1. + SCRAMSHA256 SCRAM // For SASL SCRAM-SHA-256. } // Subjectpass holds the secret key used to sign subjectpass tokens. @@ -615,6 +675,31 @@ func (a *Account) SetPassword(password string) error { var pw Password pw.Hash = string(hash) + // CRAM-MD5 calculates an HMAC-MD5, with the password as key, over a per-attempt + // unique text that includes a timestamp. HMAC performs two hashes. Both times, the + // first block is based on the key/password. We hash those first blocks now, and + // store the hash state in the database. When we actually authenticate, we'll + // complete the HMAC by hashing only the text. We cannot store crypto/hmac's hash, + // because it does not expose its internal state and isn't a BinaryMarshaler. + // ../rfc/2104:121 + pw.CRAMMD5.Ipad = md5.New() + pw.CRAMMD5.Opad = md5.New() + key := []byte(password) + if len(key) > 64 { + t := md5.Sum(key) + key = t[:] + } + ipad := make([]byte, md5.BlockSize) + opad := make([]byte, md5.BlockSize) + copy(ipad, key) + copy(opad, key) + for i := range ipad { + ipad[i] ^= 0x36 + opad[i] ^= 0x5c + } + pw.CRAMMD5.Ipad.Write(ipad) + pw.CRAMMD5.Opad.Write(opad) + pw.SCRAMSHA1.Salt = scram.MakeRandom() pw.SCRAMSHA1.Iterations = 2 * 4096 pw.SCRAMSHA1.SaltedPassword = scram.SaltPassword(sha1.New, password, pw.SCRAMSHA1.Salt, pw.SCRAMSHA1.Iterations)