package connect import ( "encoding/base64" "errors" "net" "net/url" "os" "strings" "amadan.net/rastrillo/rastrillo/assertion" json "github.com/go-json-experiment/json" ) // Credentials explicitly enables Home authentication. The public key manifest // is StdBase64-encoded JSON mapping key IDs to StdBase64 P-256 public keys. // DEKBase64 is a dedicated handoff/storage key, never the document content key. // Directory is the optional hub directory origin (HOME_DIRECTORY_ORIGIN), which // an app's switcher and "All apps" link to. It enables nothing on its own, and // it is configured rather than derived from Issuer: stripping "id." from the // credential origin is not a derivation anyone has promised. type Credentials struct { Issuer string ConsumerID string KeysBase64 string DEKBase64 string Directory string } type DecodedCredentials struct { Issuer string ConsumerID string Keys map[string][]byte DEK []byte Directory string } var ErrConfig = errors.New("home connect: invalid configuration") // A nil config preserves standalone authentication; an incomplete config // cannot silently fall back to it. Errors never include supplied values. func DecodeCredentials(cfg *Credentials, origin string) (*DecodedCredentials, error) { if cfg == nil { return nil, nil } if !homeConsumerID(cfg.ConsumerID) { return nil, ErrConfig } dek, ok := homeConfigBase64(cfg.DEKBase64, 32) if !ok { return nil, ErrConfig } if len(dek) != 32 { return nil, ErrConfig } manifest, ok := homeConfigBase64(cfg.KeysBase64, 4096) if !ok { return nil, ErrConfig } var encoded map[string]string // This decoder rejects duplicate names, invalid UTF-8 and trailing values. // A map of strings also rejects wrapper objects, arrays and nested key data. if json.Unmarshal(manifest, &encoded) != nil || len(encoded) == 0 || len(encoded) > 8 { return nil, ErrConfig } keys := make(map[string][]byte, len(encoded)) for id, encodedKey := range encoded { key, ok := homeConfigBase64(encodedKey, 65) if !ok { return nil, ErrConfig } keys[id] = key } if _, err := assertion.NewVerifier(cfg.Issuer, origin, keys); err != nil { return nil, ErrConfig } if cfg.Directory != "" && (!bareOrigin(cfg.Directory) || cfg.Directory == cfg.Issuer) { return nil, ErrConfig } return &DecodedCredentials{Issuer: cfg.Issuer, ConsumerID: cfg.ConsumerID, Keys: keys, DEK: dek, Directory: cfg.Directory}, nil } // bareOrigin is Home's originHost rule (internal/home/origins.go), which // this module cannot import: https, a lower-case DNS host of at least two // labels, no IP literal, no port, no userinfo, path, query or fragment. func bareOrigin(origin string) bool { u, err := url.Parse(origin) if err != nil || u.Scheme != "https" || u.User != nil || u.Host == "" || origin != "https://"+u.Host || u.Host != strings.ToLower(u.Host) || net.ParseIP(u.Host) != nil || len(u.Host) > 253 { return false } labels := strings.Split(u.Host, ".") if len(labels) < 2 { return false } for _, label := range labels { if len(label) == 0 || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' { return false } for _, c := range label { if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-') { return false } } } return true } func homeConsumerID(v string) bool { if len(v) == 0 || len(v) > 64 { return false } for _, b := range []byte(v) { if !(b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '_' || b == '-') { return false } } return true } func homeConfigBase64(encoded string, limit int) ([]byte, bool) { if encoded == "" || len(encoded) > base64.StdEncoding.EncodedLen(limit) { return nil, false } decoded, err := base64.StdEncoding.Strict().DecodeString(encoded) if err != nil || len(decoded) > limit || base64.StdEncoding.EncodeToString(decoded) != encoded { return nil, false } return decoded, true } // LoadEnv returns nil only when every setting is absent or empty. A partial // configuration refuses boot rather than falling back to local sign-in. func LoadEnv(prefix, origin string) (*DecodedCredentials, error) { return DecodeCredentials(CredentialsFromEnv(prefix), origin) } // The directory origin is read from HOME_DIRECTORY_ORIGIN, unprefixed: it is // the same name, and the same value, Home itself reads. It is optional, so it // does not count toward "every setting is absent". func CredentialsFromEnv(prefix string) *Credentials { c := Credentials{Issuer: os.Getenv(prefix + "_HOME_ISSUER"), ConsumerID: os.Getenv(prefix + "_HOME_CONSUMER_ID"), KeysBase64: os.Getenv(prefix + "_HOME_KEYS_BASE64"), DEKBase64: os.Getenv(prefix + "_HOME_DEK_BASE64")} if c.Issuer == "" && c.ConsumerID == "" && c.KeysBase64 == "" && c.DEKBase64 == "" { return nil } c.Directory = os.Getenv("HOME_DIRECTORY_ORIGIN") return &c }