Skip to content

Commit 7858fcc

Browse files
committed
updating git-ssh remote profile; adding custom obfuscated secret store as a fallback to keyring
1 parent c7e735b commit 7858fcc

File tree

8 files changed

+176
-120
lines changed

8 files changed

+176
-120
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ require (
9595
github.com/jmespath/go-jmespath v0.4.0 // indirect
9696
github.com/josharian/intern v1.0.0 // indirect
9797
github.com/json-iterator/go v1.1.12 // indirect
98-
github.com/kevinburke/ssh_config v1.2.0
98+
github.com/kevinburke/ssh_config v1.2.0 // indirect
9999
github.com/mailru/easyjson v0.9.0 // indirect
100100
github.com/mattn/go-colorable v0.1.14 // indirect
101101
github.com/mattn/go-isatty v0.0.20 // indirect

internal/core/environments/providers/const.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ const (
1212

1313
// Password Provider Constants
1414
passwordProviderName = "password"
15-
keyringServiceName = config.AppNameLowerCase
1615

1716
// KMS Provider Constants
1817
rsaPubKeyRefName = "rsa-pubkey"

internal/core/environments/providers/password.go

Lines changed: 10 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
package providers
22

33
import (
4-
"crypto/sha256"
54
"fmt"
65

7-
"github.com/zalando/go-keyring"
8-
"slv.sh/slv/internal/core/commons"
96
"slv.sh/slv/internal/core/input"
7+
"slv.sh/slv/internal/core/keystore"
108
"xipher.org/xipher"
119
)
1210

@@ -28,34 +26,21 @@ func bindWithPassword(skBytes []byte, inputs map[string][]byte) (ref map[string]
2826
return
2927
}
3028

31-
func getFromKeyring(sealedSecretKeyBytes []byte) (string, error) {
32-
sha256sum := sha256.Sum256(sealedSecretKeyBytes)
33-
return keyring.Get(keyringServiceName, commons.Encode(sha256sum[:]))
34-
}
35-
36-
func putToKeyring(sealedSecretKeyBytes []byte, password string) error {
37-
sha256sum := sha256.Sum256(sealedSecretKeyBytes)
38-
return keyring.Set(keyringServiceName, commons.Encode(sha256sum[:]), password)
39-
}
40-
4129
func unBindWithPassword(ref map[string][]byte) (secretKeyBytes []byte, err error) {
4230
sealedSecretKeyBytes := ref["ssk"]
4331
if len(sealedSecretKeyBytes) == 0 {
4432
return nil, errSealedSecretKeyRef
4533
}
4634
var password []byte
47-
setPasswordToKeyring := false
35+
setPasswordToKeystore := false
4836
if input.IsInteractive() == nil {
49-
pwd, err := getFromKeyring(sealedSecretKeyBytes)
50-
if err == nil {
51-
password = []byte(pwd)
52-
} else {
53-
if err == keyring.ErrNotFound {
54-
setPasswordToKeyring = true
55-
}
37+
if password, err = keystore.Get(sealedSecretKeyBytes, false); err == keystore.ErrNotFound {
38+
setPasswordToKeystore = true
5639
if password, err = input.GetHiddenInput("Enter Password: "); err != nil {
5740
return nil, err
5841
}
42+
} else if err != nil {
43+
return nil, fmt.Errorf("failed to get password from keystore: %w", err)
5944
}
6045
}
6146
if password == nil {
@@ -69,11 +54,11 @@ func unBindWithPassword(ref map[string][]byte) (secretKeyBytes []byte, err error
6954
if err != nil {
7055
return nil, errInvalidPassword
7156
}
72-
if setPasswordToKeyring {
73-
confirm, _ := input.GetConfirmation("Do you want to save the password in keyring? (y/n): ", "y")
57+
if setPasswordToKeystore {
58+
confirm, _ := input.GetConfirmation("Do you want to save the password locally? (y/n): ", "y")
7459
if confirm {
75-
if err := putToKeyring(sealedSecretKeyBytes, string(password)); err != nil {
76-
fmt.Println("Failed to save password in keyring: ", err.Error())
60+
if err := keystore.Put(sealedSecretKeyBytes, password, false); err != nil {
61+
return nil, fmt.Errorf("failed to save password to keystore: %w", err)
7762
}
7863
}
7964
}

internal/core/keystore/store.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package keystore
2+
3+
import (
4+
"crypto/sha256"
5+
"crypto/sha512"
6+
"fmt"
7+
"os"
8+
"path/filepath"
9+
10+
"github.com/zalando/go-keyring"
11+
"slv.sh/slv/internal/core/commons"
12+
"slv.sh/slv/internal/core/config"
13+
"xipher.org/xipher"
14+
)
15+
16+
const (
17+
keyringServiceName = config.AppNameLowerCase
18+
keystoreDirName = "keystore"
19+
)
20+
21+
var (
22+
keyStoreDir = filepath.Join(config.GetAppDataDir(), keystoreDirName)
23+
24+
ErrNotFound = fmt.Errorf("data not found in keystore")
25+
)
26+
27+
func putToKeyStore(storeId string, payloadId, payload []byte) error {
28+
seed := sha512.Sum512(payloadId)
29+
if xsk, err := xipher.SecretKeyFromSeed(seed); err != nil {
30+
return fmt.Errorf("error writing data to keystore: %w", err)
31+
} else {
32+
if !commons.DirExists(keyStoreDir) {
33+
if err := os.MkdirAll(keyStoreDir, 0755); err != nil {
34+
return fmt.Errorf("error creating keystore directory: %w", err)
35+
}
36+
}
37+
if ct, err := xsk.Encrypt(payload, true, false); err != nil {
38+
return fmt.Errorf("error encrypting data for keystore: %w", err)
39+
} else {
40+
storePath := filepath.Join(keyStoreDir, storeId)
41+
if err := os.WriteFile(storePath, ct, 0644); err != nil {
42+
return fmt.Errorf("error writing data to keystore: %w", err)
43+
}
44+
return nil
45+
}
46+
}
47+
}
48+
49+
func getFromKeyStore(storeId string, payloadId []byte) ([]byte, error) {
50+
seed := sha512.Sum512(payloadId)
51+
if xsk, err := xipher.SecretKeyFromSeed(seed); err != nil {
52+
return nil, fmt.Errorf("error reading data from keystore: %w", err)
53+
} else {
54+
storePath := filepath.Join(keyStoreDir, storeId)
55+
if commons.FileExists(storePath) {
56+
if payload, err := os.ReadFile(storePath); err == nil && len(payload) > 0 {
57+
if decryptedPayload, err := xsk.Decrypt(payload); err != nil {
58+
return nil, fmt.Errorf("error reading data from keystore: %w", err)
59+
} else {
60+
return decryptedPayload, nil
61+
}
62+
} else if err != nil {
63+
return nil, fmt.Errorf("error reading data from keystore: %w", err)
64+
}
65+
}
66+
return nil, ErrNotFound
67+
}
68+
}
69+
70+
func Put(id, payload []byte, useLocalStore bool) error {
71+
sha256sum := sha256.Sum256(id)
72+
storeId := commons.Encode(sha256sum[:])
73+
if err := keyring.Set(keyringServiceName, storeId, string(payload)); err == nil {
74+
return nil
75+
} else if useLocalStore {
76+
return putToKeyStore(storeId, id, payload)
77+
} else {
78+
return fmt.Errorf("error saving data to keystore: %w", err)
79+
}
80+
}
81+
82+
func Get(id []byte, useLocalStore bool) ([]byte, error) {
83+
sha256sum := sha256.Sum256(id)
84+
storeId := commons.Encode(sha256sum[:])
85+
if payloadStr, err := keyring.Get(keyringServiceName, storeId); err == nil {
86+
return []byte(payloadStr), nil
87+
} else if err == keyring.ErrNotFound {
88+
return nil, ErrNotFound
89+
} else if useLocalStore {
90+
return getFromKeyStore(storeId, id)
91+
} else {
92+
return nil, fmt.Errorf("error reading data from keystore: %w", err)
93+
}
94+
}

internal/core/profiles/const.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package profiles
33
import (
44
"errors"
55
"time"
6+
7+
"xipher.org/xipher"
68
)
79

810
// Errors and constants used by profiles
@@ -18,11 +20,14 @@ const (
1820

1921
defaultEnvManifestFileName = "environments.yaml"
2022
defaultSettingsFileName = "settings.yaml"
23+
24+
profileCryptoKeyName = "slv_profiles"
2125
)
2226

2327
var (
2428
profileMgr *profileManager = nil
2529
profileMap map[string]*Profile = make(map[string]*Profile)
30+
profileSK *xipher.SecretKey
2631

2732
envManifestFileNames = []string{defaultEnvManifestFileName, "environments.yml"}
2833
settingsFileNames = []string{defaultSettingsFileName, "settings.yml"}

internal/core/profiles/git.go

Lines changed: 43 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
package profiles
22

33
import (
4+
"fmt"
45
"os"
5-
"path/filepath"
66
"regexp"
77
"strings"
88
"time"
@@ -14,16 +14,18 @@ import (
1414
"github.com/go-git/go-git/v5/plumbing/transport"
1515
"github.com/go-git/go-git/v5/plumbing/transport/http"
1616
gitssh "github.com/go-git/go-git/v5/plumbing/transport/ssh"
17-
"github.com/kevinburke/ssh_config"
1817
"golang.org/x/crypto/ssh"
18+
"slv.sh/slv/internal/core/commons"
1919
)
2020

2121
const (
2222
configGitRepoKey = "repo"
2323
configGitBranchKey = "branch"
2424
configGitHTTPUserKey = "username"
2525
configGitHTTPTokenKey = "token"
26-
configGitHTTPSSHKey = "ssh-key"
26+
configGitSSHKey = "ssh-key"
27+
28+
gitUrlRegexPattern = `(?i)^(?:(https?|git|ssh):\/\/[\w.@\-~:/]+\.git|git@[\w.\-]+:[\w./~-]+\.git)$`
2729
)
2830

2931
var gitArgs = []arg{
@@ -47,84 +49,50 @@ var gitArgs = []arg{
4749
description: "The token to authenticate with the git repository over HTTP",
4850
},
4951
{
50-
name: configGitHTTPSSHKey,
52+
name: configGitSSHKey,
5153
sensitive: true,
5254
description: "The path to the SSH private key file to authenticate with the git repository over SSH",
5355
},
5456
}
5557

56-
func expandTilde(path string) string {
57-
if len(path) > 0 && path[0] == '~' {
58-
if home, err := os.UserHomeDir(); err == nil {
59-
return filepath.Join(home, path[1:])
60-
}
61-
}
62-
return path
63-
}
64-
65-
func getSSHKeyFiles(uri string) []string {
66-
pattern := regexp.MustCompile(`(?:[^@]+@)?([^:/]+)`)
67-
matches := pattern.FindStringSubmatch(uri)
68-
if len(matches) < 2 {
69-
return nil
70-
}
71-
hostname := matches[1]
72-
if hostname != "" {
73-
allKeyPaths := ssh_config.GetAll(hostname, "IdentityFile")
74-
var keyPaths []string
75-
keyPathMap := make(map[string]struct{})
76-
for _, keyPath := range allKeyPaths {
77-
keyPath = expandTilde(keyPath)
78-
if _, found := keyPathMap[keyPath]; !found {
79-
keyPaths = append(keyPaths, keyPath)
80-
keyPathMap[keyPath] = struct{}{}
81-
}
82-
}
83-
return keyPaths
84-
}
85-
return nil
86-
}
87-
88-
func getGitAuth(config map[string]string) transport.AuthMethod {
58+
func getGitAuth(config map[string]string) (auth transport.AuthMethod, err error) {
8959
gitUrl := config[configGitRepoKey]
60+
if !regexp.MustCompile(gitUrlRegexPattern).MatchString(gitUrl) {
61+
return nil, fmt.Errorf("invalid git URL: %s", gitUrl)
62+
}
9063
if strings.HasPrefix(gitUrl, "https://") || strings.HasPrefix(gitUrl, "http://") {
9164
username := config[configGitHTTPUserKey]
9265
token := config[configGitHTTPTokenKey]
9366
if username != "" && token != "" {
94-
return &http.BasicAuth{
67+
auth = &http.BasicAuth{
9568
Username: username,
9669
Password: token,
9770
}
71+
} else if username != "" || token != "" {
72+
err = fmt.Errorf("both username and token must be provided for HTTP authentication")
9873
}
99-
}
100-
if sshAgentAuth, err := gitssh.NewSSHAgentAuth("git"); err == nil {
101-
return sshAgentAuth
102-
}
103-
if sshKeyFile := config[configGitHTTPSSHKey]; sshKeyFile != "" {
104-
if keyBytes, err := os.ReadFile(sshKeyFile); err == nil {
105-
_, err = ssh.ParsePrivateKey(keyBytes)
106-
if err == nil {
107-
auth, err := gitssh.NewPublicKeysFromFile("git", sshKeyFile, "")
108-
if err == nil {
109-
return auth
74+
} else {
75+
if sshKey := config[configGitSSHKey]; sshKey != "" {
76+
var keyBytes []byte
77+
if commons.FileExists(sshKey) {
78+
if keyBytes, err = os.ReadFile(sshKey); err != nil {
79+
return nil, fmt.Errorf("failed to read SSH key file %s: %w", sshKey, err)
11080
}
81+
config[configGitSSHKey] = string(keyBytes)
82+
} else {
83+
keyBytes = []byte(sshKey)
11184
}
112-
}
113-
}
114-
if sshKeyFiles := getSSHKeyFiles(gitUrl); len(sshKeyFiles) > 0 {
115-
keyPath := sshKeyFiles[0]
116-
keyBytes, err := os.ReadFile(keyPath)
117-
if err == nil {
118-
_, err = ssh.ParsePrivateKey(keyBytes)
119-
if err == nil {
120-
auth, err := gitssh.NewPublicKeysFromFile("git", keyPath, "")
121-
if err == nil {
122-
return auth
123-
}
85+
if _, err = ssh.ParsePrivateKey(keyBytes); err != nil {
86+
return nil, fmt.Errorf("failed to parse SSH key: %w", err)
87+
}
88+
if auth, err = gitssh.NewPublicKeys("git", keyBytes, ""); err != nil {
89+
return nil, fmt.Errorf("failed to create SSH auth from file %s: %w", sshKey, err)
12490
}
91+
} else if auth, err = gitssh.NewSSHAgentAuth("git"); err != nil {
92+
return nil, fmt.Errorf("failed to create SSH agent auth: %w", err)
12593
}
12694
}
127-
return nil
95+
return
12896
}
12997

13098
func gitCommit(repo *git.Repository, msg string) error {
@@ -158,7 +126,9 @@ func gitSetup(dir string, config map[string]string) (err error) {
158126
cloneOptions := &git.CloneOptions{
159127
URL: gitUrl,
160128
}
161-
cloneOptions.Auth = getGitAuth(config)
129+
if cloneOptions.Auth, err = getGitAuth(config); err != nil {
130+
return err
131+
}
162132
branch := config[configGitBranchKey]
163133
if branch != "" {
164134
cloneOptions.ReferenceName = plumbing.NewBranchReferenceName(branch)
@@ -177,8 +147,12 @@ func gitPull(dir string, config map[string]string) (err error) {
177147
if err != nil {
178148
return err
179149
}
150+
var auth transport.AuthMethod
151+
if auth, err = getGitAuth(config); err != nil {
152+
return err
153+
}
180154
err = worktree.Pull(&git.PullOptions{
181-
Auth: getGitAuth(config),
155+
Auth: auth,
182156
})
183157
if err == git.NoErrAlreadyUpToDate {
184158
return nil
@@ -194,7 +168,11 @@ func gitPush(dir string, config map[string]string, note string) (err error) {
194168
if err = gitCommit(repo, note); err != nil {
195169
return err
196170
}
171+
var auth transport.AuthMethod
172+
if auth, err = getGitAuth(config); err != nil {
173+
return err
174+
}
197175
return repo.Push(&git.PushOptions{
198-
Auth: getGitAuth(config),
176+
Auth: auth,
199177
})
200178
}

0 commit comments

Comments
 (0)