diff --git a/changelog/unreleased/public-link-signature.md b/changelog/unreleased/public-link-signature.md new file mode 100644 index 0000000000..97432ee3ac --- /dev/null +++ b/changelog/unreleased/public-link-signature.md @@ -0,0 +1,8 @@ +Enhancement: Add signature authentication for public links + +Implemented signature authentication for public links in addition to the existing password authentication. +This allows web clients to efficiently download files from password protected public shares. + +https://github.com/cs3org/cs3apis/issues/110 +https://github.com/cs3org/reva/pull/1590 + diff --git a/go.mod b/go.mod index 010667df59..d70b472514 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/cheggaaa/pb v1.0.29 github.com/coreos/go-oidc v2.2.1+incompatible github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e - github.com/cs3org/go-cs3apis v0.0.0-20210322124405-872bbbf14d0b + github.com/cs3org/go-cs3apis v0.0.0-20210325133324-32b03d75a535 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/eventials/go-tus v0.0.0-20200718001131-45c7ec8f5d59 github.com/ffurano/grpc-proto v0.0.0-20210312134900-65801a1ca184 diff --git a/go.sum b/go.sum index 3d0b1e37fd..8e193dc92b 100644 --- a/go.sum +++ b/go.sum @@ -144,6 +144,8 @@ github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e h1:tqSPWQeueWTKnJVMJff github.com/cs3org/cato v0.0.0-20200828125504-e418fc54dd5e/go.mod h1:XJEZ3/EQuI3BXTp/6DUzFr850vlxq11I6satRtz0YQ4= github.com/cs3org/go-cs3apis v0.0.0-20210322124405-872bbbf14d0b h1:80DK9Yufaj1YJ0fPb6x1WZfijHWA+CMstq3MEZs/8To= github.com/cs3org/go-cs3apis v0.0.0-20210322124405-872bbbf14d0b/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= +github.com/cs3org/go-cs3apis v0.0.0-20210325133324-32b03d75a535 h1:555D8A3ddKqb4OyK9v5mdphw2zDLWKGXOkcnf1RQwTA= +github.com/cs3org/go-cs3apis v0.0.0-20210325133324-32b03d75a535/go.mod h1:UXha4TguuB52H14EMoSsCqDj7k8a/t7g4gVP+bgY5LY= github.com/cucumber/godog v0.8.1/go.mod h1:vSh3r/lM+psC1BPXvdkSEuNjmXfpVqrMGYAElF6hxnA= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= diff --git a/pkg/cbox/publicshare/sql/sql.go b/pkg/cbox/publicshare/sql/sql.go index cee22c9013..033b3acb9e 100644 --- a/pkg/cbox/publicshare/sql/sql.go +++ b/pkg/cbox/publicshare/sql/sql.go @@ -247,42 +247,43 @@ func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link return nil, err } - return m.GetPublicShare(ctx, u, req.Ref) + return m.GetPublicShare(ctx, u, req.Ref, false) } -func (m *manager) getByToken(ctx context.Context, token string, u *user.User) (*link.PublicShare, error) { +func (m *manager) getByToken(ctx context.Context, token string, u *user.User) (*link.PublicShare, string, error) { s := conversions.DBShare{Token: token} query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE share_type=? AND token=?" if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil { if err == sql.ErrNoRows { - return nil, errtypes.NotFound(token) + return nil, "", errtypes.NotFound(token) } - return nil, err + return nil, "", err } - return conversions.ConvertToCS3PublicShare(s), nil + return conversions.ConvertToCS3PublicShare(s), s.ShareWith, nil } -func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.User) (*link.PublicShare, error) { +func (m *manager) getByID(ctx context.Context, id *link.PublicShareId, u *user.User) (*link.PublicShare, string, error) { uid := conversions.FormatUserID(u.Id) s := conversions.DBShare{ID: id.OpaqueId} query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, stime, permissions FROM oc_share WHERE share_type=? AND id=? AND (uid_owner=? OR uid_initiator=?)" if err := m.db.QueryRow(query, publicShareType, id.OpaqueId, uid, uid).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.Token, &s.Expiration, &s.ShareName, &s.STime, &s.Permissions); err != nil { if err == sql.ErrNoRows { - return nil, errtypes.NotFound(id.OpaqueId) + return nil, "", errtypes.NotFound(id.OpaqueId) } - return nil, err + return nil, "", err } - return conversions.ConvertToCS3PublicShare(s), nil + return conversions.ConvertToCS3PublicShare(s), s.ShareWith, nil } -func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) (*link.PublicShare, error) { +func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) { var s *link.PublicShare + var pw string var err error switch { case ref.GetId() != nil: - s, err = m.getByID(ctx, ref.GetId(), u) + s, pw, err = m.getByID(ctx, ref.GetId(), u) case ref.GetToken() != "": - s, err = m.getByToken(ctx, ref.GetToken(), u) + s, pw, err = m.getByToken(ctx, ref.GetToken(), u) default: err = errtypes.NotFound(ref.String()) } @@ -297,10 +298,14 @@ func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.Pu return nil, errtypes.NotFound(ref.String()) } + if s.PasswordProtected && sign { + publicshare.AddSignature(s, pw) + } + return s, nil } -func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo) ([]*link.PublicShare, error) { +func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo, sign bool) ([]*link.PublicShare, error) { uid := conversions.FormatUserID(u.Id) query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(token,'') as token, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE (uid_owner=? or uid_initiator=?) AND (share_type=?)" var filterQuery string @@ -348,6 +353,9 @@ func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters [] if expired(cs3Share) { _ = m.cleanupExpiredShares() } else { + if cs3Share.PasswordProtected && sign { + publicshare.AddSignature(cs3Share, s.ShareWith) + } shares = append(shares, cs3Share) } } @@ -393,7 +401,7 @@ func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link return nil } -func (m *manager) GetPublicShareByToken(ctx context.Context, token, password string) (*link.PublicShare, error) { +func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) { s := conversions.DBShare{Token: token} query := "select coalesce(uid_owner, '') as uid_owner, coalesce(uid_initiator, '') as uid_initiator, coalesce(share_with, '') as share_with, coalesce(fileid_prefix, '') as fileid_prefix, coalesce(item_source, '') as item_source, coalesce(expiration, '') as expiration, coalesce(share_name, '') as share_name, id, stime, permissions FROM oc_share WHERE share_type=? AND token=?" if err := m.db.QueryRow(query, publicShareType, token).Scan(&s.UIDOwner, &s.UIDInitiator, &s.ShareWith, &s.Prefix, &s.ItemSource, &s.Expiration, &s.ShareName, &s.ID, &s.STime, &s.Permissions); err != nil { @@ -402,13 +410,18 @@ func (m *manager) GetPublicShareByToken(ctx context.Context, token, password str } return nil, err } + cs3Share := conversions.ConvertToCS3PublicShare(s) if s.ShareWith != "" { - if check := checkPasswordHash(password, s.ShareWith); !check { + if !authenticate(cs3Share, s.ShareWith, auth) { + // if check := checkPasswordHash(auth.Password, s.ShareWith); !check { return nil, errtypes.InvalidCredentials(token) } + + if sign { + publicshare.AddSignature(cs3Share, s.ShareWith) + } } - cs3Share := conversions.ConvertToCS3PublicShare(s) if expired(cs3Share) { if err := m.cleanupExpiredShares(); err != nil { return nil, err @@ -455,3 +468,20 @@ func checkPasswordHash(password, hash string) bool { err := bcrypt.CompareHashAndPassword([]byte(strings.TrimPrefix(hash, "1|")), []byte(password)) return err == nil } + +func authenticate(share *link.PublicShare, pw string, auth *link.PublicShareAuthentication) bool { + switch { + case auth.GetPassword() != "": + return checkPasswordHash(auth.GetPassword(), pw) + case auth.GetSignature() != nil: + sig := auth.GetSignature() + now := time.Now() + expiration := time.Unix(int64(sig.GetSignatureExpiration().GetSeconds()), int64(sig.GetSignatureExpiration().GetNanos())) + if now.After(expiration) { + return false + } + s := publicshare.CreateSignature(share.Token, pw, expiration) + return sig.GetSignature() == s + } + return false +} diff --git a/pkg/publicshare/manager/json/json.go b/pkg/publicshare/manager/json/json.go index a07728addc..eb2e17aa4b 100644 --- a/pkg/publicshare/manager/json/json.go +++ b/pkg/publicshare/manager/json/json.go @@ -222,7 +222,7 @@ func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *pr // UpdatePublicShare updates the public share func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest, g *link.Grant) (*link.PublicShare, error) { log := appctx.GetLogger(ctx) - share, err := m.GetPublicShare(ctx, u, req.Ref) + share, err := m.GetPublicShare(ctx, u, req.Ref, false) if err != nil { return nil, errors.New("ref does not exist") } @@ -301,12 +301,15 @@ func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link } // GetPublicShare gets a public share either by ID or Token. -func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) (*link.PublicShare, error) { +func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) { if ref.GetToken() != "" { - ps, err := m.getByToken(ctx, ref.GetToken()) + ps, pw, err := m.getByToken(ctx, ref.GetToken()) if err != nil { return nil, errors.New("no shares found by token") } + if ps.PasswordProtected && sign { + publicshare.AddSignature(ps, pw) + } return ps, nil } @@ -320,6 +323,7 @@ func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.Pu for _, v := range db { d := v.(map[string]interface{})["share"] + passDB := v.(map[string]interface{})["password"].(string) var ps link.PublicShare if err := utils.UnmarshalJSONToProtoV1([]byte(d.(string)), &ps); err != nil { @@ -333,6 +337,9 @@ func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.Pu } return nil, errors.New("no shares found by id:" + ref.GetId().String()) } + if ps.PasswordProtected && sign { + publicshare.AddSignature(&ps, passDB) + } return &ps, nil } @@ -341,7 +348,7 @@ func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.Pu } // ListPublicShares retrieves all the shares on the manager that are valid. -func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo) ([]*link.PublicShare, error) { +func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo, sign bool) ([]*link.PublicShare, error) { var shares []*link.PublicShare m.mutex.Lock() @@ -363,6 +370,10 @@ func (m *manager) ListPublicShares(ctx context.Context, u *user.User, filters [] continue } + if local.PublicShare.PasswordProtected && sign { + publicshare.AddSignature(&local.PublicShare, local.Password) + } + if len(filters) == 0 { shares = append(shares, &local.PublicShare) } else { @@ -457,7 +468,7 @@ func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link return errors.New("reference does not exist") } case ref.GetToken() != "": - share, err := m.getByToken(ctx, ref.GetToken()) + share, _, err := m.getByToken(ctx, ref.GetToken()) if err != nil { return err } @@ -471,10 +482,10 @@ func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link return m.writeDb(db) } -func (m *manager) getByToken(ctx context.Context, token string) (*link.PublicShare, error) { +func (m *manager) getByToken(ctx context.Context, token string) (*link.PublicShare, string, error) { db, err := m.readDb() if err != nil { - return nil, err + return nil, "", err } m.mutex.Lock() @@ -483,19 +494,20 @@ func (m *manager) getByToken(ctx context.Context, token string) (*link.PublicSha for _, v := range db { var local link.PublicShare if err := utils.UnmarshalJSONToProtoV1([]byte(v.(map[string]interface{})["share"].(string)), &local); err != nil { - return nil, err + return nil, "", err } if local.Token == token { - return &local, nil + passDB := v.(map[string]interface{})["password"].(string) + return &local, passDB, nil } } - return nil, fmt.Errorf("share with token: `%v` not found", token) + return nil, "", fmt.Errorf("share with token: `%v` not found", token) } // GetPublicShareByToken gets a public share by its opaque token. -func (m *manager) GetPublicShareByToken(ctx context.Context, token, password string) (*link.PublicShare, error) { +func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth *link.PublicShareAuthentication, sign bool) (*link.PublicShare, error) { db, err := m.readDb() if err != nil { return nil, err @@ -521,7 +533,10 @@ func (m *manager) GetPublicShareByToken(ctx context.Context, token, password str } if local.PasswordProtected { - if err := bcrypt.CompareHashAndPassword([]byte(passDB), []byte(password)); err == nil { + if authenticate(&local, passDB, auth) { + if sign { + publicshare.AddSignature(&local, passDB) + } return &local, nil } @@ -559,6 +574,25 @@ func (m *manager) writeDb(db map[string]interface{}) error { return nil } +func authenticate(share *link.PublicShare, pw string, auth *link.PublicShareAuthentication) bool { + switch { + case auth.GetPassword() != "": + if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(auth.GetPassword())); err == nil { + return true + } + case auth.GetSignature() != nil: + sig := auth.GetSignature() + now := time.Now() + expiration := time.Unix(int64(sig.GetSignatureExpiration().GetSeconds()), int64(sig.GetSignatureExpiration().GetNanos())) + if now.After(expiration) { + return false + } + s := publicshare.CreateSignature(share.Token, pw, expiration) + return sig.GetSignature() == s + } + return false +} + type publicShare struct { link.PublicShare Password string `json:"password"`