arblant
firefox-vpn-client / core / internal / service / localtoken.go
localtoken.go61 lines · 1.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
package service

import (
	"crypto/rand"
	"crypto/subtle"
	"encoding/hex"
	"errors"
	"net/http"
	"os"
	"strings"

	"github.com/armanlife/firefox-vpn-client/core/internal/securefile"
)

// localTokenHeader carries the local API token. Local clients that can read
// the data folder (the tray) use it instead of the dashboard login; anyone
// who can read that folder can already read the account credentials.
const localTokenHeader = "X-Local-Token"

// newLocalToken returns a fresh random local API token.
func newLocalToken() (string, error) {
	var b [32]byte
	if _, err := rand.Read(b[:]); err != nil {
		return "", err
	}
	return hex.EncodeToString(b[:]), nil
}

func readLocalToken(path string) (string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return "", err
	}
	token := strings.TrimSpace(string(data))
	if token == "" {
		return "", errors.New("empty local API token")
	}
	return token, nil
}

// hasLocalToken reports whether r carries the daemon's local token.
func (d *Daemon) hasLocalToken(r *http.Request) bool {
	got := r.Header.Get(localTokenHeader)
	return got != "" && d.localToken != "" && subtle.ConstantTimeCompare([]byte(got), []byte(d.localToken)) == 1
}

// loadSessionKey reads the key that signs dashboard sessions, creating a
// random one the first time; replacing the file signs everyone out.
func loadSessionKey(path string) ([]byte, error) {
	if data, err := os.ReadFile(path); err == nil && len(data) == 32 {
		return data, nil
	}
	key := make([]byte, 32)
	if _, err := rand.Read(key); err != nil {
		return nil, err
	}
	if err := securefile.WritePrivate(path, key); err != nil {
		return nil, err
	}
	return key, nil
}