github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/web/bitwarden/ciphers.go (about) 1 package bitwarden 2 3 import ( 4 "encoding/json" 5 "errors" 6 "net/http" 7 "time" 8 9 "github.com/cozy/cozy-stack/model/bitwarden" 10 "github.com/cozy/cozy-stack/model/bitwarden/settings" 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/metadata" 15 "github.com/cozy/cozy-stack/pkg/realtime" 16 "github.com/cozy/cozy-stack/web/middlewares" 17 "github.com/labstack/echo/v4" 18 ) 19 20 type loginRequest struct { 21 URI string `json:"uri"` // For compatibility with some clients 22 *bitwarden.LoginData 23 } 24 25 type idsRequest struct { 26 IDs []string `json:"ids"` 27 } 28 29 // https://github.com/bitwarden/jslib/blob/master/common/src/models/request/cipherRequest.ts 30 type cipherRequest struct { 31 Type bitwarden.CipherType `json:"type"` 32 Favorite bool `json:"favorite"` 33 Name string `json:"name"` 34 Notes string `json:"notes"` 35 FolderID string `json:"folderId"` 36 OrganizationID string `json:"organizationId"` 37 Login loginRequest `json:"login"` 38 Fields []bitwarden.Field `json:"fields"` 39 SecureNote bitwarden.MapData `json:"securenote"` 40 Card bitwarden.MapData `json:"card"` 41 Identity bitwarden.MapData `json:"identity"` 42 } 43 44 func (r *cipherRequest) toCipher() (*bitwarden.Cipher, error) { 45 if r.Name == "" { 46 return nil, errors.New("name is mandatory") 47 } 48 49 c := bitwarden.Cipher{ 50 Type: r.Type, 51 Favorite: r.Favorite, 52 Name: r.Name, 53 Notes: r.Notes, 54 FolderID: r.FolderID, 55 Fields: r.Fields, 56 } 57 switch c.Type { 58 case bitwarden.LoginType: 59 if r.Login.LoginData == nil { 60 r.Login.LoginData = &bitwarden.LoginData{} 61 } 62 if r.Login.URI != "" { 63 u := bitwarden.LoginURI{URI: r.Login.URI, Match: nil} 64 r.Login.URIs = append(r.Login.URIs, u) 65 } 66 c.Login = r.Login.LoginData 67 case bitwarden.SecureNoteType: 68 c.Data = &r.SecureNote 69 case bitwarden.CardType: 70 c.Data = &r.Card 71 case bitwarden.IdentityType: 72 c.Data = &r.Identity 73 default: 74 return nil, errors.New("type has an unknown value") 75 } 76 77 md := metadata.New() 78 md.DocTypeVersion = bitwarden.DocTypeVersion 79 c.Metadata = md 80 return &c, nil 81 } 82 83 type importCipherRequest struct { 84 Ciphers []cipherRequest `json:"ciphers"` 85 Folders []folderRequest `json:"folders"` 86 FolderRelationships []struct { 87 Cipher int `json:"key"` 88 Folder int `json:"value"` 89 } `json:"folderRelationships"` 90 } 91 92 type uriResponse struct { 93 URI string `json:"Uri"` 94 Match interface{} `json:"Match"` 95 } 96 97 type loginResponse struct { 98 URIs []uriResponse `json:"Uris"` 99 Username *string `json:"Username"` 100 Password *string `json:"Password"` 101 RevDate *string `json:"PasswordRevisionDate"` 102 TOTP *string `json:"Totp"` 103 } 104 105 type fieldResponse struct { 106 Type int `json:"Type"` 107 Name string `json:"Name"` 108 Value string `json:"Value"` 109 } 110 111 // https://github.com/bitwarden/jslib/blob/master/common/src/models/response/cipherResponse.ts 112 type cipherResponse struct { 113 Object string `json:"Object"` 114 ID string `json:"Id"` 115 Type int `json:"Type"` 116 Favorite bool `json:"Favorite"` 117 Name string `json:"Name"` 118 Notes *string `json:"Notes"` 119 FolderID *string `json:"FolderId"` 120 OrganizationID *string `json:"OrganizationId"` 121 CollectionIDs []string `json:"CollectionIds"` 122 Fields interface{} `json:"Fields"` 123 Attachments *string `json:"Attachments"` 124 Login *loginResponse `json:"Login,omitempty"` 125 SecureNote map[string]interface{} `json:"SecureNote,omitempty"` 126 Card map[string]interface{} `json:"Card,omitempty"` 127 Identity map[string]interface{} `json:"Identity,omitempty"` 128 Date time.Time `json:"RevisionDate"` 129 DeletedDate *time.Time `json:"DeletedDate,omitempty"` 130 Edit bool `json:"Edit"` 131 UseOTP bool `json:"OrganizationUseTotp"` 132 } 133 134 func titleizeKeys(data bitwarden.MapData) map[string]interface{} { 135 res := make(map[string]interface{}) 136 for k, v := range data { 137 if k == "ssn" { 138 k = "SSN" 139 } 140 key := []byte(k) 141 if 'a' <= key[0] && key[0] <= 'z' { 142 key[0] -= 'a' - 'A' 143 } 144 res[string(key)] = v 145 } 146 return res 147 } 148 149 func newCipherResponse(c *bitwarden.Cipher, setting *settings.Settings) *cipherResponse { 150 r := cipherResponse{ 151 Object: "cipher", 152 ID: c.CouchID, 153 Type: int(c.Type), 154 Favorite: c.Favorite, 155 Name: c.Name, 156 Edit: true, 157 UseOTP: false, 158 } 159 if c.DeletedDate != nil { 160 date := c.DeletedDate.UTC() 161 r.DeletedDate = &date 162 } 163 164 if c.Notes != "" { 165 r.Notes = &c.Notes 166 } 167 if c.FolderID != "" { 168 r.FolderID = &c.FolderID 169 } 170 if c.Metadata != nil { 171 r.Date = c.Metadata.UpdatedAt.UTC() 172 } 173 if c.SharedWithCozy { 174 r.OrganizationID = &setting.OrganizationID 175 r.CollectionIDs = append(r.CollectionIDs, setting.CollectionID) 176 } else if c.CollectionID != "" { 177 r.OrganizationID = &c.OrganizationID 178 r.CollectionIDs = append(r.CollectionIDs, c.CollectionID) 179 } 180 181 if len(c.Fields) > 0 { 182 fields := make([]fieldResponse, len(c.Fields)) 183 for i, f := range c.Fields { 184 fields[i] = fieldResponse{ 185 Type: f.Type, 186 Name: f.Name, 187 Value: f.Value, 188 } 189 } 190 r.Fields = fields 191 } 192 193 switch c.Type { 194 case bitwarden.LoginType: 195 if c.Login != nil { 196 r.Login = &loginResponse{} 197 if len(c.Login.URIs) > 0 { 198 r.Login.URIs = make([]uriResponse, len(c.Login.URIs)) 199 for i, u := range c.Login.URIs { 200 r.Login.URIs[i] = uriResponse{URI: u.URI, Match: u.Match} 201 } 202 } 203 if c.Login.Username != "" { 204 r.Login.Username = &c.Login.Username 205 } 206 if c.Login.Password != "" { 207 r.Login.Password = &c.Login.Password 208 } 209 if c.Login.RevDate != "" { 210 r.Login.RevDate = &c.Login.RevDate 211 } 212 if c.Login.TOTP != "" { 213 r.Login.TOTP = &c.Login.TOTP 214 } 215 } 216 case bitwarden.SecureNoteType: 217 if c.Data != nil { 218 r.SecureNote = titleizeKeys(*c.Data) 219 } 220 case bitwarden.CardType: 221 if c.Data != nil { 222 r.Card = titleizeKeys(*c.Data) 223 } 224 case bitwarden.IdentityType: 225 if c.Data != nil { 226 r.Identity = titleizeKeys(*c.Data) 227 } 228 } 229 230 return &r 231 } 232 233 type ciphersList struct { 234 Data []*cipherResponse `json:"Data"` 235 Object string `json:"Object"` 236 } 237 238 // ListCiphers is the route for listing the Bitwarden ciphers. 239 // No pagination yet. 240 func ListCiphers(c echo.Context) error { 241 inst := middlewares.GetInstance(c) 242 if err := middlewares.AllowWholeType(c, permission.GET, consts.BitwardenCiphers); err != nil { 243 return c.JSON(http.StatusUnauthorized, echo.Map{ 244 "error": "invalid token", 245 }) 246 } 247 248 var ciphers []*bitwarden.Cipher 249 req := &couchdb.AllDocsRequest{} 250 if err := couchdb.GetAllDocs(inst, consts.BitwardenCiphers, req, &ciphers); err != nil { 251 return c.JSON(http.StatusInternalServerError, echo.Map{ 252 "error": err.Error(), 253 }) 254 } 255 256 setting, err := settings.Get(inst) 257 if err != nil { 258 return c.JSON(http.StatusInternalServerError, echo.Map{ 259 "error": err.Error(), 260 }) 261 } 262 263 res := &ciphersList{Object: "list"} 264 for _, f := range ciphers { 265 res.Data = append(res.Data, newCipherResponse(f, setting)) 266 } 267 return c.JSON(http.StatusOK, res) 268 } 269 270 // CreateCipher is the handler for creating a cipher: login, secure note, etc. 271 func CreateCipher(c echo.Context) error { 272 inst := middlewares.GetInstance(c) 273 if err := middlewares.AllowWholeType(c, permission.POST, consts.BitwardenCiphers); err != nil { 274 return c.JSON(http.StatusUnauthorized, echo.Map{ 275 "error": "invalid token", 276 }) 277 } 278 279 var req cipherRequest 280 if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil { 281 return c.JSON(http.StatusBadRequest, echo.Map{ 282 "error": "invalid JSON", 283 }) 284 } 285 286 cipher, err := req.toCipher() 287 if err != nil { 288 return c.JSON(http.StatusBadRequest, echo.Map{ 289 "error": err.Error(), 290 }) 291 } 292 293 if cipher.FolderID != "" { 294 folder := &bitwarden.Folder{} 295 if err := couchdb.GetDoc(inst, consts.BitwardenFolders, cipher.FolderID, folder); err != nil { 296 return c.JSON(http.StatusBadRequest, echo.Map{ 297 "error": "folder not found", 298 }) 299 } 300 } 301 302 if err := couchdb.CreateDoc(inst, cipher); err != nil { 303 return c.JSON(http.StatusInternalServerError, echo.Map{ 304 "error": err.Error(), 305 }) 306 } 307 308 setting, err := settings.Get(inst) 309 if err != nil { 310 return c.JSON(http.StatusInternalServerError, echo.Map{ 311 "error": err.Error(), 312 }) 313 } 314 315 _ = settings.UpdateRevisionDate(inst, setting) 316 res := newCipherResponse(cipher, setting) 317 return c.JSON(http.StatusOK, res) 318 } 319 320 // CreateSharedCipher is the handler for creating a shared cipher. 321 func CreateSharedCipher(c echo.Context) error { 322 inst := middlewares.GetInstance(c) 323 if err := middlewares.AllowWholeType(c, permission.POST, consts.BitwardenCiphers); err != nil { 324 return c.JSON(http.StatusUnauthorized, echo.Map{ 325 "error": "invalid token", 326 }) 327 } 328 329 var req struct { 330 Cipher cipherRequest `json:"cipher"` 331 CollectionIDs []string `json:"collectionIds"` 332 } 333 if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil { 334 return c.JSON(http.StatusBadRequest, echo.Map{ 335 "error": "invalid JSON", 336 }) 337 } 338 339 cipher, err := req.Cipher.toCipher() 340 if err != nil { 341 return c.JSON(http.StatusBadRequest, echo.Map{ 342 "error": err.Error(), 343 }) 344 } 345 346 if cipher.FolderID != "" { 347 folder := &bitwarden.Folder{} 348 if err := couchdb.GetDoc(inst, consts.BitwardenFolders, cipher.FolderID, folder); err != nil { 349 return c.JSON(http.StatusBadRequest, echo.Map{ 350 "error": "folder not found", 351 }) 352 } 353 } 354 355 setting, err := settings.Get(inst) 356 if err != nil { 357 return c.JSON(http.StatusInternalServerError, echo.Map{ 358 "error": err.Error(), 359 }) 360 } 361 if len(req.CollectionIDs) != 1 { 362 return c.JSON(http.StatusBadRequest, echo.Map{ 363 "error": "only one collection per organization is supported", 364 }) 365 } 366 for _, id := range req.CollectionIDs { 367 if id == setting.CollectionID { 368 cipher.SharedWithCozy = true 369 } else { 370 cipher.OrganizationID = req.Cipher.OrganizationID 371 cipher.CollectionID = id 372 } 373 } 374 375 if err := couchdb.CreateDoc(inst, cipher); err != nil { 376 return c.JSON(http.StatusInternalServerError, echo.Map{ 377 "error": err.Error(), 378 }) 379 } 380 381 _ = settings.UpdateRevisionDate(inst, setting) 382 res := newCipherResponse(cipher, setting) 383 return c.JSON(http.StatusOK, res) 384 } 385 386 // GetCipher returns information about a single cipher. 387 func GetCipher(c echo.Context) error { 388 inst := middlewares.GetInstance(c) 389 if err := middlewares.AllowWholeType(c, permission.GET, consts.BitwardenCiphers); err != nil { 390 return c.JSON(http.StatusUnauthorized, echo.Map{ 391 "error": "invalid token", 392 }) 393 } 394 395 id := c.Param("id") 396 if id == "" { 397 return c.JSON(http.StatusNotFound, echo.Map{ 398 "error": "missing id", 399 }) 400 } 401 402 cipher := &bitwarden.Cipher{} 403 if err := couchdb.GetDoc(inst, consts.BitwardenCiphers, id, cipher); err != nil { 404 if couchdb.IsNotFoundError(err) { 405 return c.JSON(http.StatusNotFound, echo.Map{ 406 "error": "not found", 407 }) 408 } 409 return c.JSON(http.StatusInternalServerError, echo.Map{ 410 "error": err.Error(), 411 }) 412 } 413 414 setting, err := settings.Get(inst) 415 if err != nil { 416 return c.JSON(http.StatusInternalServerError, echo.Map{ 417 "error": err.Error(), 418 }) 419 } 420 421 res := newCipherResponse(cipher, setting) 422 return c.JSON(http.StatusOK, res) 423 } 424 425 // UpdateCipher is the route for changing a cipher. 426 func UpdateCipher(c echo.Context) error { 427 inst := middlewares.GetInstance(c) 428 if err := middlewares.AllowWholeType(c, permission.PUT, consts.BitwardenCiphers); err != nil { 429 return c.JSON(http.StatusUnauthorized, echo.Map{ 430 "error": "invalid token", 431 }) 432 } 433 434 id := c.Param("id") 435 if id == "" { 436 return c.JSON(http.StatusNotFound, echo.Map{ 437 "error": "missing id", 438 }) 439 } 440 441 old := &bitwarden.Cipher{} 442 if err := couchdb.GetDoc(inst, consts.BitwardenCiphers, id, old); err != nil { 443 if couchdb.IsNotFoundError(err) { 444 return c.JSON(http.StatusNotFound, echo.Map{ 445 "error": "not found", 446 }) 447 } 448 return c.JSON(http.StatusInternalServerError, echo.Map{ 449 "error": err.Error(), 450 }) 451 } 452 453 var req cipherRequest 454 if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil { 455 return c.JSON(http.StatusBadRequest, echo.Map{ 456 "error": "invalid JSON", 457 }) 458 } 459 cipher, err := req.toCipher() 460 if err != nil { 461 return c.JSON(http.StatusBadRequest, echo.Map{ 462 "error": err.Error(), 463 }) 464 } 465 466 if cipher.FolderID != "" && cipher.FolderID != old.FolderID { 467 folder := &bitwarden.Folder{} 468 if err := couchdb.GetDoc(inst, consts.BitwardenFolders, cipher.FolderID, folder); err != nil { 469 return c.JSON(http.StatusBadRequest, echo.Map{ 470 "error": "folder not found", 471 }) 472 } 473 } 474 475 // XXX On an update, the client send the OrganizationId but not the 476 // collectionIds. 477 if req.OrganizationID != "" { 478 if old.SharedWithCozy { 479 cipher.SharedWithCozy = true 480 } else { 481 cipher.OrganizationID = req.OrganizationID 482 cipher.CollectionID = old.CollectionID 483 } 484 } 485 486 if old.Metadata != nil { 487 cipher.Metadata = old.Metadata.Clone() 488 } 489 cipher.Metadata.ChangeUpdatedAt() 490 cipher.SetID(old.ID()) 491 cipher.SetRev(old.Rev()) 492 if err := couchdb.UpdateDocWithOld(inst, cipher, old); err != nil { 493 return c.JSON(http.StatusInternalServerError, echo.Map{ 494 "error": err.Error(), 495 }) 496 } 497 498 setting, err := settings.Get(inst) 499 if err != nil { 500 return c.JSON(http.StatusInternalServerError, echo.Map{ 501 "error": err.Error(), 502 }) 503 } 504 505 _ = settings.UpdateRevisionDate(inst, setting) 506 res := newCipherResponse(cipher, setting) 507 return c.JSON(http.StatusOK, res) 508 } 509 510 // DeleteCipher is the handler for the route to delete a cipher. 511 func DeleteCipher(c echo.Context) error { 512 inst := middlewares.GetInstance(c) 513 if err := middlewares.AllowWholeType(c, permission.DELETE, consts.BitwardenCiphers); err != nil { 514 return c.JSON(http.StatusUnauthorized, echo.Map{ 515 "error": "invalid token", 516 }) 517 } 518 519 id := c.Param("id") 520 if id == "" { 521 return c.JSON(http.StatusNotFound, echo.Map{ 522 "error": "missing id", 523 }) 524 } 525 526 cipher := &bitwarden.Cipher{} 527 if err := couchdb.GetDoc(inst, consts.BitwardenCiphers, id, cipher); err != nil { 528 if couchdb.IsNotFoundError(err) { 529 return c.JSON(http.StatusNotFound, echo.Map{ 530 "error": "not found", 531 }) 532 } 533 return c.JSON(http.StatusInternalServerError, echo.Map{ 534 "error": err.Error(), 535 }) 536 } 537 538 if err := couchdb.DeleteDoc(inst, cipher); err != nil { 539 return c.JSON(http.StatusInternalServerError, echo.Map{ 540 "error": err.Error(), 541 }) 542 } 543 544 _ = settings.UpdateRevisionDate(inst, nil) 545 return c.NoContent(http.StatusOK) 546 } 547 548 // SoftDeleteCipher is the handler for the route to soft delete a cipher. 549 // See https://github.com/bitwarden/server/pull/684 for the bitwarden implementation 550 func SoftDeleteCipher(c echo.Context) error { 551 inst := middlewares.GetInstance(c) 552 if err := middlewares.AllowWholeType(c, permission.PUT, consts.BitwardenCiphers); err != nil { 553 return c.JSON(http.StatusUnauthorized, echo.Map{ 554 "error": "invalid token", 555 }) 556 } 557 558 id := c.Param("id") 559 if id == "" { 560 return c.JSON(http.StatusNotFound, echo.Map{ 561 "error": "missing id", 562 }) 563 } 564 565 cipher := &bitwarden.Cipher{} 566 if err := couchdb.GetDoc(inst, consts.BitwardenCiphers, id, cipher); err != nil { 567 if couchdb.IsNotFoundError(err) { 568 return c.JSON(http.StatusNotFound, echo.Map{ 569 "error": "not found", 570 }) 571 } 572 return c.JSON(http.StatusInternalServerError, echo.Map{ 573 "error": err.Error(), 574 }) 575 } 576 577 setting, err := settings.Get(inst) 578 if err != nil { 579 return c.JSON(http.StatusInternalServerError, echo.Map{ 580 "error": err.Error(), 581 }) 582 } 583 584 cipher.Metadata.ChangeUpdatedAt() 585 cipher.DeletedDate = &cipher.Metadata.UpdatedAt 586 if err := couchdb.UpdateDoc(inst, cipher); err != nil { 587 return c.JSON(http.StatusInternalServerError, echo.Map{ 588 "error": err.Error(), 589 }) 590 } 591 _ = settings.UpdateRevisionDate(inst, setting) 592 593 return c.NoContent(http.StatusOK) 594 } 595 596 // RestoreCipher is the handler for the route to restore a soft-deleted cipher. 597 // See https://github.com/bitwarden/server/pull/684 for the bitwarden implementation 598 func RestoreCipher(c echo.Context) error { 599 inst := middlewares.GetInstance(c) 600 if err := middlewares.AllowWholeType(c, permission.PUT, consts.BitwardenCiphers); err != nil { 601 return c.JSON(http.StatusUnauthorized, echo.Map{ 602 "error": "invalid token", 603 }) 604 } 605 606 id := c.Param("id") 607 if id == "" { 608 return c.JSON(http.StatusNotFound, echo.Map{ 609 "error": "missing id", 610 }) 611 } 612 613 cipher := &bitwarden.Cipher{} 614 if err := couchdb.GetDoc(inst, consts.BitwardenCiphers, id, cipher); err != nil { 615 if couchdb.IsNotFoundError(err) { 616 return c.JSON(http.StatusNotFound, echo.Map{ 617 "error": "not found", 618 }) 619 } 620 return c.JSON(http.StatusInternalServerError, echo.Map{ 621 "error": err.Error(), 622 }) 623 } 624 625 setting, err := settings.Get(inst) 626 if err != nil { 627 return c.JSON(http.StatusInternalServerError, echo.Map{ 628 "error": err.Error(), 629 }) 630 } 631 632 cipher.DeletedDate = nil 633 cipher.Metadata.ChangeUpdatedAt() 634 if err := couchdb.UpdateDoc(inst, cipher); err != nil { 635 return c.JSON(http.StatusInternalServerError, echo.Map{ 636 "error": err.Error(), 637 }) 638 } 639 _ = settings.UpdateRevisionDate(inst, setting) 640 return c.NoContent(http.StatusOK) 641 } 642 643 // BulkDeleteCiphers is the handler for the route to delete ciphers in bulk. 644 func BulkDeleteCiphers(c echo.Context) error { 645 inst := middlewares.GetInstance(c) 646 if err := middlewares.AllowWholeType(c, permission.DELETE, consts.BitwardenCiphers); err != nil { 647 return c.JSON(http.StatusUnauthorized, echo.Map{ 648 "error": "invalid token", 649 }) 650 } 651 652 var req idsRequest 653 if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil { 654 return c.JSON(http.StatusBadRequest, echo.Map{ 655 "error": "invalid JSON", 656 }) 657 } 658 if len(req.IDs) == 0 { 659 return c.JSON(http.StatusBadRequest, echo.Map{ 660 "error": "Request missing ids field", 661 }) 662 } 663 664 var ciphers []bitwarden.Cipher 665 keys := couchdb.AllDocsRequest{Keys: req.IDs} 666 if err := couchdb.GetAllDocs(inst, consts.BitwardenCiphers, &keys, &ciphers); err != nil { 667 return c.JSON(http.StatusInternalServerError, echo.Map{ 668 "error": err.Error(), 669 }) 670 } 671 docs := make([]couchdb.Doc, len(ciphers)) 672 for i := range ciphers { 673 docs[i] = ciphers[i].Clone() 674 } 675 if err := couchdb.BulkDeleteDocs(inst, consts.BitwardenCiphers, docs); err != nil { 676 return c.JSON(http.StatusInternalServerError, echo.Map{ 677 "error": err.Error(), 678 }) 679 } 680 681 _ = settings.UpdateRevisionDate(inst, nil) 682 return c.NoContent(http.StatusOK) 683 } 684 685 // BulkSoftDeleteCiphers is the handler for the route to soft delete ciphers in bulk. 686 func BulkSoftDeleteCiphers(c echo.Context) error { 687 inst := middlewares.GetInstance(c) 688 if err := middlewares.AllowWholeType(c, permission.PUT, consts.BitwardenCiphers); err != nil { 689 return c.JSON(http.StatusUnauthorized, echo.Map{ 690 "error": "invalid token", 691 }) 692 } 693 694 var req idsRequest 695 if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil { 696 return c.JSON(http.StatusBadRequest, echo.Map{ 697 "error": "invalid JSON", 698 }) 699 } 700 if len(req.IDs) == 0 { 701 return c.JSON(http.StatusBadRequest, echo.Map{ 702 "error": "Request missing ids field", 703 }) 704 } 705 706 var ciphers []bitwarden.Cipher 707 keys := couchdb.AllDocsRequest{Keys: req.IDs} 708 if err := couchdb.GetAllDocs(inst, consts.BitwardenCiphers, &keys, &ciphers); err != nil { 709 return c.JSON(http.StatusInternalServerError, echo.Map{ 710 "error": err.Error(), 711 }) 712 } 713 olds := make([]interface{}, len(ciphers)) 714 docs := make([]interface{}, len(ciphers)) 715 for i := range ciphers { 716 olds[i] = ciphers[i] 717 cipher := ciphers[i].Clone().(*bitwarden.Cipher) 718 cipher.Metadata.ChangeUpdatedAt() 719 cipher.DeletedDate = &cipher.Metadata.UpdatedAt 720 docs[i] = cipher 721 } 722 if err := couchdb.BulkUpdateDocs(inst, consts.BitwardenCiphers, docs, olds); err != nil { 723 return c.JSON(http.StatusInternalServerError, echo.Map{ 724 "error": err.Error(), 725 }) 726 } 727 728 _ = settings.UpdateRevisionDate(inst, nil) 729 return c.NoContent(http.StatusOK) 730 } 731 732 // BulkRestoreCiphers is the handler for the route to restore ciphers in bulk. 733 func BulkRestoreCiphers(c echo.Context) error { 734 inst := middlewares.GetInstance(c) 735 if err := middlewares.AllowWholeType(c, permission.PUT, consts.BitwardenCiphers); err != nil { 736 return c.JSON(http.StatusUnauthorized, echo.Map{ 737 "error": "invalid token", 738 }) 739 } 740 741 var req idsRequest 742 if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil { 743 return c.JSON(http.StatusBadRequest, echo.Map{ 744 "error": "invalid JSON", 745 }) 746 } 747 if len(req.IDs) == 0 { 748 return c.JSON(http.StatusBadRequest, echo.Map{ 749 "error": "Request missing ids field", 750 }) 751 } 752 753 var ciphers []bitwarden.Cipher 754 keys := couchdb.AllDocsRequest{Keys: req.IDs} 755 if err := couchdb.GetAllDocs(inst, consts.BitwardenCiphers, &keys, &ciphers); err != nil { 756 return c.JSON(http.StatusInternalServerError, echo.Map{ 757 "error": err.Error(), 758 }) 759 } 760 olds := make([]interface{}, len(ciphers)) 761 docs := make([]interface{}, len(ciphers)) 762 for i := range ciphers { 763 olds[i] = ciphers[i] 764 cipher := ciphers[i].Clone().(*bitwarden.Cipher) 765 cipher.Metadata.ChangeUpdatedAt() 766 cipher.DeletedDate = nil 767 docs[i] = cipher 768 } 769 if err := couchdb.BulkUpdateDocs(inst, consts.BitwardenCiphers, docs, olds); err != nil { 770 return c.JSON(http.StatusInternalServerError, echo.Map{ 771 "error": err.Error(), 772 }) 773 } 774 775 setting, err := settings.Get(inst) 776 if err != nil { 777 return c.JSON(http.StatusInternalServerError, echo.Map{ 778 "error": err.Error(), 779 }) 780 } 781 _ = settings.UpdateRevisionDate(inst, setting) 782 783 res := &ciphersList{Object: "list"} 784 for i := range docs { 785 cipher := docs[i].(*bitwarden.Cipher) 786 res.Data = append(res.Data, newCipherResponse(cipher, setting)) 787 } 788 return c.JSON(http.StatusOK, res) 789 } 790 791 // https://github.com/bitwarden/jslib/blob/master/common/src/models/request/cipherShareRequest.ts 792 type shareCipherRequest struct { 793 Cipher cipherRequest `json:"cipher"` 794 CollectionIDs []string `json:"collectionIds"` 795 } 796 797 // ShareCipher is used to share a cipher with an organization. 798 func ShareCipher(c echo.Context) error { 799 inst := middlewares.GetInstance(c) 800 if err := middlewares.AllowWholeType(c, permission.PUT, consts.BitwardenCiphers); err != nil { 801 return c.JSON(http.StatusUnauthorized, echo.Map{ 802 "error": "invalid token", 803 }) 804 } 805 806 id := c.Param("id") 807 if id == "" { 808 return c.JSON(http.StatusNotFound, echo.Map{ 809 "error": "missing id", 810 }) 811 } 812 813 old := &bitwarden.Cipher{} 814 if err := couchdb.GetDoc(inst, consts.BitwardenCiphers, id, old); err != nil { 815 if couchdb.IsNotFoundError(err) { 816 return c.JSON(http.StatusNotFound, echo.Map{ 817 "error": "not found", 818 }) 819 } 820 return c.JSON(http.StatusInternalServerError, echo.Map{ 821 "error": err.Error(), 822 }) 823 } 824 825 var req shareCipherRequest 826 if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil { 827 inst.Logger().WithNamespace("bitwarden"). 828 Infof("Bad JSON: %v", err) 829 return c.JSON(http.StatusBadRequest, echo.Map{ 830 "error": "invalid JSON", 831 }) 832 } 833 cipher, err := req.Cipher.toCipher() 834 if err != nil { 835 inst.Logger().WithNamespace("bitwarden"). 836 Infof("Bad cipher: %v", err) 837 return c.JSON(http.StatusBadRequest, echo.Map{ 838 "error": err.Error(), 839 }) 840 } 841 if req.Cipher.OrganizationID == "" { 842 inst.Logger().WithNamespace("bitwarden"). 843 Infof("Bad organization: %v", req) 844 return c.JSON(http.StatusBadRequest, echo.Map{ 845 "error": "organizationId not provided", 846 }) 847 } 848 849 setting, err := settings.Get(inst) 850 if err != nil { 851 return c.JSON(http.StatusInternalServerError, echo.Map{ 852 "error": err.Error(), 853 }) 854 } 855 856 if len(req.CollectionIDs) != 1 { 857 return c.JSON(http.StatusBadRequest, echo.Map{ 858 "error": "only one collection per organization is supported", 859 }) 860 } 861 for _, id := range req.CollectionIDs { 862 if id == setting.CollectionID { 863 cipher.SharedWithCozy = true 864 cipher.OrganizationID = "" 865 cipher.CollectionID = "" 866 } else { 867 cipher.OrganizationID = req.Cipher.OrganizationID 868 cipher.CollectionID = id 869 } 870 } 871 872 if old.Metadata != nil { 873 cipher.Metadata = old.Metadata.Clone() 874 } 875 cipher.Metadata.ChangeUpdatedAt() 876 cipher.SetID(old.ID()) 877 cipher.SetRev(old.Rev()) 878 if err := couchdb.UpdateDocWithOld(inst, cipher, old); err != nil { 879 return c.JSON(http.StatusInternalServerError, echo.Map{ 880 "error": err.Error(), 881 }) 882 } 883 884 _ = settings.UpdateRevisionDate(inst, setting) 885 res := newCipherResponse(cipher, setting) 886 return c.JSON(http.StatusOK, res) 887 } 888 889 // ImportCiphers is used to import ciphers and folders in bulk. 890 func ImportCiphers(c echo.Context) error { 891 inst := middlewares.GetInstance(c) 892 if err := middlewares.AllowWholeType(c, permission.POST, consts.BitwardenCiphers); err != nil { 893 return c.JSON(http.StatusUnauthorized, echo.Map{ 894 "error": "invalid token", 895 }) 896 } 897 898 var req importCipherRequest 899 if err := json.NewDecoder(c.Request().Body).Decode(&req); err != nil { 900 return c.JSON(http.StatusBadRequest, echo.Map{ 901 "error": "invalid JSON", 902 }) 903 } 904 905 // Import the folders 906 folders := make([]interface{}, len(req.Folders)) 907 olds := make([]interface{}, len(req.Folders)) 908 for i, folder := range req.Folders { 909 folders[i] = folder.toFolder() 910 } 911 if err := couchdb.BulkUpdateDocs(inst, consts.BitwardenFolders, folders, olds); err != nil { 912 return c.JSON(http.StatusInternalServerError, echo.Map{ 913 "error": err.Error(), 914 }) 915 } 916 917 // Import the ciphers 918 ciphers := make([]interface{}, len(req.Ciphers)) 919 olds = make([]interface{}, len(req.Ciphers)) 920 for i, cipherReq := range req.Ciphers { 921 cipher, err := cipherReq.toCipher() 922 if err != nil { 923 return c.JSON(http.StatusBadRequest, echo.Map{ 924 "error": err.Error(), 925 }) 926 } 927 for _, kv := range req.FolderRelationships { 928 if kv.Cipher == i && kv.Folder < len(folders) { 929 cipher.FolderID = folders[kv.Folder].(*bitwarden.Folder).ID() 930 } 931 } 932 ciphers[i] = cipher 933 } 934 if err := couchdb.BulkUpdateDocs(inst, consts.BitwardenCiphers, ciphers, olds); err != nil { 935 return c.JSON(http.StatusInternalServerError, echo.Map{ 936 "error": err.Error(), 937 }) 938 } 939 940 // Update the revision date 941 setting, err := settings.Get(inst) 942 if err != nil { 943 return c.JSON(http.StatusInternalServerError, echo.Map{ 944 "error": err.Error(), 945 }) 946 } 947 _ = settings.UpdateRevisionDate(inst, setting) 948 949 // Send in the realtime hub an event to force a sync 950 go func() { 951 time.Sleep(1 * time.Second) 952 payload := couchdb.JSONDoc{ 953 M: map[string]interface{}{ 954 "import": true, 955 }, 956 Type: consts.BitwardenCiphers, 957 } 958 realtime.GetHub().Publish(inst, realtime.EventNotify, &payload, nil) 959 }() 960 961 return c.NoContent(http.StatusOK) 962 }