160 lines
4.4 KiB
Go
160 lines
4.4 KiB
Go
package subsonic
|
|
|
|
import (
|
|
"crypto/md5"
|
|
"database/sql"
|
|
"encoding/hex"
|
|
"strconv"
|
|
"strings"
|
|
|
|
vsauth "git.noroutes.net/NoRoutes/vibetunes/internal/auth"
|
|
"git.noroutes.net/NoRoutes/vibetunes/internal/database"
|
|
)
|
|
|
|
// authenticate implements original Subsonic authentication: either
|
|
// token/salt (t = md5(password + salt), the modern recommended mode) or
|
|
// password (p=<plaintext> or p=enc:<hex>, kept for older clients).
|
|
//
|
|
// This never touches VibeSonic's Argon2id-hashed password directly for
|
|
// token auth — that hash is one-way by design. Token auth is instead
|
|
// verified against a separately maintained, AES-GCM-encrypted copy of
|
|
// the password (see internal/auth/subsonic_secret.go); the /rest/*
|
|
// surface never reintroduces MD5 or plaintext as VibeSonic's own storage
|
|
// format, it only speaks it on the wire where the protocol requires it.
|
|
func authenticate(db *sql.DB, encryptionKey []byte, p params) (*database.User, *apiError) {
|
|
apiKey := p.Get("apiKey")
|
|
username := p.Get("u")
|
|
|
|
if apiKey != "" {
|
|
// The apiKeyAuthentication extension requires a client not send
|
|
// 'u' alongside 'apiKey' — accepting both would leave it
|
|
// ambiguous which identity the caller actually intended, so this
|
|
// is rejected outright rather than silently preferring one.
|
|
if username != "" {
|
|
return nil, errConflictingAuth
|
|
}
|
|
return authenticateAPIKey(db, apiKey)
|
|
}
|
|
|
|
if username == "" {
|
|
return nil, errMissingParameter("u")
|
|
}
|
|
|
|
user, err := database.GetUserByUsername(db, username)
|
|
if err != nil || user.Disabled {
|
|
return nil, errWrongCredentials
|
|
}
|
|
|
|
token := p.Get("t")
|
|
salt := p.Get("s")
|
|
password := p.Get("p")
|
|
|
|
switch {
|
|
case token != "" && salt != "":
|
|
if !user.SubsonicAuthSecret.Valid {
|
|
return nil, errTokenAuthNotSupported
|
|
}
|
|
plaintext, derr := vsauth.DecryptPassword(encryptionKey, user.SubsonicAuthSecret.String)
|
|
if derr != nil {
|
|
return nil, errWrongCredentials
|
|
}
|
|
sum := md5.Sum([]byte(plaintext + salt))
|
|
expected := hex.EncodeToString(sum[:])
|
|
if !strings.EqualFold(expected, token) {
|
|
return nil, errWrongCredentials
|
|
}
|
|
|
|
case password != "":
|
|
plaintext := password
|
|
if strings.HasPrefix(password, "enc:") {
|
|
decoded, herr := hex.DecodeString(strings.TrimPrefix(password, "enc:"))
|
|
if herr != nil {
|
|
return nil, errWrongCredentials
|
|
}
|
|
plaintext = string(decoded)
|
|
}
|
|
if !user.PasswordHash.Valid {
|
|
return nil, errWrongCredentials
|
|
}
|
|
ok, verr := vsauth.VerifyPassword(plaintext, user.PasswordHash.String)
|
|
if verr != nil || !ok {
|
|
return nil, errWrongCredentials
|
|
}
|
|
|
|
default:
|
|
return nil, errMissingParameter("p or t/s")
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
// authenticateAPIKey implements the OpenSubsonic apiKeyAuthentication
|
|
// extension: the key is hashed the same way as VibeSonic's own web
|
|
// settings API keys (they are exactly the same keys — one system, not a
|
|
// parallel one) and looked up by hash, never by scanning stored keys in
|
|
// plaintext.
|
|
func authenticateAPIKey(db *sql.DB, rawKey string) (*database.User, *apiError) {
|
|
key, err := database.GetAPIKeyByHash(db, vsauth.HashAPIKey(rawKey))
|
|
if err != nil {
|
|
return nil, errInvalidAPIKey
|
|
}
|
|
user, err := database.GetUserByID(db, key.UserID)
|
|
if err != nil || user.Disabled {
|
|
return nil, errInvalidAPIKey
|
|
}
|
|
_ = database.TouchAPIKey(db, key.ID)
|
|
return user, nil
|
|
}
|
|
|
|
// checkVersion rejects clients whose declared protocol version is newer
|
|
// than what this server implements. A missing or unparsable v is
|
|
// tolerated (treated as compatible) rather than rejected outright, to
|
|
// stay lenient with tooling that omits it — the protocol is additive, so
|
|
// there is no reason to reject an old client either.
|
|
func checkVersion(v string) *apiError {
|
|
if v == "" {
|
|
return nil
|
|
}
|
|
client, ok := parseVersion(v)
|
|
if !ok {
|
|
return nil
|
|
}
|
|
server, _ := parseVersion(ServerVersion)
|
|
if versionGreater(client, server) {
|
|
return errClientMustUpgrade
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type semver struct{ major, minor, patch int }
|
|
|
|
func parseVersion(v string) (semver, bool) {
|
|
parts := strings.SplitN(v, ".", 3)
|
|
if len(parts) == 0 {
|
|
return semver{}, false
|
|
}
|
|
var s semver
|
|
var err error
|
|
s.major, err = strconv.Atoi(parts[0])
|
|
if err != nil {
|
|
return semver{}, false
|
|
}
|
|
if len(parts) > 1 {
|
|
s.minor, _ = strconv.Atoi(parts[1])
|
|
}
|
|
if len(parts) > 2 {
|
|
s.patch, _ = strconv.Atoi(parts[2])
|
|
}
|
|
return s, true
|
|
}
|
|
|
|
func versionGreater(a, b semver) bool {
|
|
if a.major != b.major {
|
|
return a.major > b.major
|
|
}
|
|
if a.minor != b.minor {
|
|
return a.minor > b.minor
|
|
}
|
|
return a.patch > b.patch
|
|
}
|