2020-05-11 20:57:46 +02:00
// Copyright (C) 2020 Storj Labs, Inc.
// See LICENSE for copying information.
package uplink
import (
"context"
"strings"
"time"
"github.com/btcsuite/btcutil/base58"
"github.com/zeebo/errs"
"storj.io/common/encryption"
"storj.io/common/macaroon"
"storj.io/common/paths"
"storj.io/common/pb"
"storj.io/common/storj"
"storj.io/uplink/internal/expose"
)
// An Access Grant contains everything to access a project and specific buckets.
// It includes a potentially-restricted API Key, a potentially-restricted set
// of encryption information, and information about the Satellite responsible
// for the project's metadata.
type Access struct {
satelliteAddress string
apiKey * macaroon . APIKey
encAccess * encryptionAccess
}
// SharePrefix defines a prefix that will be shared.
type SharePrefix struct {
Bucket string
// Prefix is the prefix of the shared object keys.
//
// Note: that within a bucket, the hierarchical key derivation scheme is
// delineated by forward slashes (/), so encryption information will be
// included in the resulting access grant to decrypt any key that shares
// the same prefix up until the last slash.
Prefix string
}
// Permission defines what actions can be used to share.
type Permission struct {
// AllowDownload gives permission to download the object's content. It
// allows getting object metadata, but it does not allow listing buckets.
AllowDownload bool
// AllowUpload gives permission to create buckets and upload new objects.
// It does not allow overwriting existing objects unless AllowDelete is
// granted too.
AllowUpload bool
// AllowList gives permission to list buckets. It allows getting object
// metadata, but it does not allow downloading the object's content.
AllowList bool
// AllowDelete gives permission to delete buckets and objects. Unless
// either AllowDownload or AllowList is granted too, no object metadata and
// no error info will be returned for deleted objects.
AllowDelete bool
// NotBefore restricts when the resulting access grant is valid for.
// If set, the resulting access grant will not work if the Satellite
// believes the time is before NotBefore.
// If set, this value should always be before NotAfter.
NotBefore time . Time
// NotAfter restricts when the resulting access grant is valid for.
// If set, the resulting access grant will not work if the Satellite
// believes the time is after NotAfter.
// If set, this value should always be after NotBefore.
NotAfter time . Time
}
// ParseAccess parses a serialized access grant string.
//
// This should be the main way to instantiate an access grant for opening a project.
// See the note on RequestAccessWithPassphrase.
func ParseAccess ( access string ) ( * Access , error ) {
data , version , err := base58 . CheckDecode ( access )
if err != nil || version != 0 {
return nil , packageError . New ( "invalid access grant format" )
}
p := new ( pb . Scope )
if err := pb . Unmarshal ( data , p ) ; err != nil {
return nil , packageError . New ( "unable to unmarshal access grant: %v" , err )
}
if len ( p . SatelliteAddr ) == 0 {
return nil , packageError . New ( "access grant is missing satellite address" )
}
apiKey , err := macaroon . ParseRawAPIKey ( p . ApiKey )
if err != nil {
return nil , packageError . New ( "access grant has malformed api key: %v" , err )
}
encAccess , err := parseEncryptionAccessFromProto ( p . EncryptionAccess )
if err != nil {
return nil , packageError . New ( "access grant has malformed encryption access: %v" , err )
}
return & Access {
satelliteAddress : p . SatelliteAddr ,
apiKey : apiKey ,
encAccess : encAccess ,
} , nil
}
// Serialize serializes an access grant such that it can be used later with
// ParseAccess or other tools.
func ( access * Access ) Serialize ( ) ( string , error ) {
switch {
case len ( access . satelliteAddress ) == 0 :
return "" , packageError . New ( "access grant is missing satellite address" )
case access . apiKey == nil :
return "" , packageError . New ( "access grant is missing api key" )
case access . encAccess == nil :
return "" , packageError . New ( "access grant is missing encryption access" )
}
enc , err := access . encAccess . toProto ( )
if err != nil {
return "" , packageError . Wrap ( err )
}
data , err := pb . Marshal ( & pb . Scope {
SatelliteAddr : access . satelliteAddress ,
ApiKey : access . apiKey . SerializeRaw ( ) ,
EncryptionAccess : enc ,
} )
if err != nil {
return "" , packageError . New ( "unable to marshal access grant: %v" , err )
}
return base58 . CheckEncode ( data , 0 ) , nil
}
// RequestAccessWithPassphrase generates a new access grant using a passhprase.
// It must talk to the Satellite provided to get a project-based salt for
// deterministic key derivation.
//
// Note: this is a CPU-heavy function that uses a password-based key derivation function
// (Argon2). This should be a setup-only step. Most common interactions with the library
// should be using a serialized access grant through ParseAccess directly.
func RequestAccessWithPassphrase ( ctx context . Context , satelliteAddress , apiKey , passphrase string ) ( * Access , error ) {
return ( Config { } ) . RequestAccessWithPassphrase ( ctx , satelliteAddress , apiKey , passphrase )
}
// RequestAccessWithPassphrase generates a new access grant using a passhprase.
// It must talk to the Satellite provided to get a project-based salt for
// deterministic key derivation.
//
// Note: this is a CPU-heavy function that uses a password-based key derivation function
// (Argon2). This should be a setup-only step. Most common interactions with the library
// should be using a serialized access grant through ParseAccess directly.
func ( config Config ) RequestAccessWithPassphrase ( ctx context . Context , satelliteAddress , apiKey , passphrase string ) ( * Access , error ) {
return requestAccessWithPassphraseAndConcurrency ( ctx , config , satelliteAddress , apiKey , passphrase , 8 )
}
func init ( ) {
// expose this method for backcomp package.
expose . RequestAccessWithPassphraseAndConcurrency = requestAccessWithPassphraseAndConcurrency
// expose this method for private/access package.
expose . EnablePathEncryptionBypass = enablePathEncryptionBypass
}
// requestAccessWithPassphraseAndConcurrency requests satellite for a new access grant using a passhprase and specific concurrency for the Argon2 key derivation.
//
// NB: when modifying the signature of this func, also update backcomp and internal/expose packages.
func requestAccessWithPassphraseAndConcurrency ( ctx context . Context , config Config , satelliteAddress , apiKey , passphrase string , concurrency uint8 ) ( _ * Access , err error ) {
parsedAPIKey , err := macaroon . ParseAPIKey ( apiKey )
if err != nil {
return nil , packageError . Wrap ( err )
}
metainfo , _ , fullNodeURL , err := config . dial ( ctx , satelliteAddress , parsedAPIKey )
if err != nil {
return nil , packageError . Wrap ( err )
}
defer func ( ) { err = errs . Combine ( err , metainfo . Close ( ) ) } ( )
info , err := metainfo . GetProjectInfo ( ctx )
if err != nil {
2020-05-29 15:08:11 +02:00
return nil , convertKnownErrors ( err , "" , "" )
2020-05-11 20:57:46 +02:00
}
key , err := encryption . DeriveRootKey ( [ ] byte ( passphrase ) , info . ProjectSalt , "" , concurrency )
if err != nil {
return nil , packageError . Wrap ( err )
}
encAccess := newEncryptionAccessWithDefaultKey ( key )
encAccess . setDefaultPathCipher ( storj . EncAESGCM )
return & Access {
satelliteAddress : fullNodeURL ,
apiKey : parsedAPIKey ,
encAccess : encAccess ,
} , nil
}
// enablePathEncryptionBypass enables path encryption bypass for embedded encryption access.
//
// NB: when modifying the signature of this func, also update private/access and internal/expose packages.
func enablePathEncryptionBypass ( access * Access ) error {
access . encAccess . Store ( ) . EncryptionBypass = true
return nil
}
// Share creates a new access grant with specific permissions.
//
// Access grants can only have their existing permissions restricted,
// and the resulting access grant will only allow for the intersection of all previous
// Share calls in the access grant construction chain.
//
// Prefixes, if provided, restrict the access grant (and internal encryption information)
// to only contain enough information to allow access to just those prefixes.
func ( access * Access ) Share ( permission Permission , prefixes ... SharePrefix ) ( * Access , error ) {
if permission == ( Permission { } ) {
return nil , packageError . New ( "permission is empty" )
}
var notBefore , notAfter * time . Time
if ! permission . NotBefore . IsZero ( ) {
notBefore = & permission . NotBefore
}
if ! permission . NotAfter . IsZero ( ) {
notAfter = & permission . NotAfter
}
if notBefore != nil && notAfter != nil && notAfter . Before ( * notBefore ) {
return nil , packageError . New ( "invalid time range" )
}
caveat := macaroon . Caveat {
DisallowReads : ! permission . AllowDownload ,
DisallowWrites : ! permission . AllowUpload ,
DisallowLists : ! permission . AllowList ,
DisallowDeletes : ! permission . AllowDelete ,
NotBefore : notBefore ,
NotAfter : notAfter ,
}
sharedAccess := newEncryptionAccess ( )
sharedAccess . setDefaultPathCipher ( access . encAccess . Store ( ) . GetDefaultPathCipher ( ) )
if len ( prefixes ) == 0 {
sharedAccess . setDefaultKey ( access . encAccess . Store ( ) . GetDefaultKey ( ) )
}
for _ , prefix := range prefixes {
// If the share prefix ends in a `/` we need to remove this final slash.
// Otherwise, if we the shared prefix is `/bob/`, the encrypted shared
// prefix results in `enc("")/enc("bob")/enc("")`. This is an incorrect
// encrypted prefix, what we really want is `enc("")/enc("bob")`.
unencPath := paths . NewUnencrypted ( strings . TrimSuffix ( prefix . Prefix , "/" ) )
encPath , err := encryption . EncryptPathWithStoreCipher ( prefix . Bucket , unencPath , access . encAccess . store )
if err != nil {
return nil , err
}
derivedKey , err := encryption . DerivePathKey ( prefix . Bucket , unencPath , access . encAccess . store )
if err != nil {
return nil , err
}
if err := sharedAccess . store . Add ( prefix . Bucket , unencPath , encPath , * derivedKey ) ; err != nil {
return nil , err
}
caveat . AllowedPaths = append ( caveat . AllowedPaths , & macaroon . Caveat_Path {
Bucket : [ ] byte ( prefix . Bucket ) ,
EncryptedPathPrefix : [ ] byte ( encPath . Raw ( ) ) ,
} )
}
restrictedAPIKey , err := access . apiKey . Restrict ( caveat )
if err != nil {
return nil , err
}
restrictedAccess := & Access {
satelliteAddress : access . satelliteAddress ,
apiKey : restrictedAPIKey ,
encAccess : sharedAccess ,
}
return restrictedAccess , nil
}
// ReadOnlyPermission returns a Permission that allows reading and listing
// (if the parent access grant already allows those things).
func ReadOnlyPermission ( ) Permission {
return Permission {
AllowDownload : true ,
AllowList : true ,
}
}
// WriteOnlyPermission returns a Permission that allows writing and deleting
// (if the parent access grant already allows those things).
func WriteOnlyPermission ( ) Permission {
return Permission {
AllowUpload : true ,
AllowDelete : true ,
}
}
// FullPermission returns a Permission that allows all actions that the
// parent access grant already allows.
func FullPermission ( ) Permission {
return Permission {
AllowDownload : true ,
AllowUpload : true ,
AllowList : true ,
AllowDelete : true ,
}
}