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{}