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  }