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  }