package main import ( "crypto/pbkdf2" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "errors" "fmt" "strconv" "strings" "sync" "time" ) // Password hashes are stored as "pbkdf2-sha256$$$" // (base64, no padding) in a file only root can read. const ( pbkdf2Iterations = 600_000 pbkdf2KeyLen = 32 ) var b64 = base64.RawStdEncoding func hashPassword(password string) (string, error) { salt := make([]byte, 16) if _, err := rand.Read(salt); err != nil { return "", err } key, err := pbkdf2.Key(sha256.New, password, salt, pbkdf2Iterations, pbkdf2KeyLen) if err != nil { return "", err } return fmt.Sprintf("pbkdf2-sha256$%d$%s$%s", pbkdf2Iterations, b64.EncodeToString(salt), b64.EncodeToString(key)), nil } var errBadHash = errors.New("unrecognised password hash") func checkPassword(stored, password string) (bool, error) { parts := strings.Split(strings.TrimSpace(stored), "$") if len(parts) != 4 || parts[0] != "pbkdf2-sha256" { return false, errBadHash } iter, err := strconv.Atoi(parts[1]) if err != nil || iter < 1 { return false, errBadHash } salt, err1 := b64.DecodeString(parts[2]) want, err2 := b64.DecodeString(parts[3]) if err1 != nil || err2 != nil || len(want) == 0 { return false, errBadHash } got, err := pbkdf2.Key(sha256.New, password, salt, iter, len(want)) if err != nil { return false, err } return subtle.ConstantTimeCompare(got, want) == 1, nil } func randomToken() string { b := make([]byte, 32) if _, err := rand.Read(b); err != nil { panic(err) } return base64.RawURLEncoding.EncodeToString(b) } // Sessions live in memory: a restart of the UI (e.g. by a switch) logs // everyone out, which is acceptable for a single-admin box. type session struct { csrf string expires time.Time } type sessions struct { mu sync.Mutex ttl time.Duration m map[string]session } func newSessions(ttl time.Duration) *sessions { return &sessions{ttl: ttl, m: map[string]session{}} } func (s *sessions) create() (id string, sess session) { s.mu.Lock() defer s.mu.Unlock() id = randomToken() sess = session{csrf: randomToken(), expires: time.Now().Add(s.ttl)} s.m[id] = sess return id, sess } // get returns the session and extends it. func (s *sessions) get(id string) (session, bool) { s.mu.Lock() defer s.mu.Unlock() sess, ok := s.m[id] if !ok || time.Now().After(sess.expires) { delete(s.m, id) return session{}, false } sess.expires = time.Now().Add(s.ttl) s.m[id] = sess return sess, true } func (s *sessions) delete(id string) { s.mu.Lock() defer s.mu.Unlock() delete(s.m, id) }