oficina / home Public

Clone
git clone https://amadan.net/oficina/home

Plain git — no account needed to clone.

Download

Download this file

1package connect
2
3import (
4 "encoding/base64"
5 "errors"
6 "net"
7 "net/url"
8 "os"
9 "strings"
10
11 "amadan.net/rastrillo/rastrillo/assertion"
12 json "github.com/go-json-experiment/json"
13)
14
15// Credentials explicitly enables Home authentication. The public key manifest
16// is StdBase64-encoded JSON mapping key IDs to StdBase64 P-256 public keys.
17// DEKBase64 is a dedicated handoff/storage key, never the document content key.
18// Directory is the optional hub directory origin (HOME_DIRECTORY_ORIGIN), which
19// an app's switcher and "All apps" link to. It enables nothing on its own, and
20// it is configured rather than derived from Issuer: stripping "id." from the
21// credential origin is not a derivation anyone has promised.
22type Credentials struct {
23 Issuer string
24 ConsumerID string
25 KeysBase64 string
26 DEKBase64 string
27 Directory string
28}
29
30type DecodedCredentials struct {
31 Issuer string
32 ConsumerID string
33 Keys map[string][]byte
34 DEK []byte
35 Directory string
36}
37
38var ErrConfig = errors.New("home connect: invalid configuration")
39
40// A nil config preserves standalone authentication; an incomplete config
41// cannot silently fall back to it. Errors never include supplied values.
42func DecodeCredentials(cfg *Credentials, origin string) (*DecodedCredentials, error) {
43 if cfg == nil {
44 return nil, nil
45 }
46 if !homeConsumerID(cfg.ConsumerID) {
47 return nil, ErrConfig
48 }
49 dek, ok := homeConfigBase64(cfg.DEKBase64, 32)
50 if !ok {
51 return nil, ErrConfig
52 }
53 if len(dek) != 32 {
54 return nil, ErrConfig
55 }
56 manifest, ok := homeConfigBase64(cfg.KeysBase64, 4096)
57 if !ok {
58 return nil, ErrConfig
59 }
60 var encoded map[string]string
61 // This decoder rejects duplicate names, invalid UTF-8 and trailing values.
62 // A map of strings also rejects wrapper objects, arrays and nested key data.
63 if json.Unmarshal(manifest, &encoded) != nil || len(encoded) == 0 || len(encoded) > 8 {
64 return nil, ErrConfig
65 }
66 keys := make(map[string][]byte, len(encoded))
67 for id, encodedKey := range encoded {
68 key, ok := homeConfigBase64(encodedKey, 65)
69 if !ok {
70 return nil, ErrConfig
71 }
72 keys[id] = key
73 }
74 if _, err := assertion.NewVerifier(cfg.Issuer, origin, keys); err != nil {
75 return nil, ErrConfig
76 }
77 if cfg.Directory != "" && (!bareOrigin(cfg.Directory) || cfg.Directory == cfg.Issuer) {
78 return nil, ErrConfig
79 }
80 return &DecodedCredentials{Issuer: cfg.Issuer, ConsumerID: cfg.ConsumerID, Keys: keys, DEK: dek, Directory: cfg.Directory}, nil
81}
82
83// bareOrigin is Home's originHost rule (internal/home/origins.go), which
84// this module cannot import: https, a lower-case DNS host of at least two
85// labels, no IP literal, no port, no userinfo, path, query or fragment.
86func bareOrigin(origin string) bool {
87 u, err := url.Parse(origin)
88 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 {
89 return false
90 }
91 labels := strings.Split(u.Host, ".")
92 if len(labels) < 2 {
93 return false
94 }
95 for _, label := range labels {
96 if len(label) == 0 || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' {
97 return false
98 }
99 for _, c := range label {
100 if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-') {
101 return false
102 }
103 }
104 }
105 return true
106}
107
108func homeConsumerID(v string) bool {
109 if len(v) == 0 || len(v) > 64 {
110 return false
111 }
112 for _, b := range []byte(v) {
113 if !(b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '_' || b == '-') {
114 return false
115 }
116 }
117 return true
118}
119
120func homeConfigBase64(encoded string, limit int) ([]byte, bool) {
121 if encoded == "" || len(encoded) > base64.StdEncoding.EncodedLen(limit) {
122 return nil, false
123 }
124 decoded, err := base64.StdEncoding.Strict().DecodeString(encoded)
125 if err != nil || len(decoded) > limit || base64.StdEncoding.EncodeToString(decoded) != encoded {
126 return nil, false
127 }
128 return decoded, true
129}
130
131// LoadEnv returns nil only when every setting is absent or empty. A partial
132// configuration refuses boot rather than falling back to local sign-in.
133func LoadEnv(prefix, origin string) (*DecodedCredentials, error) {
134 return DecodeCredentials(CredentialsFromEnv(prefix), origin)
135}
136
137// The directory origin is read from HOME_DIRECTORY_ORIGIN, unprefixed: it is
138// the same name, and the same value, Home itself reads. It is optional, so it
139// does not count toward "every setting is absent".
140func CredentialsFromEnv(prefix string) *Credentials {
141 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")}
142 if c.Issuer == "" && c.ConsumerID == "" && c.KeysBase64 == "" && c.DEKBase64 == "" {
143 return nil
144 }
145 c.Directory = os.Getenv("HOME_DIRECTORY_ORIGIN")
146 return &c
147}
148