Skip to content

Commit

Permalink
Merge pull request #911 from tylercreller/OCM-4966
Browse files Browse the repository at this point in the history
Migrate keychain to use non-CGO libraries
  • Loading branch information
tylercreller authored Feb 29, 2024
2 parents 455b71d + fe0b2ea commit fbf6c59
Show file tree
Hide file tree
Showing 16 changed files with 487 additions and 20 deletions.
14 changes: 13 additions & 1 deletion .github/workflows/check-pull-request.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,27 @@ jobs:

test:
name: Test
runs-on: ubuntu-latest
strategy:
matrix:
go:
- "1.21"
platform:
- ubuntu-latest
- macos-latest
- windows-latest
runs-on: ${{ matrix.platform }}
steps:
- name: Checkout the source
uses: actions/checkout@v2

- name: Install Keyrings (macOS-only)
if: ${{ contains(fromJSON('["macos-latest"]'), matrix.platform) }}
run: brew install pass gnupg

- name: Install Keyrings (linux)
if: ${{ contains(fromJSON('["ubuntu-latest"]'), matrix.platform) }}
run: sudo apt-get install pass

- name: Setup Go
uses: actions/setup-go@v2
with:
Expand Down
3 changes: 3 additions & 0 deletions authentication/handler_test.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
//go:build linux
// +build linux

/*
Copyright (c) 2019 Red Hat, Inc.
Expand Down
71 changes: 71 additions & 0 deletions authentication/securestore/keychain_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
//go:build darwin
// +build darwin

/*
Copyright (c) 2024 Red Hat, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package securestore

import (
"time"

. "github.com/onsi/ginkgo/v2" // nolint
. "github.com/onsi/gomega" // nolint

. "github.com/openshift-online/ocm-sdk-go/testing" // nolint
)

var _ = Describe("Keychain", func() {
const backend = "keychain"

BeforeEach(func() {
err := RemoveConfigFromKeyring(backend)
Expect(err).To(BeNil())
})

When("Listing Keyrings", func() {
It("Lists keychain as a valid keyring", func() {
backends := AvailableBackends()
Expect(backends).To(ContainElement(backend))
})
})

When("Using Keychain", func() {
It("Stores/Removes via Keychain", func() {
// Create the token
accessToken := MakeTokenString("Bearer", 15*time.Minute)

// Run insert
err := UpsertConfigToKeyring(backend, []byte(accessToken))

Expect(err).To(BeNil())

// Check the content of the keyring
result, err := GetConfigFromKeyring(backend)
Expect(result).To(Equal([]byte(accessToken)))
Expect(err).To(BeNil())

// Remove the configuration from the keyring
err = RemoveConfigFromKeyring(backend)
Expect(err).To(BeNil())

// Ensure the keyring is empty
result, err = GetConfigFromKeyring(backend)
Expect(result).To(Equal([]byte("")))
Expect(err).To(BeNil())
})
})
})
101 changes: 82 additions & 19 deletions authentication/securestore/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@ package securestore
import (
"bytes"
"compress/gzip"
"errors"
"fmt"
"io"
"runtime"
"strings"

"github.com/99designs/keyring"
gokeyring "github.com/zalando/go-keyring"
)

const (
Expand Down Expand Up @@ -46,8 +49,6 @@ func getKeyringConfig(backend string) keyring.Config {
}

// IsBackendAvailable provides validation that the desired backend is available on the current OS.
//
// Note: CGO_ENABLED=1 is required for darwin builds (enables OSX Keychain)
func IsBackendAvailable(backend string) (isAvailable bool) {
if backend == "" {
return false
Expand All @@ -64,11 +65,14 @@ func IsBackendAvailable(backend string) (isAvailable bool) {
}

// AvailableBackends provides a slice of all available backend keys on the current OS.
//
// Note: CGO_ENABLED=1 is required for darwin builds (enables OSX Keychain)
func AvailableBackends() []string {
b := []string{}

if isDarwin() {
// Assume Keychain is always available on Darwin. It will not return from keyring.AvailableBackends()
b = append(b, "keychain")
}

// Intersection between available backends from OS and allowed backends
for _, avail := range keyring.AvailableBackends() {
for _, allowed := range AllowedBackends {
Expand All @@ -82,13 +86,15 @@ func AvailableBackends() []string {
}

// UpsertConfigToKeyring will upsert the provided credentials to the desired OS secure store.
//
// Note: CGO_ENABLED=1 is required for darwin builds (enables OSX Keychain)
func UpsertConfigToKeyring(backend string, creds []byte) error {
if err := ValidateBackend(backend); err != nil {
return err
}

if isDarwin() && isKeychain(backend) {
return keychainUpsert(creds)
}

ring, err := keyring.Open(getKeyringConfig(backend))
if err != nil {
return err
Expand Down Expand Up @@ -116,41 +122,41 @@ func UpsertConfigToKeyring(backend string, creds []byte) error {
}

// RemoveConfigFromKeyring will remove the credentials from the first priority OS secure store.
//
// Note: CGO_ENABLED=1 is required for OSX Keychain and darwin builds
func RemoveConfigFromKeyring(backend string) error {
if err := ValidateBackend(backend); err != nil {
return err
}

if isDarwin() && isKeychain(backend) {
return keychainRemove()
}

ring, err := keyring.Open(getKeyringConfig(backend))
if err != nil {
return err
}

err = ring.Remove(ItemKey)
if err != nil {
if err == keyring.ErrKeyNotFound {
if errors.Is(err, keyring.ErrKeyNotFound) {
// Ignore not found errors, key is already removed
return nil
}

if strings.Contains(err.Error(), "Keychain Error. (-25244)") {
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())
}
}

return err
}

// GetConfigFromKeyring will retrieve the credentials from the first priority OS secure store.
//
// Note: CGO_ENABLED=1 is required for darwin builds (enables OSX Keychain)
func GetConfigFromKeyring(backend string) ([]byte, error) {
if err := ValidateBackend(backend); err != nil {
return nil, err
}

if isDarwin() && isKeychain(backend) {
return keychainGet()
}

credentials := []byte("")

ring, err := keyring.Open(getKeyringConfig(backend))
Expand All @@ -159,9 +165,9 @@ func GetConfigFromKeyring(backend string) ([]byte, error) {
}

i, err := ring.Get(ItemKey)
if err != nil && err != keyring.ErrKeyNotFound {
if err != nil && !errors.Is(err, keyring.ErrKeyNotFound) {
return credentials, err
} else if err == keyring.ErrKeyNotFound {
} else if errors.Is(err, keyring.ErrKeyNotFound) {
// Not found, continue
} else {
credentials = i.Data
Expand All @@ -182,8 +188,6 @@ func GetConfigFromKeyring(backend string) ([]byte, error) {
}

// Validates that the requested backend is valid and available, returns an error if not.
//
// Note: CGO_ENABLED=1 is required for darwin builds (enables OSX Keychain)
func ValidateBackend(backend string) error {
if backend == "" {
return ErrKeyringInvalid
Expand All @@ -207,6 +211,55 @@ func ValidateBackend(backend string) error {
return nil
}

func keychainGet() ([]byte, error) {
credentials, err := gokeyring.Get(ItemKey, ItemKey)
if err != nil && !errors.Is(err, gokeyring.ErrNotFound) {
return []byte(credentials), err
} else if errors.Is(err, gokeyring.ErrNotFound) {
return []byte(""), nil
}

if len(credentials) == 0 {
// No creds to decompress, return early
return []byte(""), nil
}

creds, err := decompressConfig([]byte(credentials))
if err != nil {
return nil, err
}
return creds, nil
}

func keychainUpsert(creds []byte) error {
compressed, err := compressConfig(creds)
if err != nil {
return err
}

err = gokeyring.Set(ItemKey, ItemKey, string(compressed))
if err != nil {
return err
}

return nil
}

func keychainRemove() error {
err := gokeyring.Delete(ItemKey, ItemKey)
if err != nil {
if errors.Is(err, gokeyring.ErrNotFound) {
// Ignore not found errors, key is already removed
return nil
}
if strings.Contains(err.Error(), "Keychain Error. (-25244)") {
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())
}
}

return err
}

// Compresses credential bytes to help ensure all OS secure stores can store the data.
// Windows Credential Manager has a 2500 byte limit.
func compressConfig(creds []byte) ([]byte, error) {
Expand Down Expand Up @@ -241,3 +294,13 @@ func decompressConfig(creds []byte) ([]byte, error) {

return output, err
}

// isDarwin returns true if the current OS runtime is "darwin"
func isDarwin() bool {
return runtime.GOOS == "darwin"
}

// isKeychain returns true if the backend is "keychain"
func isKeychain(backend string) bool {
return backend == "keychain"
}
Loading

0 comments on commit fbf6c59

Please sign in to comment.