github.com/cozy/cozy-stack@v0.0.0-20240327093429-939e4a21320e/model/bitwarden/cipher.go (about)

     1  // Package bitwarden is used for managing the ciphers, encrypted on the client
     2  // side.
     3  package bitwarden
     4  
     5  import (
     6  	"encoding/json"
     7  	"strconv"
     8  	"time"
     9  
    10  	"github.com/cozy/cozy-stack/model/instance"
    11  	"github.com/cozy/cozy-stack/model/permission"
    12  	"github.com/cozy/cozy-stack/pkg/consts"
    13  	"github.com/cozy/cozy-stack/pkg/couchdb"
    14  	"github.com/cozy/cozy-stack/pkg/couchdb/mango"
    15  	"github.com/cozy/cozy-stack/pkg/metadata"
    16  )
    17  
    18  // DocTypeVersion represents the doctype version. Each time this document
    19  // structure is modified, update this value
    20  const DocTypeVersion = "1"
    21  
    22  // CipherType is used to know what contains the cipher: a login, a secure note,
    23  // a card or an identity.
    24  type CipherType int
    25  
    26  // LoginType, SecureNoteType, CardType, and IdentityType are the 4 possible
    27  // types of ciphers.
    28  const (
    29  	LoginType      = 1
    30  	SecureNoteType = 2
    31  	CardType       = 3
    32  	IdentityType   = 4
    33  )
    34  
    35  // Possible types for ciphers additional fields
    36  const (
    37  	FieldTypeText    = 0
    38  	FieldTypeHidden  = 1
    39  	FieldTypeBoolean = 2
    40  )
    41  
    42  // LoginURI is a field for an URI.
    43  // See https://github.com/bitwarden/jslib/blob/master/common/src/models/api/loginUriApi.ts
    44  type LoginURI struct {
    45  	URI   string      `json:"uri"`
    46  	Match interface{} `json:"match,omitempty"`
    47  }
    48  
    49  // LoginData is the encrypted data for a cipher with the login type.
    50  type LoginData struct {
    51  	URIs     []LoginURI `json:"uris,omitempty"`
    52  	Username string     `json:"username,omitempty"`
    53  	Password string     `json:"password,omitempty"`
    54  	RevDate  string     `json:"passwordRevisionDate,omitempty"`
    55  	TOTP     string     `json:"totp,omitempty"`
    56  }
    57  
    58  // Field is used to store some additional fields.
    59  type Field struct {
    60  	// See https://github.com/bitwarden/jslib/blob/master/common/src/enums/fieldType.ts
    61  	Type  int    `json:"type"`
    62  	Name  string `json:"name"`
    63  	Value string `json:"value"`
    64  }
    65  
    66  // MapData is used for the data of secure note, card, and identity.
    67  type MapData map[string]interface{}
    68  
    69  // Cipher is an encrypted item that can be a login, a secure note, a card or an
    70  // identity.
    71  type Cipher struct {
    72  	CouchID        string                 `json:"_id,omitempty"`
    73  	CouchRev       string                 `json:"_rev,omitempty"`
    74  	Type           CipherType             `json:"type"`
    75  	SharedWithCozy bool                   `json:"shared_with_cozy"`
    76  	Favorite       bool                   `json:"favorite,omitempty"`
    77  	Name           string                 `json:"name"`
    78  	Notes          string                 `json:"notes,omitempty"`
    79  	FolderID       string                 `json:"folder_id,omitempty"`
    80  	OrganizationID string                 `json:"organization_id,omitempty"`
    81  	CollectionID   string                 `json:"collection_id,omitempty"`
    82  	Login          *LoginData             `json:"login,omitempty"`
    83  	Data           *MapData               `json:"data,omitempty"`
    84  	Fields         []Field                `json:"fields"`
    85  	Metadata       *metadata.CozyMetadata `json:"cozyMetadata,omitempty"`
    86  	DeletedDate    *time.Time             `json:"deletedDate,omitempty"`
    87  }
    88  
    89  // ID returns the cipher qualified identifier
    90  func (c *Cipher) ID() string { return c.CouchID }
    91  
    92  // Rev returns the cipher revision
    93  func (c *Cipher) Rev() string { return c.CouchRev }
    94  
    95  // DocType returns the cipher document type
    96  func (c *Cipher) DocType() string { return consts.BitwardenCiphers }
    97  
    98  // Clone implements couchdb.Doc
    99  func (c *Cipher) Clone() couchdb.Doc {
   100  	cloned := *c
   101  	if c.Login != nil {
   102  		uris := make([]LoginURI, len(c.Login.URIs))
   103  		copy(uris, c.Login.URIs)
   104  		cloned.Login = &LoginData{
   105  			URIs:     uris,
   106  			Username: c.Login.Username,
   107  			Password: c.Login.Password,
   108  			RevDate:  c.Login.RevDate,
   109  			TOTP:     c.Login.TOTP,
   110  		}
   111  	}
   112  	cloned.Fields = make([]Field, len(c.Fields))
   113  	copy(cloned.Fields, c.Fields)
   114  	if c.Metadata != nil {
   115  		cloned.Metadata = c.Metadata.Clone()
   116  	}
   117  	return &cloned
   118  }
   119  
   120  // Fetch implements permissions.Fetcher
   121  func (c *Cipher) Fetch(field string) []string {
   122  	switch field {
   123  	case "deletedDate":
   124  		if c.DeletedDate != nil {
   125  			date := *c.DeletedDate
   126  			return []string{date.String()}
   127  		}
   128  		return []string{""}
   129  	case "shared_with_cozy":
   130  		return []string{strconv.FormatBool(c.SharedWithCozy)}
   131  	case "type":
   132  		return []string{strconv.FormatInt(int64(c.Type), 32)}
   133  	case "name":
   134  		return []string{c.Name}
   135  	case "organization_id":
   136  		return []string{c.OrganizationID}
   137  	case "collection_id":
   138  		return []string{c.CollectionID}
   139  	}
   140  	return nil
   141  }
   142  
   143  // SetID changes the cipher qualified identifier
   144  func (c *Cipher) SetID(id string) { c.CouchID = id }
   145  
   146  // SetRev changes the cipher revision
   147  func (c *Cipher) SetRev(rev string) { c.CouchRev = rev }
   148  
   149  // FindCiphersInFolder finds the ciphers in the given folder.
   150  func FindCiphersInFolder(inst *instance.Instance, folderID string) ([]*Cipher, error) {
   151  	var ciphers []*Cipher
   152  	req := &couchdb.FindRequest{
   153  		UseIndex: "by-folder-id",
   154  		Selector: mango.Equal("folder_id", folderID),
   155  		Limit:    1000,
   156  	}
   157  	err := couchdb.FindDocs(inst, consts.BitwardenCiphers, req, &ciphers)
   158  	if err != nil {
   159  		return nil, err
   160  	}
   161  	return ciphers, nil
   162  }
   163  
   164  // DeleteUnrecoverableCiphers will delete all the ciphers that are not shared
   165  // with the cozy organization. It should be called when the master password is
   166  // lost, as there are no ways to recover those encrypted ciphers.
   167  func DeleteUnrecoverableCiphers(inst *instance.Instance) error {
   168  	var ciphers []couchdb.Doc
   169  	err := couchdb.ForeachDocs(inst, consts.BitwardenCiphers, func(_ string, data json.RawMessage) error {
   170  		var c Cipher
   171  		if err := json.Unmarshal(data, &c); err != nil {
   172  			return err
   173  		}
   174  		if !c.SharedWithCozy {
   175  			ciphers = append(ciphers, &c)
   176  		}
   177  		return nil
   178  	})
   179  	if err != nil {
   180  		if couchdb.IsNoDatabaseError(err) {
   181  			return nil
   182  		}
   183  		return err
   184  	}
   185  	return couchdb.BulkDeleteDocs(inst, consts.BitwardenCiphers, ciphers)
   186  }
   187  
   188  var _ couchdb.Doc = &Cipher{}
   189  var _ permission.Fetcher = &Cipher{}