Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Migrate keychain to use non-CGO libraries #911

Merged
merged 1 commit into from
Feb 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
tylercreller marked this conversation as resolved.
Show resolved Hide resolved

/*
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")
tylercreller marked this conversation as resolved.
Show resolved Hide resolved
}

// 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))
tylercreller marked this conversation as resolved.
Show resolved Hide resolved
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
Loading