package main // Cipher model: decrypt/encrypt for the operations the lane uses // (secure notes with hidden fields; login items for password fields). import ( "encoding/json" "fmt" "strings" ) // encStr handles empty/unset gracefully. type encFunc func(string) (string, error) type CipherField struct { Type int `json:"type"` // 0=text, 1=hidden, 2=boolean (Bitwarden field type) Name *string `json:"name,omitempty"` Value *string `json:"value,omitempty"` } type CipherLogin struct { Username *string `json:"username,omitempty"` Password *string `json:"password,omitempty"` URIs []any `json:"uris,omitempty"` } type cipherRaw struct { ID string `json:"id"` OrganizationID *string `json:"organizationId"` Type int `json:"type"` // 1=login, 2=secureNote Name string `json:"name"` Notes *string `json:"notes"` Fields []CipherField `json:"fields,omitempty"` Key *string `json:"key,omitempty"` Login *CipherLogin `json:"login,omitempty"` SecureNote map[string]any `json:"secureNote,omitempty"` DeletedDate *string `json:"deletedDate,omitempty"` Extra map[string]interface{} `json:"-"` } // cipherKeyFor returns the 64B key to use for a cipher's data. func cipherKeyFor(userKey []byte, encKey *string) ([]byte, error) { if encKey == nil || *encKey == "" { return userKey, nil } es, err := ParseEncString(*encKey) if err != nil { return nil, err } return symDecrypt(userKey, es) } func decStr(userKey []byte, cipherKey []byte, s *string) (string, error) { if s == nil || *s == "" { return "", nil } es, err := ParseEncString(*s) if err != nil { return "", err } pt, err := symDecrypt(cipherKey, es) if err != nil { return "", err } return string(pt), nil } func encStr(userKey []byte, s string) (string, error) { if s == "" { return "", nil } return symEncrypt(userKey, []byte(s)) } // PlainField is a decrypted custom field. type PlainField struct { Type int `json:"type"` Name string `json:"name"` Value string `json:"value"` } // PlainCipher is a decrypted item view. type PlainCipher struct { ID string `json:"id"` Name string `json:"name"` Notes string `json:"notes"` Type int `json:"type"` Fields []PlainField `json:"fields"` Login struct { Username string `json:"username"` Password string `json:"password"` } `json:"login"` FolderID string `json:"folderId"` URIs []string `json:"uris"` } // DecryptCipher converts a raw sync cipher into PlainCipher. func DecryptCipher(userKey []byte, raw []byte) (*PlainCipher, error) { var cr cipherRaw if err := json.Unmarshal(raw, &cr); err != nil { return nil, err } if cr.DeletedDate != nil { return nil, fmt.Errorf("deleted") } ck, err := cipherKeyFor(userKey, cr.Key) if err != nil { return nil, err } pc := &PlainCipher{ID: cr.ID, Type: cr.Type} if pc.Name, err = decStr(userKey, ck, &cr.Name); err != nil { return nil, fmt.Errorf("name: %w", err) } if cr.Notes != nil { if pc.Notes, err = decStr(userKey, ck, cr.Notes); err != nil { return nil, fmt.Errorf("notes: %w", err) } } for _, f := range cr.Fields { pf := PlainField{Type: f.Type} if pf.Name, err = decStr(userKey, ck, f.Name); err != nil { continue } if pf.Value, err = decStr(userKey, ck, f.Value); err != nil { continue } pc.Fields = append(pc.Fields, pf) } if cr.Login != nil { if cr.Login.Username != nil { pc.Login.Username, _ = decStr(userKey, ck, cr.Login.Username) } if cr.Login.Password != nil { pc.Login.Password, _ = decStr(userKey, ck, cr.Login.Password) } for _, u := range cr.Login.URIs { var um map[string]any um, _ = u.(map[string]any) if um != nil { if us, ok := um["uri"].(string); ok { v, err := decStr(userKey, ck, &us) if err == nil { pc.URIs = append(pc.URIs, v) } } } } } return pc, nil } // BuildSecureNoteJSON produces an encrypted create/update payload for a // secure-note item with hidden fields (type 1). func BuildSecureNoteJSON(userKey []byte, name, folderID, notes string, fields []PlainField, existing *cipherRaw) (map[string]any, error) { nameEnc, err := encStr(userKey, name) if err != nil { return nil, err } notesEnc, err := encStr(userKey, notes) if err != nil { return nil, err } var encFields []map[string]any for _, f := range fields { fn, err := encStr(userKey, f.Name) if err != nil { return nil, err } fv, err := encStr(userKey, f.Value) if err != nil { return nil, err } encFields = append(encFields, map[string]any{ "type": f.Type, "name": fn, "value": fv, }) } payload := map[string]any{ "type": 2, "name": nameEnc, "notes": notesEnc, "fields": encFields, "secureNote": map[string]any{"type": 0}, } if folderID != "" { payload["folderId"] = folderID } if existing != nil { payload["id"] = existing.ID if existing.OrganizationID != nil { payload["organizationId"] = existing.OrganizationID } } return payload, nil } // parseEnvFields parses KEY=VALUE lines (quoted or bare) into hidden fields. func parseEnvFields(text string) []PlainField { var out []PlainField for _, line := range strings.Split(text, "\n") { line = strings.TrimSpace(line) if line == "" || strings.HasPrefix(line, "#") { continue } eq := strings.Index(line, "=") if eq <= 0 { continue } k := line[:eq] v := line[eq+1:] v = strings.Trim(v, "'\"") out = append(out, PlainField{Type: 1, Name: k, Value: v}) } return out } // BuildLoginJSON produces an encrypted login-item payload: username/password // as first-class login fields, URIs, and the remaining keys as custom fields // (hidden for secrets, text for URLs/IDs). func BuildLoginJSON(userKey []byte, name, folderID, username, password string, uris []string, fields []PlainField, existing *cipherRaw) (map[string]any, error) { nameEnc, err := encStr(userKey, name) if err != nil { return nil, err } login := map[string]any{} if username != "" { u, err := encStr(userKey, username) if err != nil { return nil, err } login["username"] = u } if password != "" { p, err := encStr(userKey, password) if err != nil { return nil, err } login["password"] = p } if len(uris) > 0 { var list []map[string]any for _, u := range uris { ue, err := encStr(userKey, u) if err != nil { return nil, err } list = append(list, map[string]any{"uri": ue}) } login["uris"] = list } var encFields []map[string]any for _, f := range fields { fn, err := encStr(userKey, f.Name) if err != nil { return nil, err } fv, err := encStr(userKey, f.Value) if err != nil { return nil, err } encFields = append(encFields, map[string]any{"type": f.Type, "name": fn, "value": fv}) } payload := map[string]any{"type": 1, "name": nameEnc, "login": login} if len(encFields) > 0 { payload["fields"] = encFields } if folderID != "" { payload["folderId"] = folderID } if existing != nil { payload["id"] = existing.ID if existing.OrganizationID != nil { payload["organizationId"] = existing.OrganizationID } } return payload, nil }