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 }