Skip to content

Commit

Permalink
signature authentication for public links (cs3org#1590)
Browse files Browse the repository at this point in the history
  • Loading branch information
C0rby authored and root committed Apr 20, 2021
1 parent 374d671 commit ab6ee18
Show file tree
Hide file tree
Showing 5 changed files with 103 additions and 29 deletions.
8 changes: 8 additions & 0 deletions changelog/unreleased/public-link-signature.md
Original file line number Diff line number Diff line change
@@ -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

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down
62 changes: 46 additions & 16 deletions pkg/cbox/publicshare/sql/sql.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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
Expand Down Expand Up @@ -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
}
58 changes: 46 additions & 12 deletions pkg/publicshare/manager/json/json.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand Down Expand Up @@ -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
}

Expand All @@ -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 {
Expand All @@ -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
}

Expand All @@ -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()
Expand All @@ -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 {
Expand Down Expand Up @@ -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
}
Expand All @@ -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()
Expand All @@ -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
Expand All @@ -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
}

Expand Down Expand Up @@ -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"`
Expand Down

0 comments on commit ab6ee18

Please sign in to comment.