dragonchain/dragonchain-sdk-go

View on GitHub
credentials.go

Summary

Maintainability
A
3 hrs
Test Coverage
// Copyright 2020 Dragonchain, Inc. or its affiliates. All Rights Reserved.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//     http://www.apache.org/licenses/LICENSE-2.0
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package dragonchain

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/base64"
    "errors"
    "fmt"
    "hash"
    "os"
    "strings"

    "golang.org/x/crypto/blake2b"
    "golang.org/x/crypto/sha3"
)

// ErrUnsupportedHashAlgo is thrown when an unsupported hash method is provided.
var ErrUnsupportedHashAlgo = errors.New("hash method not supported")

// ErrNoCredentials is thrown if no credentials file can be found.
var ErrNoCredentials = errors.New("no credentials found")

// Supported hash functions
const (
    HashSHA256     = "SHA256"
    HashSHA3256    = "SHA3-256"
    HashBLAKE2b512 = "BLAKE2b512"
)

// Environment variables used to get chainID and Keys. These can be overridden before calling NewCredentials.
var (
    EnvDcIDName  = "DRAGONCHAIN_ID"
    EnvKeyName   = "AUTH_KEY"
    EnvKeyIDName = "AUTH_KEY_ID"
)

// Authenticator generates the authentication header for requests to chain.
type Authenticator interface {
    GetDragonchainID() string
    GetAuthorization(httpVerb, path string, timestamp, contentType, content string) string
}

// Credentials implements the Authenticator interface to generate authentication headers.
type Credentials struct {
    dcID      string
    authKey   string
    authKeyID string
    algorithm string
    hashFunc  func() hash.Hash
}

// NewCredentials uses the provided values to create a new Credentials instance for the given chain.
func NewCredentials(dcID, authKey, authKeyID, algorithm string) (*Credentials, error) {
    var err error
    var hashFunc func() hash.Hash

    switch algorithm {
    case "":
        // If no hash algorithm is given, we default to SHA256
        algorithm = HashSHA256
        fallthrough
    case HashSHA256:
        hashFunc = sha256.New
    case HashSHA3256:
        hashFunc = sha3.New256
    case HashBLAKE2b512:
        // blake2b does not implement 'func() hash.Hash' that is excepted from hmac.New
        // therefore we must create the following function to generate the correct func interface
        // blake2b.New512 only returns an error if an incorrect hash size is given. blake2b.New512
        // uses a hard coded hash size so a error is never returned and we can safely ignore it.
        // https://github.com/golang/go/issues/21644
        hashFunc = func() hash.Hash {
            h, _ := blake2b.New512(nil)
            return h
        }
    default:
        return nil, ErrUnsupportedHashAlgo
    }

    if len(dcID) == 0 {
        if dcID, err = getDragonchainID(); err != nil {
            return nil, err
        }
    }
    if len(authKey) == 0 {
        if authKey, err = getAuthKey(dcID); err != nil {
            return nil, err
        }
    }
    if len(authKeyID) == 0 {
        if authKeyID, err = getAuthKeyID(dcID); err != nil {
            return nil, err
        }
    }

    // If dcID, authKey, or authKeyID are empty, we must return an error
    if len(dcID) == 0 || len(authKey) == 0 || len(authKeyID) == 0 {
        return nil, ErrNoCredentials
    }

    return &Credentials{
        dcID:      dcID,
        authKey:   authKey,
        authKeyID: authKeyID,
        algorithm: algorithm,
        hashFunc:  hashFunc,
    }, nil
}

func getDragonchainID() (string, error) {
    dcID := os.Getenv(EnvDcIDName)
    if len(dcID) > 0 {
        return dcID, nil
    }

    configs, err := GetCredentialConfigs()
    if err == ErrNoConfigurationFileFound {
        return "", nil
    } else if err != nil {
        return "", err
    }

    return configs.DefaultDcID, nil
}

func getAuthKey(dcID string) (string, error) {
    authKey := os.Getenv(EnvKeyName)
    if len(authKey) > 0 {
        return authKey, nil
    }

    configs, err := GetCredentialConfigs()
    if err == ErrNoConfigurationFileFound {
        return "", nil
    } else if err != nil {
        return "", err
    }
    key, ok := configs.AuthKeys[dcID]
    if !ok {
        return "", nil
    }
    return key.AuthKey, nil
}

func getAuthKeyID(ID string) (string, error) {
    authKeyID := os.Getenv(EnvKeyIDName)
    if len(authKeyID) > 0 {
        return authKeyID, nil
    }

    configs, err := GetCredentialConfigs()
    if err == ErrNoConfigurationFileFound {
        return "", nil
    } else if err != nil {
        return "", err
    }
    key, ok := configs.AuthKeys[ID]
    if !ok {
        return "", nil
    }
    return key.AuthKeyID, nil
}

func (creds *Credentials) hmacMessage(httpVerb, path string, timestamp, contentType, content string) string {
    h := creds.hashFunc()

    h.Write([]byte(content))
    hashContent := h.Sum(nil)
    b64Content := base64.StdEncoding.EncodeToString(hashContent)

    msg := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s",
        strings.ToUpper(httpVerb),
        path,
        creds.dcID,
        timestamp,
        contentType,
        b64Content,
    )

    return msg
}

func (creds *Credentials) createHmac(secret, message string) []byte {
    h := hmac.New(creds.hashFunc, []byte(secret))
    h.Write([]byte(message))
    return h.Sum(nil)
}

// GetDragonchainID returns the current chain's ID.
func (creds *Credentials) GetDragonchainID() string {
    return creds.dcID
}

// GetAuthorization returns the current chain's authorization as a string.
func (creds *Credentials) GetAuthorization(httpVerb, path string, timestamp, contentType, content string) string {
    msgStr := creds.hmacMessage(httpVerb, path, timestamp, contentType, content)
    hmacMsg := creds.createHmac(creds.authKey, msgStr)
    b64hmac := base64.StdEncoding.EncodeToString(hmacMsg)
    return fmt.Sprintf("DC1-HMAC-%s %s:%s", creds.algorithm, creds.authKeyID, b64hmac)
}