Skip to content

Commit

Permalink
signature authentication for public links
Browse files Browse the repository at this point in the history
Implemented the mechanism proposed here: cs3org/cs3apis#110.
The signature authentication is limited to downloads.
  • Loading branch information
David Christofas committed Mar 26, 2021
1 parent fb410ea commit 1446ee5
Show file tree
Hide file tree
Showing 12 changed files with 248 additions and 53 deletions.
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/go-ldap/ldap/v3 v3.2.4
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
16 changes: 13 additions & 3 deletions internal/grpc/services/publicshareprovider/publicshareprovider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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{
Expand Down Expand Up @@ -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
}
Expand All @@ -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{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -552,6 +552,7 @@ 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))
addShare(listContainerR.Infos[i], ls)
}

return listContainerR, nil
Expand Down Expand Up @@ -679,6 +680,7 @@ func (s *service) resolveToken(ctx context.Context, token string) (string, *link
Token: token,
},
},
Sign: true,
},
)
switch {
Expand All @@ -697,6 +699,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
}
40 changes: 33 additions & 7 deletions internal/http/services/owncloud/ocdav/dav.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
}
1 change: 1 addition & 0 deletions internal/http/services/owncloud/ocdav/ocdav.go
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
25 changes: 23 additions & 2 deletions internal/http/services/owncloud/ocdav/propfind.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,11 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"path"
"strconv"
"strings"
"time"

"go.opencensus.io/trace"

Expand Down Expand Up @@ -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
// <oc:privatelink>https://phoenix.owncloud.com/f/9</oc:privatelink>
fallthrough
case "downloadUrl": // desktop
fallthrough
case "dDC": // desktop
fallthrough
case "data-fingerprint": // desktop
Expand Down
35 changes: 33 additions & 2 deletions pkg/auth/manager/publicshares/publicshares.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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:
Expand Down
60 changes: 44 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 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 {
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,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
}
Loading

0 comments on commit 1446ee5

Please sign in to comment.