1// Copyright (C) 2020 Storj Labs, Inc. 2// See LICENSE for copying information. 3 4package uplink 5 6import ( 7 "context" 8 "errors" 9 "strings" 10 "time" 11 12 "github.com/zeebo/errs" 13 14 "storj.io/common/encryption" 15 "storj.io/common/macaroon" 16 "storj.io/common/paths" 17 "storj.io/common/rpc" 18 "storj.io/common/storj" 19 "storj.io/uplink/private/access2" 20 "storj.io/uplink/private/metainfo" 21) 22 23// An Access Grant contains everything to access a project and specific buckets. 24// It includes a potentially-restricted API Key, a potentially-restricted set 25// of encryption information, and information about the Satellite responsible 26// for the project's metadata. 27type Access struct { 28 satelliteURL storj.NodeURL 29 apiKey *macaroon.APIKey 30 encAccess *access2.EncryptionAccess 31} 32 33// getAPIKey are exposing the state do private methods. 34// 35// NB: this is used with linkname in internal/expose. 36// It needs to be updated when this is updated. 37// 38//lint:ignore U1000, used with linkname 39//nolint: unused 40func (access *Access) getAPIKey() *macaroon.APIKey { return access.apiKey } 41 42// getEncAccess are exposing the state do private methods. 43// 44// NB: this is used with linkname in internal/expose. 45// It needs to be updated when this is updated. 46// 47//lint:ignore U1000, used with linkname 48//nolint: unused 49func (access *Access) getEncAccess() *access2.EncryptionAccess { return access.encAccess } 50 51// SharePrefix defines a prefix that will be shared. 52type SharePrefix struct { 53 Bucket string 54 // Prefix is the prefix of the shared object keys. 55 // 56 // Note: that within a bucket, the hierarchical key derivation scheme is 57 // delineated by forward slashes (/), so encryption information will be 58 // included in the resulting access grant to decrypt any key that shares 59 // the same prefix up until the last slash. 60 Prefix string 61} 62 63// Permission defines what actions can be used to share. 64type Permission struct { 65 // AllowDownload gives permission to download the object's content. It 66 // allows getting object metadata, but it does not allow listing buckets. 67 AllowDownload bool 68 // AllowUpload gives permission to create buckets and upload new objects. 69 // It does not allow overwriting existing objects unless AllowDelete is 70 // granted too. 71 AllowUpload bool 72 // AllowList gives permission to list buckets. It allows getting object 73 // metadata, but it does not allow downloading the object's content. 74 AllowList bool 75 // AllowDelete gives permission to delete buckets and objects. Unless 76 // either AllowDownload or AllowList is granted too, no object metadata and 77 // no error info will be returned for deleted objects. 78 AllowDelete bool 79 // NotBefore restricts when the resulting access grant is valid for. 80 // If set, the resulting access grant will not work if the Satellite 81 // believes the time is before NotBefore. 82 // If set, this value should always be before NotAfter. 83 NotBefore time.Time 84 // NotAfter restricts when the resulting access grant is valid for. 85 // If set, the resulting access grant will not work if the Satellite 86 // believes the time is after NotAfter. 87 // If set, this value should always be after NotBefore. 88 NotAfter time.Time 89} 90 91// ParseAccess parses a serialized access grant string. 92// 93// This should be the main way to instantiate an access grant for opening a project. 94// See the note on RequestAccessWithPassphrase. 95func ParseAccess(access string) (*Access, error) { 96 inner, err := access2.ParseAccess(access) 97 if err != nil { 98 return nil, packageError.Wrap(err) 99 } 100 101 satelliteURL, err := parseNodeURL(inner.SatelliteAddress) 102 if err != nil { 103 return nil, packageError.Wrap(err) 104 } 105 106 return &Access{ 107 satelliteURL: satelliteURL, 108 apiKey: inner.APIKey, 109 encAccess: inner.EncAccess, 110 }, nil 111} 112 113// SatelliteAddress returns the satellite node URL for this access grant. 114func (access *Access) SatelliteAddress() string { 115 return access.satelliteURL.String() 116} 117 118// Serialize serializes an access grant such that it can be used later with 119// ParseAccess or other tools. 120func (access *Access) Serialize() (string, error) { 121 inner := access2.Access{ 122 SatelliteAddress: access.satelliteURL.String(), 123 APIKey: access.apiKey, 124 EncAccess: access.encAccess, 125 } 126 return inner.Serialize() 127} 128 129// RequestAccessWithPassphrase generates a new access grant using a passhprase. 130// It must talk to the Satellite provided to get a project-based salt for 131// deterministic key derivation. 132// 133// Note: this is a CPU-heavy function that uses a password-based key derivation function 134// (Argon2). This should be a setup-only step. Most common interactions with the library 135// should be using a serialized access grant through ParseAccess directly. 136func RequestAccessWithPassphrase(ctx context.Context, satelliteAddress, apiKey, passphrase string) (*Access, error) { 137 return (Config{}).RequestAccessWithPassphrase(ctx, satelliteAddress, apiKey, passphrase) 138} 139 140// RequestAccessWithPassphrase generates a new access grant using a passhprase. 141// It must talk to the Satellite provided to get a project-based salt for 142// deterministic key derivation. 143// 144// Note: this is a CPU-heavy function that uses a password-based key derivation function 145// (Argon2). This should be a setup-only step. Most common interactions with the library 146// should be using a serialized access grant through ParseAccess directly. 147func (config Config) RequestAccessWithPassphrase(ctx context.Context, satelliteAddress, apiKey, passphrase string) (*Access, error) { 148 return config.requestAccessWithPassphraseAndConcurrency(ctx, satelliteAddress, apiKey, passphrase, 8) 149} 150 151// requestAccessWithPassphraseAndConcurrency requests satellite for a new access grant using a passhprase and specific concurrency for the Argon2 key derivation. 152// 153// NB: this is used with linkname in internal/expose. 154// It needs to be updated when this is updated. 155func (config Config) requestAccessWithPassphraseAndConcurrency(ctx context.Context, satelliteAddress, apiKey, passphrase string, concurrency uint8) (_ *Access, err error) { 156 parsedAPIKey, err := macaroon.ParseAPIKey(apiKey) 157 if err != nil { 158 return nil, packageError.Wrap(err) 159 } 160 161 satelliteURL, err := parseNodeURL(satelliteAddress) 162 if err != nil { 163 return nil, packageError.Wrap(err) 164 } 165 166 dialer, err := config.getDialer(ctx) 167 if err != nil { 168 return nil, packageError.Wrap(err) 169 } 170 defer func() { err = errs.Combine(err, dialer.Pool.Close()) }() 171 172 metainfo, err := metainfo.DialNodeURL(ctx, dialer, satelliteURL.String(), parsedAPIKey, config.UserAgent) 173 if err != nil { 174 return nil, packageError.Wrap(err) 175 } 176 defer func() { err = errs.Combine(err, metainfo.Close()) }() 177 178 info, err := metainfo.GetProjectInfo(ctx) 179 if err != nil { 180 return nil, convertKnownErrors(err, "", "") 181 } 182 183 key, err := encryption.DeriveRootKey([]byte(passphrase), info.ProjectSalt, "", concurrency) 184 if err != nil { 185 return nil, packageError.Wrap(err) 186 } 187 188 encAccess := access2.NewEncryptionAccessWithDefaultKey(key) 189 encAccess.SetDefaultPathCipher(storj.EncAESGCM) 190 encAccess.LimitTo(parsedAPIKey) 191 192 return &Access{ 193 satelliteURL: satelliteURL, 194 apiKey: parsedAPIKey, 195 encAccess: encAccess, 196 }, nil 197} 198 199// parseNodeURL parses the address into a storj.NodeURL adding the node id if necessary 200// for known addresses. 201func parseNodeURL(address string) (storj.NodeURL, error) { 202 nodeURL, err := storj.ParseNodeURL(address) 203 if err != nil { 204 return nodeURL, packageError.Wrap(err) 205 } 206 207 // Node id is required in satelliteNodeID for all unknown (non-storj) satellites. 208 // For known satellite it will be automatically prepended. 209 if nodeURL.ID.IsZero() { 210 nodeID, found := rpc.KnownNodeID(nodeURL.Address) 211 if !found { 212 return nodeURL, packageError.New("node id is required in satelliteNodeURL") 213 } 214 nodeURL.ID = nodeID 215 } 216 217 return nodeURL, nil 218} 219 220// Share creates a new access grant with specific permissions. 221// 222// Access grants can only have their existing permissions restricted, 223// and the resulting access grant will only allow for the intersection of all previous 224// Share calls in the access grant construction chain. 225// 226// Prefixes, if provided, restrict the access grant (and internal encryption information) 227// to only contain enough information to allow access to just those prefixes. 228// 229// To revoke an access grant see the Project.RevokeAccess method. 230func (access *Access) Share(permission Permission, prefixes ...SharePrefix) (*Access, error) { 231 if permission == (Permission{}) { 232 return nil, packageError.New("permission is empty") 233 } 234 235 var notBefore, notAfter *time.Time 236 if !permission.NotBefore.IsZero() { 237 notBefore = &permission.NotBefore 238 } 239 if !permission.NotAfter.IsZero() { 240 notAfter = &permission.NotAfter 241 } 242 243 if notBefore != nil && notAfter != nil && notAfter.Before(*notBefore) { 244 return nil, packageError.New("invalid time range") 245 } 246 247 caveat := macaroon.WithNonce(macaroon.Caveat{ 248 DisallowReads: !permission.AllowDownload, 249 DisallowWrites: !permission.AllowUpload, 250 DisallowLists: !permission.AllowList, 251 DisallowDeletes: !permission.AllowDelete, 252 NotBefore: notBefore, 253 NotAfter: notAfter, 254 }) 255 256 sharedAccess := access2.NewEncryptionAccess() 257 sharedAccess.SetDefaultPathCipher(access.encAccess.Store.GetDefaultPathCipher()) 258 if len(prefixes) == 0 { 259 sharedAccess.SetDefaultKey(access.encAccess.Store.GetDefaultKey()) 260 } 261 262 for _, prefix := range prefixes { 263 // If the share prefix ends in a `/` we need to remove this final slash. 264 // Otherwise, if we the shared prefix is `/bob/`, the encrypted shared 265 // prefix results in `enc("")/enc("bob")/enc("")`. This is an incorrect 266 // encrypted prefix, what we really want is `enc("")/enc("bob")`. 267 unencPath := paths.NewUnencrypted(strings.TrimSuffix(prefix.Prefix, "/")) 268 269 encPath, err := encryption.EncryptPathWithStoreCipher(prefix.Bucket, unencPath, access.encAccess.Store) 270 if err != nil { 271 return nil, err 272 } 273 derivedKey, err := encryption.DerivePathKey(prefix.Bucket, unencPath, access.encAccess.Store) 274 if err != nil { 275 return nil, err 276 } 277 278 if err := sharedAccess.Store.Add(prefix.Bucket, unencPath, encPath, *derivedKey); err != nil { 279 return nil, err 280 } 281 caveat.AllowedPaths = append(caveat.AllowedPaths, &macaroon.Caveat_Path{ 282 Bucket: []byte(prefix.Bucket), 283 EncryptedPathPrefix: []byte(encPath.Raw()), 284 }) 285 } 286 287 restrictedAPIKey, err := access.apiKey.Restrict(caveat) 288 if err != nil { 289 return nil, err 290 } 291 292 restrictedAccess := &Access{ 293 satelliteURL: access.satelliteURL, 294 apiKey: restrictedAPIKey, 295 encAccess: sharedAccess, 296 } 297 return restrictedAccess, nil 298} 299 300// RevokeAccess revokes the API key embedded in the provided access grant. 301// 302// When an access grant is revoked, it will also revoke any further-restricted 303// access grants created (via the Access.Share method) from the revoked access 304// grant. 305// 306// An access grant is authorized to revoke any further-restricted access grant 307// created from it. An access grant cannot revoke itself. An unauthorized 308// request will return an error. 309// 310// There may be a delay between a successful revocation request and actual 311// revocation, depending on the satellite's access caching policies. 312func (project *Project) RevokeAccess(ctx context.Context, access *Access) (err error) { 313 defer mon.Task()(&ctx)(&err) 314 315 metainfoClient, err := project.dialMetainfoClient(ctx) 316 if err != nil { 317 return err 318 } 319 defer func() { err = errs.Combine(err, metainfoClient.Close()) }() 320 321 err = metainfoClient.RevokeAPIKey(ctx, metainfo.RevokeAPIKeyParams{ 322 APIKey: access.apiKey.SerializeRaw(), 323 }) 324 return convertKnownErrors(err, "", "") 325} 326 327// ReadOnlyPermission returns a Permission that allows reading and listing 328// (if the parent access grant already allows those things). 329func ReadOnlyPermission() Permission { 330 return Permission{ 331 AllowDownload: true, 332 AllowList: true, 333 } 334} 335 336// WriteOnlyPermission returns a Permission that allows writing and deleting 337// (if the parent access grant already allows those things). 338func WriteOnlyPermission() Permission { 339 return Permission{ 340 AllowUpload: true, 341 AllowDelete: true, 342 } 343} 344 345// FullPermission returns a Permission that allows all actions that the 346// parent access grant already allows. 347func FullPermission() Permission { 348 return Permission{ 349 AllowDownload: true, 350 AllowUpload: true, 351 AllowList: true, 352 AllowDelete: true, 353 } 354} 355 356// OverrideEncryptionKey overrides the root encryption key for the prefix in 357// bucket with encryptionKey. 358// 359// This function is useful for overriding the encryption key in user-specific 360// access grants when implementing multitenancy in a single app bucket. 361// See the relevant section in the package documentation. 362func (access *Access) OverrideEncryptionKey(bucket, prefix string, encryptionKey *EncryptionKey) error { 363 if !strings.HasSuffix(prefix, "/") { 364 return errors.New("prefix must end with slash") 365 } 366 367 // We need to remove the trailing slash. Otherwise, if we the shared 368 // prefix is `/bob/`, the encrypted shared prefix results in 369 // `enc("")/enc("bob")/enc("")`. This is an incorrect encrypted prefix, 370 // what we really want is `enc("")/enc("bob")`. 371 prefix = strings.TrimSuffix(prefix, "/") 372 373 store := access.encAccess.Store 374 375 unencPath := paths.NewUnencrypted(prefix) 376 encPath, err := encryption.EncryptPathWithStoreCipher(bucket, unencPath, store) 377 if err != nil { 378 return convertKnownErrors(err, bucket, prefix) 379 } 380 381 err = store.Add(bucket, unencPath, encPath, *encryptionKey.key) 382 return convertKnownErrors(err, bucket, prefix) 383} 384