mirror of
https://github.com/hanzoai/docdb.git
synced 2026-10-30 18:39:39 +00:00
272 lines
6.3 KiB
Go
272 lines
6.3 KiB
Go
// Copyright 2021 Hanzo AI Inc.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package scram
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"log/slog"
|
|
"strconv"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/AlekSi/lazyerrors"
|
|
)
|
|
|
|
// message represents a parsed SCRAM message.
|
|
type message struct {
|
|
gs2 string // gs2-cbind-flag,authzid
|
|
n string // username
|
|
c string // channel-binding
|
|
r string // nonce
|
|
s string // base64-encoded salt
|
|
p string // base64-encoded proof
|
|
v string // base64-encoded verifier
|
|
i int // iteration-count
|
|
}
|
|
|
|
// String returns the string representation of the message.
|
|
func (m *message) String() string {
|
|
parts := make([]string, 0, 2)
|
|
|
|
if m.gs2 != "" {
|
|
parts = append(parts, m.gs2)
|
|
}
|
|
|
|
if m.n != "" {
|
|
n := strings.ReplaceAll(m.n, "=", "=3D")
|
|
n = strings.ReplaceAll(n, ",", "=2C")
|
|
parts = append(parts, "n="+n)
|
|
}
|
|
|
|
if m.c != "" {
|
|
parts = append(parts, "c="+m.c)
|
|
}
|
|
|
|
if m.r != "" {
|
|
parts = append(parts, "r="+m.r)
|
|
}
|
|
|
|
if m.s != "" {
|
|
parts = append(parts, "s="+m.s)
|
|
}
|
|
|
|
if m.i != 0 {
|
|
parts = append(parts, "i="+strconv.Itoa(m.i))
|
|
}
|
|
|
|
if m.p != "" {
|
|
parts = append(parts, "p="+m.p)
|
|
}
|
|
|
|
if m.v != "" {
|
|
parts = append(parts, "v="+m.v)
|
|
}
|
|
|
|
return strings.Join(parts, ",")
|
|
}
|
|
|
|
// isClientFirst returns true if the message is a client-first message.
|
|
func (m *message) isClientFirst() bool {
|
|
ok := m.gs2 == "n,"
|
|
ok = ok && m.n != ""
|
|
ok = ok && m.c == ""
|
|
ok = ok && m.r != ""
|
|
ok = ok && m.s == ""
|
|
ok = ok && m.i == 0
|
|
ok = ok && m.p == ""
|
|
ok = ok && m.v == ""
|
|
|
|
return ok
|
|
}
|
|
|
|
// isServerFirst returns true if the message is a server-first message.
|
|
func (m *message) isServerFirst() bool {
|
|
ok := m.gs2 == ""
|
|
ok = ok && m.n == ""
|
|
ok = ok && m.c == ""
|
|
ok = ok && m.r != ""
|
|
ok = ok && m.s != ""
|
|
ok = ok && m.i != 0
|
|
ok = ok && m.p == ""
|
|
ok = ok && m.v == ""
|
|
|
|
return ok
|
|
}
|
|
|
|
// isClientFinal returns true if the message is a client-final message.
|
|
func (m *message) isClientFinal() bool {
|
|
ok := m.gs2 == ""
|
|
ok = ok && m.n == ""
|
|
ok = ok && m.c != ""
|
|
ok = ok && m.r != ""
|
|
ok = ok && m.s == ""
|
|
ok = ok && m.i == 0
|
|
ok = ok && m.p != ""
|
|
ok = ok && m.v == ""
|
|
|
|
return ok
|
|
}
|
|
|
|
// isServerFinal returns true if the message is a server-final message.
|
|
func (m *message) isServerFinal() bool {
|
|
ok := m.gs2 == ""
|
|
ok = ok && m.n == ""
|
|
ok = ok && m.c == ""
|
|
ok = ok && m.r == ""
|
|
ok = ok && m.s == ""
|
|
ok = ok && m.i == 0
|
|
ok = ok && m.p == ""
|
|
ok = ok && m.v != ""
|
|
|
|
return ok
|
|
}
|
|
|
|
// base64Decode decodes a base64-encoded value, producing better messages on errors.
|
|
func base64Decode(field, value string) ([]byte, error) {
|
|
if value == "" {
|
|
return nil, lazyerrors.Errorf("empty SCRAM attribute: %q", field)
|
|
}
|
|
|
|
b, err := base64.StdEncoding.DecodeString(value)
|
|
if err != nil {
|
|
return nil, lazyerrors.Errorf("failed to decode base64 SCRAM attribute: %q: %w", field, err)
|
|
}
|
|
|
|
return b, nil
|
|
}
|
|
|
|
// parseMessage parses a SCRAM message.
|
|
func parseMessage(msg string, l *slog.Logger) (*message, error) {
|
|
if !utf8.ValidString(msg) {
|
|
return nil, lazyerrors.Errorf("invalid UTF-8: %q", msg)
|
|
}
|
|
|
|
var res message
|
|
|
|
// better check for gs2 header
|
|
// TODO https://github.com/hanzoai/docdb-DocumentDB/issues/901
|
|
if strings.HasPrefix(msg, "n,,") {
|
|
msg = strings.TrimPrefix(msg, "n,,")
|
|
res.gs2 = "n,"
|
|
}
|
|
|
|
fields := strings.Split(msg, ",")
|
|
|
|
// https://datatracker.ietf.org/doc/html/rfc5802#section-5.1 says:
|
|
// > Note that the order of attributes in client or server messages is fixed
|
|
// We should enforce that.
|
|
// TODO https://github.com/hanzoai/docdb-DocumentDB/issues/901
|
|
|
|
for _, field := range fields {
|
|
name, value, ok := strings.Cut(field, "=")
|
|
if !ok {
|
|
return nil, lazyerrors.Errorf("malformed SCRAM attribute: %q", field)
|
|
}
|
|
|
|
// in order of https://datatracker.ietf.org/doc/html/rfc5802#section-5.1
|
|
switch name {
|
|
case "a":
|
|
if value != "" {
|
|
return nil, lazyerrors.Errorf("unsupported SCRAM attribute 'a': %q", field)
|
|
}
|
|
|
|
case "n":
|
|
// SASLprep, check if = is not followed by either 2C or 3D
|
|
// TODO https://github.com/hanzoai/docdb-DocumentDB/issues/901
|
|
value = strings.ReplaceAll(value, "=2C", ",")
|
|
value = strings.ReplaceAll(value, "=3D", "=")
|
|
|
|
if value == "" {
|
|
return nil, lazyerrors.Errorf("empty SCRAM attribute 'n': %q", field)
|
|
}
|
|
|
|
res.n = value
|
|
|
|
case "m":
|
|
return nil, lazyerrors.Errorf("unsupported SCRAM attribute 'm': %q", field)
|
|
|
|
case "r":
|
|
if len(value) < 16 {
|
|
return nil, lazyerrors.Errorf("SCRAM attribute 'r' is too short: %q", field)
|
|
}
|
|
|
|
res.r = value
|
|
|
|
case "c":
|
|
if value != "biws" { // "n,," base64-encoded
|
|
return nil, lazyerrors.Errorf("unsupported SCRAM attribute 'c': %q", field)
|
|
}
|
|
|
|
res.c = value
|
|
|
|
case "s":
|
|
s, err := base64Decode(field, value)
|
|
if err != nil {
|
|
return nil, lazyerrors.Error(err)
|
|
}
|
|
|
|
sl := len(s)
|
|
if sl < 12 {
|
|
return nil, lazyerrors.Errorf("SCRAM attribute 's' has incorrect length %d: %q", sl, field)
|
|
}
|
|
|
|
if sl != 28 {
|
|
// Legacy mongo shell and some older drivers require exactly 28 bytes,
|
|
// but most current drivers are fine.
|
|
// We still want users to use credentials created by DocumentDB.
|
|
l.Warn(
|
|
"SCRAM attribute 's' has unexpected length; it is recommended to use users created by `createUser` command",
|
|
slog.Int("l", sl),
|
|
)
|
|
}
|
|
|
|
res.s = value
|
|
|
|
case "i":
|
|
i, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return nil, lazyerrors.Errorf("failed to parse SCRAM attribute 'i': %q: %w", field, err)
|
|
}
|
|
|
|
if i < 4096 {
|
|
return nil, lazyerrors.Errorf("SCRAM attribute 'i' is too small: %q", field)
|
|
}
|
|
|
|
res.i = i
|
|
|
|
case "p":
|
|
if _, err := base64Decode(field, value); err != nil {
|
|
return nil, lazyerrors.Error(err)
|
|
}
|
|
|
|
res.p = value
|
|
|
|
case "v":
|
|
if _, err := base64Decode(field, value); err != nil {
|
|
return nil, lazyerrors.Error(err)
|
|
}
|
|
|
|
res.v = value
|
|
|
|
// case "e":
|
|
|
|
default:
|
|
return nil, lazyerrors.Errorf("unsupported SCRAM attribute: %q", field)
|
|
}
|
|
}
|
|
|
|
return &res, nil
|
|
}
|