| 1 | package docs |
| 2 | |
| 3 | import ( |
| 4 | "encoding/base64" |
| 5 | "errors" |
| 6 | "net" |
| 7 | "net/url" |
| 8 | "strings" |
| 9 | |
| 10 | "amadan.net/oficina/home/handoff/seal" |
| 11 | "amadan.net/rastrillo/rastrillo/assertion" |
| 12 | json "github.com/go-json-experiment/json" |
| 13 | ) |
| 14 | |
| 15 | // HomeConfig 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 | type HomeConfig struct { |
| 19 | Issuer string |
| 20 | ConsumerID string |
| 21 | KeysBase64 string |
| 22 | DEKBase64 string |
| 23 | } |
| 24 | |
| 25 | type homeSettings struct { |
| 26 | issuer string |
| 27 | consumerID string |
| 28 | keys map[string][]byte |
| 29 | dek []byte |
| 30 | } |
| 31 | |
| 32 | var errHomeConfig = errors.New("docs: invalid Home configuration") |
| 33 | |
| 34 | // A nil config preserves standalone authentication; an incomplete config |
| 35 | // cannot silently fall back to it. Errors never include supplied values. |
| 36 | func validatedHomeConfig(cfg *HomeConfig, origin string) (*homeSettings, error) { |
| 37 | if cfg == nil { |
| 38 | return nil, nil |
| 39 | } |
| 40 | if !homeConsumerID(cfg.ConsumerID) { |
| 41 | return nil, errHomeConfig |
| 42 | } |
| 43 | dek, ok := homeConfigBase64(cfg.DEKBase64, 32) |
| 44 | if !ok { |
| 45 | return nil, errHomeConfig |
| 46 | } |
| 47 | if _, err := seal.New(dek, "oficina/docs"); err != nil { |
| 48 | return nil, errHomeConfig |
| 49 | } |
| 50 | manifest, ok := homeConfigBase64(cfg.KeysBase64, 4096) |
| 51 | if !ok { |
| 52 | return nil, errHomeConfig |
| 53 | } |
| 54 | var encoded map[string]string |
| 55 | // This decoder rejects duplicate names, invalid UTF-8 and trailing values. |
| 56 | // A map of strings also rejects wrapper objects, arrays and nested key data. |
| 57 | if json.Unmarshal(manifest, &encoded) != nil || len(encoded) == 0 || len(encoded) > 8 { |
| 58 | return nil, errHomeConfig |
| 59 | } |
| 60 | keys := make(map[string][]byte, len(encoded)) |
| 61 | for id, encodedKey := range encoded { |
| 62 | key, ok := homeConfigBase64(encodedKey, 65) |
| 63 | if !ok { |
| 64 | return nil, errHomeConfig |
| 65 | } |
| 66 | keys[id] = key |
| 67 | } |
| 68 | if _, err := assertion.NewVerifier(cfg.Issuer, origin, keys); err != nil { |
| 69 | return nil, errHomeConfig |
| 70 | } |
| 71 | return &homeSettings{issuer: cfg.Issuer, consumerID: cfg.ConsumerID, keys: keys, dek: dek}, nil |
| 72 | } |
| 73 | |
| 74 | func homeConsumerID(v string) bool { |
| 75 | if len(v) == 0 || len(v) > 64 { |
| 76 | return false |
| 77 | } |
| 78 | for _, b := range []byte(v) { |
| 79 | if !(b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z' || b >= '0' && b <= '9' || b == '_' || b == '-') { |
| 80 | return false |
| 81 | } |
| 82 | } |
| 83 | return true |
| 84 | } |
| 85 | |
| 86 | func homeConfigBase64(encoded string, limit int) ([]byte, bool) { |
| 87 | if encoded == "" || len(encoded) > base64.StdEncoding.EncodedLen(limit) { |
| 88 | return nil, false |
| 89 | } |
| 90 | decoded, err := base64.StdEncoding.Strict().DecodeString(encoded) |
| 91 | if err != nil || len(decoded) > limit || base64.StdEncoding.EncodeToString(decoded) != encoded { |
| 92 | return nil, false |
| 93 | } |
| 94 | return decoded, true |
| 95 | } |
| 96 | |
| 97 | var errDirectoryConfig = errors.New("docs: invalid $HOME_DIRECTORY_ORIGIN") |
| 98 | |
| 99 | // validatedDirectory checks the hub directory origin the app switcher |
| 100 | // links to, by Home's own originHost rules (home internal/home/origins.go): |
| 101 | // https, a lower-case DNS name of at least two labels, no IP address, no |
| 102 | // port, userinfo, path, query or fragment. Empty is allowed and means no |
| 103 | // switcher. Docs reads it itself because it is not on home/connect, where |
| 104 | // it would be connect.Credentials.Directory (spec §6.2); that move is R3. |
| 105 | func validatedDirectory(origin string) (string, error) { |
| 106 | if origin == "" { |
| 107 | return "", nil |
| 108 | } |
| 109 | u, err := url.Parse(origin) |
| 110 | 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 { |
| 111 | return "", errDirectoryConfig |
| 112 | } |
| 113 | labels := strings.Split(u.Host, ".") |
| 114 | if len(labels) < 2 { |
| 115 | return "", errDirectoryConfig |
| 116 | } |
| 117 | for _, label := range labels { |
| 118 | if len(label) == 0 || len(label) > 63 || label[0] == '-' || label[len(label)-1] == '-' { |
| 119 | return "", errDirectoryConfig |
| 120 | } |
| 121 | for _, c := range label { |
| 122 | if !(c >= 'a' && c <= 'z' || c >= '0' && c <= '9' || c == '-') { |
| 123 | return "", errDirectoryConfig |
| 124 | } |
| 125 | } |
| 126 | } |
| 127 | return origin, nil |
| 128 | } |
| 129 | |