github.com/viant/toolbox@v0.34.5/cred/config.go (about)

     1  package cred
     2  
     3  import (
     4  	"bytes"
     5  	"crypto/x509"
     6  	"encoding/base64"
     7  	"encoding/json"
     8  	"encoding/pem"
     9  	"errors"
    10  	"fmt"
    11  	"github.com/viant/toolbox"
    12  	"golang.org/x/crypto/ssh"
    13  	"golang.org/x/oauth2/google"
    14  	"golang.org/x/oauth2/jwt"
    15  	"gopkg.in/yaml.v2"
    16  	"io"
    17  	"io/ioutil"
    18  	"os"
    19  	"path"
    20  	"strings"
    21  )
    22  
    23  var sshKeyFileCandidates = []string{"/.ssh/id_rsa", "/.ssh/id_dsa"}
    24  var DefaultKey = []byte{0x24, 0x66, 0xDD, 0x87, 0x8B, 0x96, 0x3C, 0x9D}
    25  var PasswordCipher = GetDefaultPasswordCipher()
    26  
    27  type Config struct {
    28  	Username          string `json:",omitempty"`
    29  	Email             string `json:",omitempty"`
    30  	Password          string `json:",omitempty"`
    31  	EncryptedPassword string `json:",omitempty"`
    32  	Endpoint          string `json:",omitempty"`
    33  
    34  	PrivateKeyPath              string `json:",omitempty"`
    35  	PrivateKeyPassword          string `json:",omitempty"`
    36  	PrivateKeyEncryptedPassword string `json:",omitempty"`
    37  
    38  	//amazon cloud credential
    39  	Key       string `json:",omitempty"`
    40  	Secret    string `json:",omitempty"`
    41  	Region    string `json:",omitempty"`
    42  	AccountID string `json:",omitempty"`
    43  	Token     string `json:",omitempty"`
    44  
    45  	//google cloud credential
    46  	ClientEmail             string `json:"client_email,omitempty"`
    47  	TokenURL                string `json:"token_uri,omitempty"`
    48  	PrivateKey              string `json:"private_key,omitempty"`
    49  	PrivateKeyID            string `json:"private_key_id,omitempty"`
    50  	ProjectID               string `json:"project_id,omitempty"`
    51  	TokenURI                string `json:"token_uri"`
    52  	Type                    string `json:"type"`
    53  	ClientX509CertURL       string `json:"client_x509_cert_url"`
    54  	AuthProviderX509CertURL string `json:"auth_provider_x509_cert_url"`
    55  
    56  	//JSON string for this secret
    57  	Data            string `json:",omitempty"`
    58  	sshClientConfig *ssh.ClientConfig
    59  	jwtClientConfig *jwt.Config
    60  }
    61  
    62  func (c *Config) Load(filename string) error {
    63  	reader, err := toolbox.OpenFile(filename)
    64  	if err != nil {
    65  		return err
    66  	}
    67  	defer reader.Close()
    68  	ext := path.Ext(filename)
    69  	return c.LoadFromReader(reader, ext)
    70  }
    71  
    72  func (c *Config) LoadFromReader(reader io.Reader, ext string) error {
    73  	if strings.Contains(ext, "yaml") || strings.Contains(ext, "yml") {
    74  		var data, err = ioutil.ReadAll(reader)
    75  		if err != nil {
    76  			return err
    77  		}
    78  		err = yaml.Unmarshal(data, c)
    79  		if err != nil {
    80  			return err
    81  		}
    82  	} else {
    83  		err := json.NewDecoder(reader).Decode(c)
    84  		if err != nil {
    85  			return nil
    86  		}
    87  	}
    88  	if c.EncryptedPassword != "" {
    89  		decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(c.EncryptedPassword))
    90  		data, err := ioutil.ReadAll(decoder)
    91  		if err != nil {
    92  			return err
    93  		}
    94  		c.Password = string(PasswordCipher.Decrypt(data))
    95  	} else if c.Password != "" {
    96  		c.encryptPassword(c.Password)
    97  	}
    98  
    99  	if c.PrivateKeyEncryptedPassword != "" {
   100  		decoder := base64.NewDecoder(base64.StdEncoding, strings.NewReader(c.PrivateKeyEncryptedPassword))
   101  		data, err := ioutil.ReadAll(decoder)
   102  		if err != nil {
   103  			return err
   104  		}
   105  		c.PrivateKeyPassword = string(PasswordCipher.Decrypt(data))
   106  	}
   107  	return nil
   108  }
   109  
   110  func (c *Config) Save(filename string) error {
   111  	_ = os.Remove(filename)
   112  	file, err := os.OpenFile(filename, os.O_RDWR|os.O_CREATE, 0644)
   113  	if err != nil {
   114  		return err
   115  	}
   116  	defer file.Close()
   117  	return c.Write(file)
   118  }
   119  
   120  func (c *Config) Write(writer io.Writer) error {
   121  	var password = c.Password
   122  	defer func() { c.Password = password }()
   123  	if password != "" {
   124  		c.encryptPassword(password)
   125  		c.Password = ""
   126  	}
   127  	return json.NewEncoder(writer).Encode(c)
   128  }
   129  
   130  func (c *Config) encryptPassword(password string) {
   131  	encrypted := PasswordCipher.Encrypt([]byte(password))
   132  	buf := new(bytes.Buffer)
   133  	encoder := base64.NewEncoder(base64.StdEncoding, buf)
   134  	defer encoder.Close()
   135  	encoder.Write(encrypted)
   136  	encoder.Close()
   137  	c.EncryptedPassword = string(buf.Bytes())
   138  }
   139  
   140  func (c *Config) applyDefaultIfNeeded() {
   141  	if c.Username == "" {
   142  		c.Username = os.Getenv("USER")
   143  	}
   144  	if c.PrivateKeyPath == "" && c.Password == "" {
   145  		homeDirectory := os.Getenv("HOME")
   146  		if homeDirectory != "" {
   147  			for _, candidate := range sshKeyFileCandidates {
   148  				filename := path.Join(homeDirectory, candidate)
   149  				file, err := os.Open(filename)
   150  				if err == nil {
   151  					file.Close()
   152  					c.PrivateKeyPath = filename
   153  					break
   154  				}
   155  			}
   156  		}
   157  	}
   158  }
   159  
   160  //IsKeyEncrypted checks if supplied key content is encrypyed by password
   161  func IsKeyEncrypted(keyPath string) bool {
   162  	privateKeyBytes, err := ioutil.ReadFile(keyPath)
   163  	if err != nil {
   164  		return false
   165  	}
   166  	block, _ := pem.Decode(privateKeyBytes)
   167  	if block == nil {
   168  		return false
   169  	}
   170  	return strings.Contains(block.Headers["Proc-Type"], "ENCRYPTED")
   171  }
   172  
   173  //SSHClientConfig returns a new instance of sshClientConfig
   174  func (c *Config) SSHClientConfig() (*ssh.ClientConfig, error) {
   175  	return c.ClientConfig()
   176  }
   177  
   178  //NewJWTConfig returns new JWT config for supplied scopes
   179  func (c *Config) NewJWTConfig(scopes ...string) (*jwt.Config, error) {
   180  	var result = &jwt.Config{
   181  		Email:        c.ClientEmail,
   182  		Subject:      c.ClientEmail,
   183  		PrivateKey:   []byte(c.PrivateKey),
   184  		PrivateKeyID: c.PrivateKeyID,
   185  		Scopes:       scopes,
   186  		TokenURL:     c.TokenURL,
   187  	}
   188  
   189  	if c.PrivateKeyPath != "" && c.PrivateKey == "" {
   190  		privateKey, err := ioutil.ReadFile(c.PrivateKeyPath)
   191  		if err != nil {
   192  			return nil, fmt.Errorf("failed to open provide key: %v, %v", c.PrivateKeyPath, err)
   193  		}
   194  		result.PrivateKey = privateKey
   195  	}
   196  	if result.TokenURL == "" {
   197  		result.TokenURL = google.JWTTokenURL
   198  	}
   199  	return result, nil
   200  }
   201  
   202  //JWTConfig returns jwt config and projectID
   203  func (c *Config) JWTConfig(scopes ...string) (config *jwt.Config, projectID string, err error) {
   204  	config, err = c.NewJWTConfig(scopes...)
   205  	return config, c.ProjectID, err
   206  }
   207  
   208  func loadPEM(location string, password string) ([]byte, error) {
   209  	var pemBytes []byte
   210  	if IsKeyEncrypted(location) {
   211  		block, _ := pem.Decode(pemBytes)
   212  		if block == nil {
   213  			return nil, errors.New("invalid PEM data")
   214  		}
   215  		if x509.IsEncryptedPEMBlock(block) {
   216  			key, err := x509.DecryptPEMBlock(block, []byte(password))
   217  			if err != nil {
   218  				return nil, err
   219  			}
   220  			block = &pem.Block{Type: block.Type, Bytes: key}
   221  			pemBytes = pem.EncodeToMemory(block)
   222  			return pemBytes, nil
   223  		}
   224  	}
   225  	return ioutil.ReadFile(location)
   226  }
   227  
   228  //ClientConfig returns a new instance of sshClientConfig
   229  func (c *Config) ClientConfig() (*ssh.ClientConfig, error) {
   230  	if c.sshClientConfig != nil {
   231  		return c.sshClientConfig, nil
   232  	}
   233  	c.applyDefaultIfNeeded()
   234  	result := &ssh.ClientConfig{
   235  		User:            c.Username,
   236  		HostKeyCallback: ssh.InsecureIgnoreHostKey(),
   237  		Auth:            make([]ssh.AuthMethod, 0),
   238  	}
   239  
   240  	if c.Password != "" {
   241  		result.Auth = append(result.Auth, ssh.Password(c.Password))
   242  	}
   243  
   244  	if c.PrivateKeyPath != "" {
   245  		password := c.PrivateKeyPassword //backward-compatible
   246  		if password == "" {
   247  			password = c.Password
   248  		}
   249  		pemBytes, err := loadPEM(c.PrivateKeyPath, password)
   250  		key, err := ssh.ParsePrivateKey(pemBytes)
   251  		if err != nil {
   252  			return nil, err
   253  		}
   254  		result.Auth = append(result.Auth, ssh.PublicKeys(key))
   255  	}
   256  	c.sshClientConfig = result
   257  	return result, nil
   258  }
   259  
   260  //NewConfig create a new config for supplied file name
   261  func NewConfig(filename string) (*Config, error) {
   262  	var config = &Config{}
   263  	err := config.Load(filename)
   264  	if err != nil {
   265  		return nil, err
   266  	}
   267  	config.applyDefaultIfNeeded()
   268  	return config, nil
   269  }
   270  
   271  //GetDefaultPasswordCipher return a default password cipher
   272  func GetDefaultPasswordCipher() Cipher {
   273  	var result, err = NewBlowfishCipher(DefaultKey)
   274  	if err != nil {
   275  		return nil
   276  	}
   277  	return result
   278  }