diff --git a/satellite/console/wasm/README.md b/satellite/console/wasm/README.md index 03e6e7bb0..7fdbe8622 100644 --- a/satellite/console/wasm/README.md +++ b/satellite/console/wasm/README.md @@ -33,3 +33,149 @@ The HTML file should include the following: Additionally, the HTTP `Content-Security-Policy (CSP) script-src` directive will need to be modified to allow wasm code to be executed. See: [WebAssembly Content Security Policy docs](https://github.com/WebAssembly/content-security-policy/blob/master/proposals/CSP.md) + +### Usage + +#### function newPermission + +```js +function newPermission() +``` +- **Returns:** + + ```js + { + value: { + AllowDownload: false, + AllowUpload: false, + AllowDelete: false, + AllowList: false, + NotBefore: "0001-01-01T00:00:00Z", + NotAfter: "0001-01-01T00:00:00Z", + }, + error: "" + } + ``` + +- **Usage:** + + newPermission creates a new Permission object with all available permission settings set to default value. + +- **Example:** + + ```js + var permission = newPermission().value; + permission.AllowedDownload = true; + ``` + + +### function setAPIKeyPermission + +```js +function setAPIKeyPermission(apiKey, buckets, permission) + +``` + +- **Arguments** + Accepts three arguments: `apiKey`, `buckets` and `permission` + + - **apiKey** + - **Type:** `String` + - **Details:** + This parameter is required + + - **buckets** + - **Type:** `Array` + - **Details:** + An array of bucket names that restrict the api key to only contain enough information to allow access to just those buckets. + If no bucket names are provided, meaning an empty array, then all buckets are allowed. + This parameter is required. + + - **permission** + - **Type:** `Object` + - **Details:** + An object that defines what actions can be used for a given api key. + It should be constructed by calling `newPermission` + See also: https://github.com/storj/uplink/blob/b8e0f0a90665143a8ce975d92530737130874f5a/access.go#L46 + This parameter is required. + +- **Returns** + ```js + { + value: "restricted-api-key", + error: "" + } + ``` + - if an error message is returned, `value` will be set to an empty string. + +- **Usage** + Creates a new api key with specific permissions. + +- **Example** + ```js + var apiKey = "super-secret-key"; + var buckets = ["test-bucket"]; + var permission = newPermission().value + permission.allowUpload = true + var restrictedAPIKey = setAPIKeyPermission(apiKey, buckets, permission) + ``` + +### function generateAccessGrant + +```js +function generateAccessGrant(satelliteNodeURL, apiKey, encryptionPassphrase, projectID) + +``` + +- **Arguments** + Accepts four arguments: `satelliteNodeURL`, `apiKey`, `encryptionPassphrase` and `projectID` + + - **satelliteNodeURL** + - **Type:** `String` + - **Details:** + A string that contains satellite node id and satellite address. + Example: 12tDhBcuMevundiuZPQJd613iW5vCdFtkRDBjBEfjdVtv1hbfCL@127.0.0.1:10000 + This parameter is required + + - **apiKey** + - **Type:** `String` + - **Details:** + This parameter is required + + - **encryptionPassphrase** + - **Type:** `String` + - **Details:** + A string that's used for encryption. + This parameter is required. + + - **projectID** + - **Type:** `String` + - **Details:** + A project-based salt for determinitic key derivation. + Currently it's referring to a project ID. However, it might change in the future to have more randomness. + This parameter is required. + +- **Returns** + ```js + { + value: "access-grant", + error: "" + } + ``` + - if an error message is returned, `value` will be set to an empty string. + +- **Usage** + Creates a new api key with specific permissions. + +- **Example** + ```js + var satelliteNodeURL = "12tDhBcuMevundiuZPQJd613iW5vCdFtkRDBjBEfjdVtv1hbfCL@127.0.0.1:10000" + var apiKey = "super-secret-key"; + var passphrase = "123"; + var projectID = "project-id" + var result = generateAccessGrant(satelliteNodeURL, apiKey, passphrase, projectID); + if (result.err != "") { + // something went wrong + } + var access = result.value + ``` diff --git a/satellite/console/wasm/access.go b/satellite/console/wasm/access.go index 0f34a962e..d15ddf2cd 100644 --- a/satellite/console/wasm/access.go +++ b/satellite/console/wasm/access.go @@ -5,48 +5,68 @@ package main import ( - "fmt" + "crypto/sha256" + "encoding/json" "syscall/js" + "time" + + "github.com/zeebo/errs" "storj.io/common/encryption" "storj.io/common/macaroon" "storj.io/common/storj" + "storj.io/common/uuid" + "storj.io/uplink" "storj.io/uplink/private/access2" ) func main() { js.Global().Set("generateAccessGrant", generateAccessGrant()) + js.Global().Set("setAPIKeyPermission", setAPIKeyPermission()) + js.Global().Set("newPermission", newPermission()) <-make(chan bool) } func generateAccessGrant() js.Func { - return js.FuncOf(func(this js.Value, args []js.Value) interface{} { + return js.FuncOf(responseHandler(func(this js.Value, args []js.Value) (interface{}, error) { if len(args) < 4 { - return fmt.Sprintf("Error not enough arguments. Need 4, but only %d supplied. The order of arguments are: satellite Node URL, API key, encryption passphrase, and project salt.", len(args)) + return nil, errs.New("not enough arguments. Need 4, but only %d supplied. The order of arguments are: satellite Node URL, API key, encryption passphrase, and project ID.", len(args)) } satelliteNodeURL := args[0].String() apiKey := args[1].String() encryptionPassphrase := args[2].String() projectSalt := args[3].String() - return genAccessGrant(satelliteNodeURL, + access, err := genAccessGrant(satelliteNodeURL, apiKey, encryptionPassphrase, projectSalt, ) - }) + if err != nil { + return nil, err + } + + return access, nil + })) } -func genAccessGrant(satelliteNodeURL, apiKey, encryptionPassphrase, projectSalt string) string { +func genAccessGrant(satelliteNodeURL, apiKey, encryptionPassphrase, projectID string) (string, error) { parsedAPIKey, err := macaroon.ParseAPIKey(apiKey) if err != nil { - return err.Error() + return "", err + } + + id, err := uuid.FromString(projectID) + if err != nil { + return "", err } const concurrency = 8 - key, err := encryption.DeriveRootKey([]byte(encryptionPassphrase), []byte(projectSalt), "", concurrency) + salt := sha256.Sum256(id[:]) + + key, err := encryption.DeriveRootKey([]byte(encryptionPassphrase), salt[:], "", concurrency) if err != nil { - return err.Error() + return "", err } encAccess := access2.NewEncryptionAccessWithDefaultKey(key) @@ -58,7 +78,153 @@ func genAccessGrant(satelliteNodeURL, apiKey, encryptionPassphrase, projectSalt } accessString, err := a.Serialize() if err != nil { - return err.Error() + return "", err + } + return accessString, nil +} + +// setAPIKeyPermission creates a new api key with specific permissions. +func setAPIKeyPermission() js.Func { + return js.FuncOf(responseHandler(func(this js.Value, args []js.Value) (interface{}, error) { + if len(args) < 3 { + return nil, errs.New("not enough arguments. Need 3, but only %d supplied. The order of arguments are: API key, bucket names, and permission object.", len(args)) + } + apiKey := args[0].String() + + // convert array of bucket names to go []string type + buckets := args[1] + if ok := buckets.InstanceOf(js.Global().Get("Array")); !ok { + return nil, errs.New("invalid data type. Expect Array, Got %s", buckets.Type().String()) + } + bucketNames, err := parseArrayOfStrings(buckets) + if err != nil { + return nil, err + } + + // convert js permission to go permission type + permissionJS := args[2] + if permissionJS.Type() != js.TypeObject { + return nil, errs.New("invalid argument type. Expect %s, Got %s", js.TypeObject.String(), permissionJS.Type().String()) + } + permission, err := parsePermission(permissionJS) + if err != nil { + return nil, err + } + + restrictedKey, err := setPermission(apiKey, bucketNames, permission) + if err != nil { + return nil, err + } + + return restrictedKey.Serialize(), nil + })) +} + +// newPermission creates a new permission object. +func newPermission() js.Func { + return js.FuncOf(responseHandler(func(this js.Value, args []js.Value) (interface{}, error) { + p, err := json.Marshal(uplink.Permission{}) + if err != nil { + return nil, err + } + + var jsObj map[string]interface{} + err = json.Unmarshal(p, &jsObj) + if err != nil { + return nil, err + } + return jsObj, nil + })) +} + +func setPermission(key string, buckets []string, permission uplink.Permission) (*macaroon.APIKey, error) { + if permission == (uplink.Permission{}) { + return nil, errs.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, errs.New("invalid time range") + } + + caveat := macaroon.Caveat{ + DisallowReads: !permission.AllowDownload, + DisallowWrites: !permission.AllowUpload, + DisallowLists: !permission.AllowList, + DisallowDeletes: !permission.AllowDelete, + NotBefore: notBefore, + NotAfter: notAfter, + } + + for _, b := range buckets { + caveat.AllowedPaths = append(caveat.AllowedPaths, &macaroon.Caveat_Path{ + Bucket: []byte(b), + }) + } + + apiKey, err := macaroon.ParseAPIKey(key) + if err != nil { + return nil, err + } + + restrictedKey, err := apiKey.Restrict(caveat) + if err != nil { + return nil, err + } + + return restrictedKey, nil +} + +func parsePermission(arg js.Value) (uplink.Permission, error) { + var permission uplink.Permission + + // convert javascript object to a json string + jsJSON := js.Global().Get("JSON") + p := jsJSON.Call("stringify", arg) + + err := json.Unmarshal([]byte(p.String()), &permission) + if err != nil { + return permission, err + } + + return permission, nil +} + +func parseArrayOfStrings(arg js.Value) ([]string, error) { + data := make([]string, arg.Length()) + for i := 0; i < arg.Length(); i++ { + data[i] = arg.Index(i).String() + } + + return data, nil +} + +type result struct { + value interface{} + err error +} + +func (r result) ToJS() map[string]interface{} { + var errMsg string + if r.err != nil { + errMsg = r.err.Error() + } + return map[string]interface{}{ + "value": js.ValueOf(r.value), + "error": errMsg, + } +} + +func responseHandler(fn func(this js.Value, args []js.Value) (value interface{}, err error)) func(js.Value, []js.Value) interface{} { + return func(this js.Value, args []js.Value) interface{} { + value, err := fn(this, args) + return result{value, err}.ToJS() } - return accessString }