package server import ( "errors" "fmt" "net/http" "strings" "time" "cc.tiredsysadmin.keeporg/internal/blob" "cc.tiredsysadmin.keeporg/internal/kdbx" "cc.tiredsysadmin.keeporg/internal/secret" "cc.tiredsysadmin.keeporg/internal/store" ) // MinVaultPasswordLen is the shortest vault password the server will create // a vault with. It guards an organization's whole password database. const MinVaultPasswordLen = 12 var errNoKeyring = errors.New( "this server has no master key, so it cannot hold vault keys: set " + secret.MasterKeyEnv + " or master_key_file and restart") // createVault makes a new, empty KDBX database, encrypts it with the given // vault password and stores it as version 1. When the strategy needs the // key, the password is wrapped under the server's master key and kept; for // every other strategy it is used once and forgotten. func (s *Server) createVault(u *store.User, name, groupID string, strategy store.Strategy, password string) (dbView, error) { name = strings.TrimSpace(name) switch { case name == "": return dbView{}, errors.New("the vault needs a name") case len(password) < MinVaultPasswordLen: return dbView{}, fmt.Errorf("the vault password must be at least %d characters", MinVaultPasswordLen) case !strategy.Valid(): return dbView{}, errors.New("unknown merge strategy") case strategy.NeedsKey() && s.keyring == nil: return dbView{}, errNoKeyring } // Build the empty database before touching any state, so a failure here // leaves nothing behind. db := kdbx.New(name, s.cfg.VaultKDF) data, err := db.Marshal(password) if err != nil { return dbView{}, fmt.Errorf("creating the vault file: %w", err) } id := secret.ID() wrapped := "" if strategy.NeedsKey() { if wrapped, err = s.keyring.Wrap(id, password); err != nil { return dbView{}, err } } if err := s.blobs.Put(id, 1, data); err != nil { return dbView{}, err } var out dbView err = s.st.Update(func(st *store.State) error { if st.Groups[groupID] == nil { return errors.New("unknown owner group") } if !st.IsGroupAdmin(u.ID, groupID) { return errForbidden } rec := &store.Database{ ID: id, Name: name, OwnerGroupID: groupID, MergeStrategy: strategy, WrappedKey: wrapped, Version: 1, SHA256: blob.SHA256(data), Size: int64(len(data)), UpdatedAt: time.Now(), UpdatedBy: u.Username, } st.Databases[id] = rec out = toView(rec, store.PermAdmin) return nil }) if err != nil { if rerr := s.blobs.Forget(id); rerr != nil { s.log.Printf("rolling back vault %s: %v", id, rerr) } return dbView{}, err } s.notifyList(id) s.log.Printf("%s created vault %q (%s), strategy %s", u.Username, name, id, strategy) return out, nil } // handleCreateVault is the API behind the web form: it creates the database // record and its first, empty KDBX version in one step. func (s *Server) handleCreateVault(w http.ResponseWriter, r *http.Request, u *store.User) { var req struct { Name string `json:"name"` OwnerGroupID string `json:"owner_group_id"` MergeStrategy store.Strategy `json:"merge_strategy"` Password string `json:"password"` } if !readJSON(w, r, &req) { return } if req.MergeStrategy == "" { req.MergeStrategy = store.StrategyMerge } out, err := s.createVault(u, req.Name, req.OwnerGroupID, req.MergeStrategy, req.Password) if err != nil { deny(w, err) return } writeJSON(w, http.StatusCreated, out) } // handleSetVaultKey stores, replaces or removes the key the server keeps for // a database, which is what lets an existing database start being merged. // The password is checked against the stored file before it is accepted. func (s *Server) handleSetVaultKey(w http.ResponseWriter, r *http.Request, u *store.User) { id := r.PathValue("id") var req struct { Password string `json:"password"` Forget bool `json:"forget"` } if !readJSON(w, r, &req) { return } var ( version int64 perm store.Perm ) s.st.Read(func(st *store.State) { perm = st.DatabasePerm(u.ID, id) if d := st.Databases[id]; d != nil { version = d.Version } }) if perm != store.PermAdmin { deny(w, errForbidden) return } if !req.Forget { if s.keyring == nil { deny(w, errNoKeyring) return } if version == 0 { deny(w, errors.New("this database has no content yet, so the password cannot be checked")) return } data, err := s.blobs.Get(id, version) if err != nil { deny(w, err) return } if _, err := kdbx.Open(data, req.Password); err != nil { deny(w, fmt.Errorf("that password does not open the stored vault: %w", err)) return } } wrapped := "" if !req.Forget { var err error if wrapped, err = s.keyring.Wrap(id, req.Password); err != nil { deny(w, err) return } } err := s.st.Update(func(st *store.State) error { d := st.Databases[id] if d == nil { return errors.New("no such database") } if req.Forget && d.MergeStrategy == store.StrategyMerge { return errors.New("switch the merge strategy away from merge before removing the key") } d.WrappedKey = wrapped return nil }) if err != nil { deny(w, err) return } writeJSON(w, http.StatusOK, map[string]bool{"key_stored": wrapped != ""}) } // vaultPassword unwraps the stored key for a database. func (s *Server) vaultPassword(id, wrapped string) (string, error) { if wrapped == "" { return "", errors.New("no vault key is stored for this database") } if s.keyring == nil { return "", errNoKeyring } return s.keyring.Unwrap(id, wrapped) }