smcli setfield: surgical single-field rotation updates; env exposes login triple
ci / vet (push) Successful in 1m2s
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:
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user