github.com/dtroyer-salad/og2/v2@v2.0.0-20240412154159-c47231610877/registry/remote/credentials/native_store.go (about) 1 /* 2 Copyright The ORAS Authors. 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package credentials 17 18 import ( 19 "bytes" 20 "context" 21 "encoding/json" 22 "os/exec" 23 "strings" 24 25 "oras.land/oras-go/v2/registry/remote/auth" 26 "oras.land/oras-go/v2/registry/remote/credentials/internal/executer" 27 ) 28 29 const ( 30 remoteCredentialsPrefix = "docker-credential-" 31 emptyUsername = "<token>" 32 errCredentialsNotFoundMessage = "credentials not found in native keychain" 33 ) 34 35 // dockerCredentials mimics how docker credential helper binaries store 36 // credential information. 37 // Reference: 38 // - https://docs.docker.com/engine/reference/commandline/login/#credential-helper-protocol 39 type dockerCredentials struct { 40 ServerURL string `json:"ServerURL"` 41 Username string `json:"Username"` 42 Secret string `json:"Secret"` 43 } 44 45 // nativeStore implements a credentials store using native keychain to keep 46 // credentials secure. 47 type nativeStore struct { 48 exec executer.Executer 49 } 50 51 // NewNativeStore creates a new native store that uses a remote helper program to 52 // manage credentials. 53 // 54 // The argument of NewNativeStore can be the native keychains 55 // ("wincred" for Windows, "pass" for linux and "osxkeychain" for macOS), 56 // or any program that follows the docker-credentials-helper protocol. 57 // 58 // Reference: 59 // - https://docs.docker.com/engine/reference/commandline/login#credentials-store 60 func NewNativeStore(helperSuffix string) Store { 61 return &nativeStore{ 62 exec: executer.New(remoteCredentialsPrefix + helperSuffix), 63 } 64 } 65 66 // NewDefaultNativeStore returns a native store based on the platform-default 67 // docker credentials helper and a bool indicating if the native store is 68 // available. 69 // - Windows: "wincred" 70 // - Linux: "pass" or "secretservice" 71 // - macOS: "osxkeychain" 72 // 73 // Reference: 74 // - https://docs.docker.com/engine/reference/commandline/login/#credentials-store 75 func NewDefaultNativeStore() (Store, bool) { 76 if helper := getDefaultHelperSuffix(); helper != "" { 77 return NewNativeStore(helper), true 78 } 79 return nil, false 80 } 81 82 // Get retrieves credentials from the store for the given server. 83 func (ns *nativeStore) Get(ctx context.Context, serverAddress string) (auth.Credential, error) { 84 var cred auth.Credential 85 out, err := ns.exec.Execute(ctx, strings.NewReader(serverAddress), "get") 86 if err != nil { 87 if err.Error() == errCredentialsNotFoundMessage { 88 // do not return an error if the credentials are not in the keychain. 89 return auth.EmptyCredential, nil 90 } 91 return auth.EmptyCredential, err 92 } 93 var dockerCred dockerCredentials 94 if err := json.Unmarshal(out, &dockerCred); err != nil { 95 return auth.EmptyCredential, err 96 } 97 // bearer auth is used if the username is "<token>" 98 if dockerCred.Username == emptyUsername { 99 cred.RefreshToken = dockerCred.Secret 100 } else { 101 cred.Username = dockerCred.Username 102 cred.Password = dockerCred.Secret 103 } 104 return cred, nil 105 } 106 107 // Put saves credentials into the store. 108 func (ns *nativeStore) Put(ctx context.Context, serverAddress string, cred auth.Credential) error { 109 dockerCred := &dockerCredentials{ 110 ServerURL: serverAddress, 111 Username: cred.Username, 112 Secret: cred.Password, 113 } 114 if cred.RefreshToken != "" { 115 dockerCred.Username = emptyUsername 116 dockerCred.Secret = cred.RefreshToken 117 } 118 credJSON, err := json.Marshal(dockerCred) 119 if err != nil { 120 return err 121 } 122 _, err = ns.exec.Execute(ctx, bytes.NewReader(credJSON), "store") 123 return err 124 } 125 126 // Delete removes credentials from the store for the given server. 127 func (ns *nativeStore) Delete(ctx context.Context, serverAddress string) error { 128 _, err := ns.exec.Execute(ctx, strings.NewReader(serverAddress), "erase") 129 return err 130 } 131 132 // getDefaultHelperSuffix returns the default credential helper suffix. 133 func getDefaultHelperSuffix() string { 134 platformDefault := getPlatformDefaultHelperSuffix() 135 if _, err := exec.LookPath(remoteCredentialsPrefix + platformDefault); err == nil { 136 return platformDefault 137 } 138 return "" 139 }