141 lines
3.8 KiB
Go
141 lines
3.8 KiB
Go
// Copyright 2013 Google Inc. All Rights Reserved.
|
|
//
|
|
// 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 keyring
|
|
|
|
import (
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"os/exec"
|
|
"strings"
|
|
|
|
"al.essio.dev/pkg/shellescape"
|
|
)
|
|
|
|
const (
|
|
execPathKeychain = "/usr/bin/security"
|
|
|
|
// encodingPrefix is a well-known prefix added to strings encoded by Set.
|
|
encodingPrefix = "go-keyring-encoded:"
|
|
base64EncodingPrefix = "go-keyring-base64:"
|
|
)
|
|
|
|
type macOSXKeychain struct{}
|
|
|
|
// func (*MacOSXKeychain) IsAvailable() bool {
|
|
// return exec.Command(execPathKeychain).Run() != exec.ErrNotFound
|
|
// }
|
|
|
|
// Get password from macos keyring given service and user name.
|
|
func (k macOSXKeychain) Get(service, username string) (string, error) {
|
|
out, err := exec.Command(
|
|
execPathKeychain,
|
|
"find-generic-password",
|
|
"-s", service,
|
|
"-wa", username).CombinedOutput()
|
|
if err != nil {
|
|
if strings.Contains(string(out), "could not be found") {
|
|
err = ErrNotFound
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
trimStr := strings.TrimSpace(string(out[:]))
|
|
// if the string has the well-known prefix, assume it's encoded
|
|
if strings.HasPrefix(trimStr, encodingPrefix) {
|
|
dec, err := hex.DecodeString(trimStr[len(encodingPrefix):])
|
|
return string(dec), err
|
|
} else if strings.HasPrefix(trimStr, base64EncodingPrefix) {
|
|
dec, err := base64.StdEncoding.DecodeString(trimStr[len(base64EncodingPrefix):])
|
|
return string(dec), err
|
|
}
|
|
|
|
return trimStr, nil
|
|
}
|
|
|
|
// Set stores a secret in the macos keyring given a service name and a user.
|
|
func (k macOSXKeychain) Set(service, username, password string) error {
|
|
// if the added secret has multiple lines or some non ascii,
|
|
// osx will hex encode it on return. To avoid getting garbage, we
|
|
// encode all passwords
|
|
password = base64EncodingPrefix + base64.StdEncoding.EncodeToString([]byte(password))
|
|
|
|
cmd := exec.Command(execPathKeychain, "-i")
|
|
stdIn, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
|
|
command := fmt.Sprintf("add-generic-password -U -s %s -a %s -w %s\n", shellescape.Quote(service), shellescape.Quote(username), shellescape.Quote(password))
|
|
if len(command) > 4096 {
|
|
return ErrSetDataTooBig
|
|
}
|
|
|
|
if _, err := io.WriteString(stdIn, command); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err = stdIn.Close(); err != nil {
|
|
return err
|
|
}
|
|
|
|
err = cmd.Wait()
|
|
return err
|
|
}
|
|
|
|
// Delete deletes a secret, identified by service & user, from the keyring.
|
|
func (k macOSXKeychain) Delete(service, username string) error {
|
|
out, err := exec.Command(
|
|
execPathKeychain,
|
|
"delete-generic-password",
|
|
"-s", service,
|
|
"-a", username).CombinedOutput()
|
|
if strings.Contains(string(out), "could not be found") {
|
|
err = ErrNotFound
|
|
}
|
|
return err
|
|
}
|
|
|
|
// DeleteAll deletes all secrets for a given service
|
|
func (k macOSXKeychain) DeleteAll(service string) error {
|
|
// if service is empty, do nothing otherwise it might accidentally delete all secrets
|
|
if service == "" {
|
|
return ErrNotFound
|
|
}
|
|
// Delete each secret in a while loop until there is no more left
|
|
// under the service
|
|
for {
|
|
out, err := exec.Command(
|
|
execPathKeychain,
|
|
"delete-generic-password",
|
|
"-s", service).CombinedOutput()
|
|
if strings.Contains(string(out), "could not be found") {
|
|
return nil
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
func init() {
|
|
provider = macOSXKeychain{}
|
|
}
|