smcli setfield: surgical single-field rotation updates; env exposes login triple
ci / vet (push) Successful in 1m2s

Rotation waves need per-field updates on login items without touching
the rest (password/username/custom fields), and consumers need URI/
USERNAME/PASSWORD surfaced by env.
Ticket: https://projects.knownelement.com/issues/829
This commit is contained in:
2026-09-06 18:52:34 -05:00
parent 907ddb6000
commit 68ba03c1af
3 changed files with 101 additions and 1 deletions
+100
View File
@@ -424,6 +424,11 @@ func main() {
for _, f := range sync.Folders {
fmt.Println(f.ID, f.Name)
}
case "setfield":
if len(os.Args) < 5 {
fatal("setfield <name> <key> <value>")
}
err = cmdSetField(os.Args[2], os.Args[3], os.Args[4])
case "convert":
if len(os.Args) < 3 {
fatal("convert <name>")
@@ -548,3 +553,98 @@ func cmdConvert(name string) error {
}
func urisOf(pc *PlainCipher) []string { return pc.URIs }
// cmdSetField surgically updates one key on an existing item: login.password,
// login.username, an existing custom field, or appends a new hidden field.
func cmdSetField(name, key, value string) error {
s, err := loadState()
if err != nil {
return err
}
c, err := newClientFromState(s)
if err != nil {
return err
}
raw, err := c.Sync()
if err != nil {
return err
}
var sync struct {
Ciphers []json.RawMessage `json:"ciphers"`
}
if err := json.Unmarshal(raw, &sync); err != nil {
return err
}
var cr *cipherRaw
for _, r := range sync.Ciphers {
var x cipherRaw
if json.Unmarshal(r, &x) == nil && x.ID != "" {
pc2, derr := DecryptCipher(c.UserSymKey, r)
if derr == nil && pc2.Name == name {
cr = &x
break
}
}
}
if cr == nil {
return fmt.Errorf("item not found: %s", name)
}
ck, err := cipherKeyFor(c.UserSymKey, cr.Key)
if err != nil {
return err
}
switch strings.ToLower(key) {
case "password":
p, err := encStr(c.UserSymKey, value)
if err != nil {
return err
}
if cr.Login == nil {
cr.Login = &CipherLogin{}
}
cr.Login.Password = &p
case "username":
p, err := encStr(c.UserSymKey, value)
if err != nil {
return err
}
if cr.Login == nil {
cr.Login = &CipherLogin{}
}
cr.Login.Username = &p
default:
found := false
for i := range cr.Fields {
fn, derr := decStr(ck, ck, cr.Fields[i].Name)
if derr == nil && fn == key {
fv, err := encStr(c.UserSymKey, value)
if err != nil {
return err
}
cr.Fields[i].Value = &fv
found = true
break
}
}
if !found {
fn, _ := encStr(c.UserSymKey, key)
fv, _ := encStr(c.UserSymKey, value)
cr.Fields = append(cr.Fields, CipherField{Type: 1, Name: &fn, Value: &fv})
}
}
payload := map[string]any{"type": cr.Type, "name": cr.Name}
if cr.Login != nil {
payload["login"] = cr.Login
}
if cr.Fields != nil {
payload["fields"] = cr.Fields
}
if cr.Notes != nil {
payload["notes"] = *cr.Notes
}
if _, err := c.EditCipher(cr.ID, payload); err != nil {
return err
}
fmt.Println("updated:", name, key)
return nil
}