github.com/openshift-online/ocm-sdk-go@v0.1.473/authentication/securestore/main.go (about)

     1  package securestore
     2  
     3  import (
     4  	"bytes"
     5  	"compress/gzip"
     6  	"errors"
     7  	"fmt"
     8  	"io"
     9  	"runtime"
    10  	"strings"
    11  
    12  	"github.com/99designs/keyring"
    13  	gokeyring "github.com/zalando/go-keyring"
    14  )
    15  
    16  const (
    17  	KindInternetPassword = "Internet password" // MacOS Keychain item kind
    18  	ItemKey              = "RedHatSSO"
    19  	CollectionName       = "login" // Common OS default collection name
    20  	MaxWindowsByteSize   = 2500    // Windows Credential Manager has a 2500 byte limit
    21  )
    22  
    23  var (
    24  	ErrKeyringUnavailable = fmt.Errorf("keyring is valid but is not available on the current OS")
    25  	ErrKeyringInvalid     = fmt.Errorf("keyring is invalid, expected one of: [%v]", strings.Join(AllowedBackends, ", "))
    26  	AllowedBackends       = []string{
    27  		string(keyring.WinCredBackend),
    28  		string(keyring.KeychainBackend),
    29  		string(keyring.SecretServiceBackend),
    30  		string(keyring.PassBackend),
    31  	}
    32  )
    33  
    34  func getKeyringConfig(backend string) keyring.Config {
    35  	return keyring.Config{
    36  		AllowedBackends: []keyring.BackendType{keyring.BackendType(backend)},
    37  		// Generic
    38  		ServiceName: ItemKey,
    39  		// MacOS
    40  		KeychainName:                   CollectionName,
    41  		KeychainTrustApplication:       true,
    42  		KeychainSynchronizable:         false,
    43  		KeychainAccessibleWhenUnlocked: false,
    44  		// Windows
    45  		WinCredPrefix: ItemKey,
    46  		// Secret Service
    47  		LibSecretCollectionName: CollectionName,
    48  	}
    49  }
    50  
    51  // IsBackendAvailable provides validation that the desired backend is available on the current OS.
    52  func IsBackendAvailable(backend string) (isAvailable bool) {
    53  	if backend == "" {
    54  		return false
    55  	}
    56  
    57  	for _, avail := range AvailableBackends() {
    58  		if avail == backend {
    59  			isAvailable = true
    60  			break
    61  		}
    62  	}
    63  
    64  	return isAvailable
    65  }
    66  
    67  // AvailableBackends provides a slice of all available backend keys on the current OS.
    68  func AvailableBackends() []string {
    69  	b := []string{}
    70  
    71  	if isDarwin() {
    72  		// Assume Keychain is always available on Darwin. It will not return from keyring.AvailableBackends()
    73  		b = append(b, "keychain")
    74  	}
    75  
    76  	// Intersection between available backends from OS and allowed backends
    77  	for _, avail := range keyring.AvailableBackends() {
    78  		for _, allowed := range AllowedBackends {
    79  			if string(avail) == allowed {
    80  				b = append(b, allowed)
    81  			}
    82  		}
    83  	}
    84  
    85  	return b
    86  }
    87  
    88  // UpsertConfigToKeyring will upsert the provided credentials to the desired OS secure store.
    89  func UpsertConfigToKeyring(backend string, creds []byte) error {
    90  	if err := ValidateBackend(backend); err != nil {
    91  		return err
    92  	}
    93  
    94  	if isDarwin() && isKeychain(backend) {
    95  		return keychainUpsert(creds)
    96  	}
    97  
    98  	ring, err := keyring.Open(getKeyringConfig(backend))
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	compressed, err := compressConfig(creds)
   104  	if err != nil {
   105  		return err
   106  	}
   107  
   108  	// check if available backend contains windows credential manager and exceeds the byte limit
   109  	if len(compressed) > MaxWindowsByteSize &&
   110  		backend == string(keyring.WinCredBackend) {
   111  		return fmt.Errorf("credentials are too large for Windows Credential Manager: %d bytes (max %d)", len(compressed), MaxWindowsByteSize)
   112  	}
   113  
   114  	err = ring.Set(keyring.Item{
   115  		Label:       ItemKey,
   116  		Key:         ItemKey,
   117  		Description: KindInternetPassword,
   118  		Data:        compressed,
   119  	})
   120  
   121  	return err
   122  }
   123  
   124  // RemoveConfigFromKeyring will remove the credentials from the first priority OS secure store.
   125  func RemoveConfigFromKeyring(backend string) error {
   126  	if err := ValidateBackend(backend); err != nil {
   127  		return err
   128  	}
   129  
   130  	if isDarwin() && isKeychain(backend) {
   131  		return keychainRemove()
   132  	}
   133  
   134  	ring, err := keyring.Open(getKeyringConfig(backend))
   135  	if err != nil {
   136  		return err
   137  	}
   138  
   139  	err = ring.Remove(ItemKey)
   140  	if err != nil {
   141  		if errors.Is(err, keyring.ErrKeyNotFound) {
   142  			// Ignore not found errors, key is already removed
   143  			return nil
   144  		}
   145  	}
   146  
   147  	return err
   148  }
   149  
   150  // GetConfigFromKeyring will retrieve the credentials from the first priority OS secure store.
   151  func GetConfigFromKeyring(backend string) ([]byte, error) {
   152  	if err := ValidateBackend(backend); err != nil {
   153  		return nil, err
   154  	}
   155  
   156  	if isDarwin() && isKeychain(backend) {
   157  		return keychainGet()
   158  	}
   159  
   160  	credentials := []byte("")
   161  
   162  	ring, err := keyring.Open(getKeyringConfig(backend))
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  
   167  	i, err := ring.Get(ItemKey)
   168  	if err != nil && !errors.Is(err, keyring.ErrKeyNotFound) {
   169  		return credentials, err
   170  	} else if errors.Is(err, keyring.ErrKeyNotFound) {
   171  		// Not found, continue
   172  	} else {
   173  		credentials = i.Data
   174  	}
   175  
   176  	if len(credentials) == 0 {
   177  		// No creds to decompress, return early
   178  		return credentials, nil
   179  	}
   180  
   181  	creds, err := decompressConfig(credentials)
   182  	if err != nil {
   183  		return nil, err
   184  	}
   185  
   186  	return creds, nil
   187  
   188  }
   189  
   190  // Validates that the requested backend is valid and available, returns an error if not.
   191  func ValidateBackend(backend string) error {
   192  	if backend == "" {
   193  		return ErrKeyringInvalid
   194  	} else {
   195  		isAllowedBackend := false
   196  		for _, allowed := range AllowedBackends {
   197  			if allowed == backend {
   198  				isAllowedBackend = true
   199  				break
   200  			}
   201  		}
   202  		if !isAllowedBackend {
   203  			return ErrKeyringInvalid
   204  		}
   205  	}
   206  
   207  	if !IsBackendAvailable(backend) {
   208  		return ErrKeyringUnavailable
   209  	}
   210  
   211  	return nil
   212  }
   213  
   214  func keychainGet() ([]byte, error) {
   215  	credentials, err := gokeyring.Get(ItemKey, ItemKey)
   216  	if err != nil && !errors.Is(err, gokeyring.ErrNotFound) {
   217  		return []byte(credentials), err
   218  	} else if errors.Is(err, gokeyring.ErrNotFound) {
   219  		return []byte(""), nil
   220  	}
   221  
   222  	if len(credentials) == 0 {
   223  		// No creds to decompress, return early
   224  		return []byte(""), nil
   225  	}
   226  
   227  	creds, err := decompressConfig([]byte(credentials))
   228  	if err != nil {
   229  		return nil, err
   230  	}
   231  	return creds, nil
   232  }
   233  
   234  func keychainUpsert(creds []byte) error {
   235  	compressed, err := compressConfig(creds)
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	err = gokeyring.Set(ItemKey, ItemKey, string(compressed))
   241  	if err != nil {
   242  		return err
   243  	}
   244  
   245  	return nil
   246  }
   247  
   248  func keychainRemove() error {
   249  	err := gokeyring.Delete(ItemKey, ItemKey)
   250  	if err != nil {
   251  		if errors.Is(err, gokeyring.ErrNotFound) {
   252  			// Ignore not found errors, key is already removed
   253  			return nil
   254  		}
   255  		if strings.Contains(err.Error(), "Keychain Error. (-25244)") {
   256  			return fmt.Errorf("%s\nThis application may not have permission to delete from the Keychain. Please check the permissions in the Keychain and try again", err.Error())
   257  		}
   258  	}
   259  
   260  	return err
   261  }
   262  
   263  // Compresses credential bytes to help ensure all OS secure stores can store the data.
   264  // Windows Credential Manager has a 2500 byte limit.
   265  func compressConfig(creds []byte) ([]byte, error) {
   266  	var b bytes.Buffer
   267  	gz := gzip.NewWriter(&b)
   268  
   269  	_, err := gz.Write(creds)
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  
   274  	err = gz.Close()
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  
   279  	return b.Bytes(), nil
   280  }
   281  
   282  // Decompresses credential bytes
   283  func decompressConfig(creds []byte) ([]byte, error) {
   284  	reader := bytes.NewReader(creds)
   285  	gzreader, err := gzip.NewReader(reader)
   286  	if err != nil {
   287  		return nil, err
   288  	}
   289  
   290  	output, err := io.ReadAll(gzreader)
   291  	if err != nil {
   292  		return nil, err
   293  	}
   294  
   295  	return output, err
   296  }
   297  
   298  // isDarwin returns true if the current OS runtime is "darwin"
   299  func isDarwin() bool {
   300  	return runtime.GOOS == "darwin"
   301  }
   302  
   303  // isKeychain returns true if the backend is "keychain"
   304  func isKeychain(backend string) bool {
   305  	return backend == "keychain"
   306  }