ci / vet (push) Successful in 55s
convert of login items keeps original env names in fields; env now also exposes the login triple so sourced consumers can map to legacy names. Ticket: https://projects.knownelement.com/issues/832
279 lines
7.2 KiB
Go
279 lines
7.2 KiB
Go
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
|
|
}
|