package store import ( "database/sql" "encoding/base64" "errors" "fmt" "git.dcglab.co.uk/steve/emcli/internal/crypto" ) // Role selects which DEK wrap slot a command may unlock. type Role int const ( RoleAgent Role = iota // agent commands; uses dek_wrap_agent (admin slot as fallback) RoleAdmin // all commands; uses dek_wrap_admin ONLY ) const ( settingDEKWrapAdmin = "dek_wrap_admin" settingDEKWrapAgent = "dek_wrap_agent" ) // ErrLocked means the DB has no DEK wrap slots yet (never initialized). var ErrLocked = errors.New("emcli DB is not initialized; run `emcli init`") // dbPath returns the file path SQLite opened (used by tests to re-open). func (s *Store) dbPath() string { var p string _ = s.db.QueryRow("PRAGMA database_list").Scan(new(int), new(string), &p) return p } // InitKeys generates a DEK (only if absent), seals it under both KEKs, writes // both wrap slots, and unlocks the store. If the slots already exist it does // NOT regenerate the DEK — it unlocks via the admin slot (idempotent re-init). func (s *Store) InitKeys(adminKey, agentKey []byte) error { if _, err := s.GetSetting(settingDEKWrapAdmin); err == nil { // Already initialised: the DEK and both wrap slots already exist, so the // agent key is not consumed here. Only the admin key is used to unlock the // existing dek_wrap_admin slot; the DEK itself is preserved unchanged. return s.Unlock(RoleAdmin, adminKey, nil) } dek, err := crypto.NewDEK() if err != nil { return err } wrapAdmin, err := crypto.Seal(adminKey, dek) if err != nil { return err } wrapAgent, err := crypto.Seal(agentKey, dek) if err != nil { return err } if err := s.SetSetting(settingDEKWrapAdmin, base64.StdEncoding.EncodeToString(wrapAdmin)); err != nil { return err } if err := s.SetSetting(settingDEKWrapAgent, base64.StdEncoding.EncodeToString(wrapAgent)); err != nil { return err } s.key = dek return nil } // Unlock loads the DEK into the store by decrypting the wrap slot for role. // RoleAdmin uses the admin slot ONLY. RoleAgent prefers the agent slot and // falls back to the admin slot only when no agent key is supplied. func (s *Store) Unlock(role Role, adminKey, agentKey []byte) error { switch role { case RoleAdmin: return s.unlockSlot(settingDEKWrapAdmin, adminKey) case RoleAgent: if len(agentKey) > 0 { return s.unlockSlot(settingDEKWrapAgent, agentKey) } return s.unlockSlot(settingDEKWrapAdmin, adminKey) default: return fmt.Errorf("unknown role %d", role) } } func (s *Store) unlockSlot(settingKey string, kek []byte) error { if len(kek) == 0 { return ErrLocked } enc, err := s.GetSetting(settingKey) if errors.Is(err, sql.ErrNoRows) { return ErrLocked } if err != nil { return err } blob, err := base64.StdEncoding.DecodeString(enc) if err != nil { return fmt.Errorf("corrupt wrap slot %q: %w", settingKey, err) } dek, err := crypto.Open(kek, blob) if err != nil { return errors.New("wrong key for this DB") } s.key = dek return nil }