diff --git a/changelog/unreleased/public-link-signature.md b/changelog/unreleased/public-link-signature.md new file mode 100644 index 00000000000..97432ee3acd --- /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 7300366f0b3..ab17cdbc04f 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/go-ldap/ldap/v3 v3.2.4 diff --git a/go.sum b/go.sum index 7ca92662a6d..b9d66f0932a 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/internal/grpc/services/publicshareprovider/publicshareprovider.go b/internal/grpc/services/publicshareprovider/publicshareprovider.go index a53faf72dbe..0a8c5d2357f 100644 --- a/internal/grpc/services/publicshareprovider/publicshareprovider.go +++ b/internal/grpc/services/publicshareprovider/publicshareprovider.go @@ -21,6 +21,7 @@ package publicshareprovider import ( "context" "fmt" + "time" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" @@ -148,8 +149,17 @@ func (s *service) GetPublicShareByToken(ctx context.Context, req *link.GetPublic log := appctx.GetLogger(ctx) log.Debug().Msg("getting public share by token") + sig := req.GetAuthentication().GetSignature() + auth := publicshare.Authentication{ + Password: req.GetAuthentication().GetPassword(), + Signature: publicshare.Signature{ + Value: sig.GetSignature(), + Expiration: time.Unix(int64(sig.GetSignatureExpiration().GetSeconds()), int64(sig.GetSignatureExpiration().GetNanos())), + }, + } + // there are 2 passes here, and the second request has no password - found, err := s.sm.GetPublicShareByToken(ctx, req.GetToken(), req.GetPassword()) + found, err := s.sm.GetPublicShareByToken(ctx, req.GetToken(), auth, req.GetSign()) switch v := err.(type) { case nil: return &link.GetPublicShareByTokenResponse{ @@ -180,7 +190,7 @@ func (s *service) GetPublicShare(ctx context.Context, req *link.GetPublicShareRe log.Error().Msg("error getting user from context") } - found, err := s.sm.GetPublicShare(ctx, u, req.Ref) + found, err := s.sm.GetPublicShare(ctx, u, req.Ref, req.GetSign()) if err != nil { return nil, err } @@ -196,7 +206,7 @@ func (s *service) ListPublicShares(ctx context.Context, req *link.ListPublicShar log.Info().Str("publicshareprovider", "list").Msg("list public share") user, _ := user.ContextGetUser(ctx) - shares, err := s.sm.ListPublicShares(ctx, user, req.Filters, &provider.ResourceInfo{}) + shares, err := s.sm.ListPublicShares(ctx, user, req.Filters, &provider.ResourceInfo{}, req.GetSign()) if err != nil { log.Err(err).Msg("error listing shares") return &link.ListPublicSharesResponse{ diff --git a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go index e5560eeeb5e..66c36780273 100644 --- a/internal/grpc/services/publicstorageprovider/publicstorageprovider.go +++ b/internal/grpc/services/publicstorageprovider/publicstorageprovider.go @@ -552,6 +552,9 @@ func (s *service) ListContainer(ctx context.Context, req *provider.ListContainer for i := range listContainerR.Infos { filterPermissions(listContainerR.Infos[i].PermissionSet, ls.GetPermissions().Permissions) listContainerR.Infos[i].Path = path.Join(s.mountPath, "/", tkn, relativePath, path.Base(listContainerR.Infos[i].Path)) + if err := addShare(listContainerR.Infos[i], ls); err != nil { + appctx.GetLogger(ctx).Error().Err(err).Interface("share", ls).Interface("info", listContainerR.Infos[i]).Msg("error when adding share") + } } return listContainerR, nil @@ -679,6 +682,7 @@ func (s *service) resolveToken(ctx context.Context, token string) (string, *link Token: token, }, }, + Sign: true, }, ) switch { @@ -697,6 +701,5 @@ func (s *service) resolveToken(ctx context.Context, token string) (string, *link case pathRes.Status.Code != rpc.Code_CODE_OK: return "", nil, pathRes.Status, nil } - return pathRes.Path, publicShareResponse.GetShare(), nil, nil } diff --git a/internal/http/services/owncloud/ocdav/dav.go b/internal/http/services/owncloud/ocdav/dav.go index e3d17a7cafe..3412684dcf4 100644 --- a/internal/http/services/owncloud/ocdav/dav.go +++ b/internal/http/services/owncloud/ocdav/dav.go @@ -171,16 +171,22 @@ func (h *DavHandler) Handler(s *svc) http.Handler { w.WriteHeader(http.StatusNotFound) } - _, pass, _ := r.BasicAuth() + var res *gatewayv1beta1.AuthenticateResponse token, _ := router.ShiftPath(r.URL.Path) - - authenticateRequest := gatewayv1beta1.AuthenticateRequest{ - Type: "publicshares", - ClientId: token, - ClientSecret: pass, + if _, pass, ok := r.BasicAuth(); ok { + res, err = handleBasicAuth(r.Context(), c, token, pass) + } else { + // We restrict the pre-signed urls to downloads. + if r.Method != http.MethodGet { + w.WriteHeader(http.StatusUnauthorized) + return + } + q := r.URL.Query() + sig := q.Get("signature") + expiration := q.Get("expiration") + res, err = handleSignatureAuth(r.Context(), c, token, sig, expiration) } - res, err := c.Authenticate(r.Context(), &authenticateRequest) switch { case err != nil: w.WriteHeader(http.StatusInternalServerError) @@ -247,3 +253,23 @@ func getTokenStatInfo(ctx context.Context, client gatewayv1beta1.GatewayAPIClien Spec: &provider.Reference_Path{Path: path.Join("/public", token)}, }}) } + +func handleBasicAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, token, pw string) (*gatewayv1beta1.AuthenticateResponse, error) { + authenticateRequest := gatewayv1beta1.AuthenticateRequest{ + Type: "publicshares", + ClientId: token, + ClientSecret: "password|" + pw, + } + + return c.Authenticate(ctx, &authenticateRequest) +} + +func handleSignatureAuth(ctx context.Context, c gatewayv1beta1.GatewayAPIClient, token, sig, expiration string) (*gatewayv1beta1.AuthenticateResponse, error) { + authenticateRequest := gatewayv1beta1.AuthenticateRequest{ + Type: "publicshares", + ClientId: token, + ClientSecret: "signature|" + sig + "|" + expiration, + } + + return c.Authenticate(ctx, &authenticateRequest) +} diff --git a/internal/http/services/owncloud/ocdav/ocdav.go b/internal/http/services/owncloud/ocdav/ocdav.go index f73ae249440..c2b7561cf2b 100644 --- a/internal/http/services/owncloud/ocdav/ocdav.go +++ b/internal/http/services/owncloud/ocdav/ocdav.go @@ -72,6 +72,7 @@ type Config struct { GatewaySvc string `mapstructure:"gatewaysvc"` Timeout int64 `mapstructure:"timeout"` Insecure bool `mapstructure:"insecure"` + PublicURL string `mapstructure:"public_url"` } func (c *Config) init() { diff --git a/internal/http/services/owncloud/ocdav/propfind.go b/internal/http/services/owncloud/ocdav/propfind.go index 7e880c952a3..71eb313918b 100644 --- a/internal/http/services/owncloud/ocdav/propfind.go +++ b/internal/http/services/owncloud/ocdav/propfind.go @@ -26,9 +26,11 @@ import ( "fmt" "io" "net/http" + "net/url" "path" "strconv" "strings" + "time" "go.opencensus.io/trace" @@ -660,11 +662,30 @@ func (s *svc) mdToPropResponse(ctx context.Context, pf *propfindXML, md *provide } else { propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:owner-display-name", "")) } + case "downloadURL": // desktop + if isPublic && md.Type == provider.ResourceType_RESOURCE_TYPE_FILE { + var path string + if !ls.PasswordProtected { + path = md.Path + } else { + expiration := time.Unix(int64(ls.Signature.SignatureExpiration.Seconds), int64(ls.Signature.SignatureExpiration.Nanos)) + var sb strings.Builder + + sb.WriteString(md.Path) + sb.WriteString("?signature=") + sb.WriteString(ls.Signature.Signature) + sb.WriteString("&expiration=") + sb.WriteString(url.QueryEscape(expiration.Format(time.RFC3339))) + + path = sb.String() + } + propstatOK.Prop = append(propstatOK.Prop, s.newProp("oc:downloadURL", s.c.PublicURL+baseURI+path)) + } else { + propstatNotFound.Prop = append(propstatNotFound.Prop, s.newProp("oc:"+pf.Prop[i].Local, "")) + } case "privatelink": // phoenix only // https://phoenix.owncloud.com/f/9 fallthrough - case "downloadUrl": // desktop - fallthrough case "dDC": // desktop fallthrough case "data-fingerprint": // desktop diff --git a/pkg/auth/manager/publicshares/publicshares.go b/pkg/auth/manager/publicshares/publicshares.go index b6a674b1d30..fdffdaf5a15 100644 --- a/pkg/auth/manager/publicshares/publicshares.go +++ b/pkg/auth/manager/publicshares/publicshares.go @@ -20,11 +20,14 @@ package publicshares import ( "context" + "strings" + "time" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" userprovider "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" rpcv1beta1 "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" "github.com/cs3org/reva/pkg/auth" "github.com/cs3org/reva/pkg/auth/manager/registry" "github.com/cs3org/reva/pkg/errtypes" @@ -72,9 +75,37 @@ func (m *manager) Authenticate(ctx context.Context, token, secret string) (*user return nil, err } + var auth *link.PublicShareAuthentication + if strings.HasPrefix(secret, "password|") { + secret = strings.TrimPrefix(secret, "password|") + auth = &link.PublicShareAuthentication{ + Spec: &link.PublicShareAuthentication_Password{ + Password: secret, + }, + } + } else if strings.HasPrefix(secret, "signature|") { + secret = strings.TrimPrefix(secret, "signature|") + parts := strings.Split(secret, "|") + sig, expiration := parts[0], parts[1] + exp, _ := time.Parse(time.RFC3339, expiration) + + auth = &link.PublicShareAuthentication{ + Spec: &link.PublicShareAuthentication_Signature{ + Signature: &link.ShareSignature{ + Signature: sig, + SignatureExpiration: &typesv1beta1.Timestamp{ + Seconds: uint64(exp.UnixNano() / 1000000000), + Nanos: uint32(exp.UnixNano() % 1000000000), + }, + }, + }, + } + } + publicShareResponse, err := gwConn.GetPublicShareByToken(ctx, &link.GetPublicShareByTokenRequest{ - Token: token, - Password: secret, + Token: token, + Authentication: auth, + Sign: true, }) switch { case err != nil: diff --git a/pkg/cbox/publicshare/sql/sql.go b/pkg/cbox/publicshare/sql/sql.go index cee22c90139..14eb17c9a67 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 publicshare.Authentication, 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,18 @@ 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 publicshare.Authentication) bool { + switch { + case auth.Password != "": + return checkPasswordHash(auth.Password, pw) + case auth.Signature != publicshare.Signature{}: + now := time.Now() + if now.After(auth.Signature.Expiration) { + return false + } + sig := publicshare.CreateSignature(share.Token, pw, auth.Signature.Expiration) + return auth.Signature.Value == sig + } + return false +} diff --git a/pkg/publicshare/manager/json/json.go b/pkg/publicshare/manager/json/json.go index a07728addca..efc1cae0098 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 publicshare.Authentication, 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,23 @@ func (m *manager) writeDb(db map[string]interface{}) error { return nil } +func authenticate(share *link.PublicShare, pw string, auth publicshare.Authentication) bool { + switch { + case auth.Password != "": + if err := bcrypt.CompareHashAndPassword([]byte(pw), []byte(auth.Password)); err == nil { + return true + } + case auth.Signature != publicshare.Signature{}: + now := time.Now() + if now.After(auth.Signature.Expiration) { + return false + } + sig := publicshare.CreateSignature(share.Token, pw, auth.Signature.Expiration) + return auth.Signature.Value == sig + } + return false +} + type publicShare struct { link.PublicShare Password string `json:"password"` diff --git a/pkg/publicshare/manager/memory/memory.go b/pkg/publicshare/manager/memory/memory.go index 22ac44be7ec..7a6134e3d12 100644 --- a/pkg/publicshare/manager/memory/memory.go +++ b/pkg/publicshare/manager/memory/memory.go @@ -105,7 +105,7 @@ func (m *manager) CreatePublicShare(ctx context.Context, u *user.User, rInfo *pr // UpdatePublicShare updates the expiration date, permissions and Mtime 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") } @@ -144,12 +144,12 @@ func (m *manager) UpdatePublicShare(ctx context.Context, u *user.User, req *link return share, nil } -func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) (share *link.PublicShare, err error) { +func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (share *link.PublicShare, err error) { // TODO(refs) return an error if the share is expired. // Attempt to fetch public share by token if ref.GetToken() != "" { - share, err = m.GetPublicShareByToken(ctx, ref.GetToken(), "") + share, err = m.GetPublicShareByToken(ctx, ref.GetToken(), publicshare.Authentication{}, sign) if err != nil { return nil, errors.New("no shares found by token") } @@ -166,7 +166,7 @@ func (m *manager) GetPublicShare(ctx context.Context, u *user.User, ref *link.Pu return } -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) { // TODO(refs) filter out expired shares shares := []*link.PublicShare{} m.shares.Range(func(k, v interface{}) bool { @@ -202,7 +202,7 @@ func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link } m.shares.Delete(s.Token) case ref.GetToken() != "": - if _, err := m.GetPublicShareByToken(ctx, ref.GetToken(), ""); err != nil { + if _, err := m.GetPublicShareByToken(ctx, ref.GetToken(), publicshare.Authentication{}, false); err != nil { return errors.New("reference does not exist") } m.shares.Delete(ref.GetToken()) @@ -212,7 +212,7 @@ func (m *manager) RevokePublicShare(ctx context.Context, u *user.User, ref *link return nil } -func (m *manager) GetPublicShareByToken(ctx context.Context, token string, password string) (*link.PublicShare, error) { +func (m *manager) GetPublicShareByToken(ctx context.Context, token string, auth publicshare.Authentication, sign bool) (*link.PublicShare, error) { if ps, ok := m.shares.Load(token); ok { return ps.(*link.PublicShare), nil } diff --git a/pkg/publicshare/publicshare.go b/pkg/publicshare/publicshare.go index 7a328c5598b..6e7f5bb6682 100644 --- a/pkg/publicshare/publicshare.go +++ b/pkg/publicshare/publicshare.go @@ -20,18 +20,66 @@ package publicshare import ( "context" + "crypto/hmac" + "crypto/sha256" + "crypto/sha512" + "encoding/hex" + "time" user "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1" link "github.com/cs3org/go-cs3apis/cs3/sharing/link/v1beta1" provider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + typesv1beta1 "github.com/cs3org/go-cs3apis/cs3/types/v1beta1" ) // Manager manipulates public shares. type Manager interface { CreatePublicShare(ctx context.Context, u *user.User, md *provider.ResourceInfo, g *link.Grant) (*link.PublicShare, error) UpdatePublicShare(ctx context.Context, u *user.User, req *link.UpdatePublicShareRequest, g *link.Grant) (*link.PublicShare, error) - GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) (*link.PublicShare, error) - ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo) ([]*link.PublicShare, error) + GetPublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference, sign bool) (*link.PublicShare, error) + ListPublicShares(ctx context.Context, u *user.User, filters []*link.ListPublicSharesRequest_Filter, md *provider.ResourceInfo, sign bool) ([]*link.PublicShare, error) RevokePublicShare(ctx context.Context, u *user.User, ref *link.PublicShareReference) error - GetPublicShareByToken(ctx context.Context, token, password string) (*link.PublicShare, error) + GetPublicShareByToken(ctx context.Context, token string, auth Authentication, sign bool) (*link.PublicShare, error) +} + +// Authentication bundles the different authentication options for public shares +type Authentication struct { + Password string + Signature Signature +} + +// Signature contains the required parameters for signature authentication +type Signature struct { + Value string + Expiration time.Time +} + +// CreateSignature calculates a signature for a public share. +func CreateSignature(token, pw string, expiration time.Time) string { + h := sha256.New() + h.Write([]byte(pw)) + key := make([]byte, 0, 32) + key = h.Sum(key) + + mac := hmac.New(sha512.New512_256, key) + mac.Write([]byte(token + "|" + expiration.Format(time.RFC3339))) + + sig := make([]byte, 0, 32) + sig = mac.Sum(sig) + + return hex.EncodeToString(sig) +} + +// AddSignature augments a public share with a signature. +// The signature has a validity of 30 minutes. +func AddSignature(share *link.PublicShare, pw string) { + expiration := time.Now().Add(time.Minute * 30) + sig := CreateSignature(share.Token, pw, expiration) + share.Signature = &link.ShareSignature{ + Signature: sig, + SignatureExpiration: &typesv1beta1.Timestamp{ + Seconds: uint64(expiration.UnixNano() / 1000000000), + Nanos: uint32(expiration.UnixNano() % 1000000000), + }, + } }