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

support 1password as backend (#404) #130

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Expand Up @@ -16,6 +16,6 @@ jobs:
- uses: actions/setup-go@v2
with:
go-version: 1.19
- run: brew install pass gnupg
- run: brew install pass gnupg 1password-cli
- uses: actions/checkout@v2
- run: go test -race ./...
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -13,6 +13,7 @@ Currently Keyring supports the following backends
* [Pass](https://www.passwordstore.org/)
* [Encrypted file (JWT)](https://datatracker.ietf.org/doc/html/rfc7519)
* [KeyCtl](https://linux.die.net/man/1/keyctl)
* [1Password](https://1password.com)


## Usage
Expand Down
9 changes: 9 additions & 0 deletions config.go
Expand Up @@ -55,4 +55,13 @@ type Config struct {

// WinCredPrefix is a string prefix to prepend to the key name
WinCredPrefix string

// OnePasswordAccount is the name of the 1Password account to use
OnePasswordAccount string

// OnePasswordVault is the name of the 1Password vault to use
OnePasswordVault string

// OnePasswordPrefix is a string prefix to prepend to the key name
OnePasswordPrefix string
}
7 changes: 4 additions & 3 deletions go.mod
Expand Up @@ -9,14 +9,15 @@ require (
github.com/godbus/dbus v0.0.0-20190726142602-4481cbc300e2
github.com/gsterjov/go-libsecret v0.0.0-20161001094733-a6f4afe4910c
github.com/mtibben/percent v0.2.1
github.com/stretchr/testify v1.7.0
github.com/stretchr/testify v1.8.2
golang.org/x/sys v0.3.0
golang.org/x/term v0.3.0
)

require (
github.com/1Password/connect-sdk-go v1.5.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/objx v0.3.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
github.com/stretchr/objx v0.5.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
11 changes: 11 additions & 0 deletions go.sum
@@ -1,3 +1,5 @@
github.com/1Password/connect-sdk-go v1.5.3 h1:KyjJ+kCKj6BwB2Y8tPM1Ixg5uIS6HsB0uWA8U38p/Uk=
github.com/1Password/connect-sdk-go v1.5.3/go.mod h1:5rSymY4oIYtS4G3t0oMkGAXBeoYiukV3vkqlnEjIDJs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs=
github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4/go.mod h1:hN7oaIRCjzsZ2dE+yG5k+rsdt3qcwykqK6HVGcKwsw4=
github.com/danieljoos/wincred v1.1.2 h1:QLdCxFs1/Yl4zduvBdcHB8goaYk9RARS2SgLLRuAyr0=
Expand All @@ -23,9 +25,16 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.3.0 h1:NGXK3lHquSN08v5vWalVI/L8XU9hdzE/G6xsrze47As=
github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
golang.org/x/sys v0.0.0-20210819135213-f52c844e1c1c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand All @@ -37,3 +46,5 @@ gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2 changes: 2 additions & 0 deletions keyring.go
Expand Up @@ -20,6 +20,7 @@ const (
WinCredBackend BackendType = "wincred"
FileBackend BackendType = "file"
PassBackend BackendType = "pass"
OnePasswordBackend BackendType = "onepassword"
)

// This order makes sure the OS-specific backends
Expand All @@ -36,6 +37,7 @@ var backendOrder = []BackendType{
// General
PassBackend,
FileBackend,
OnePasswordBackend,
}

var supportedBackends = map[BackendType]opener{}
Expand Down
196 changes: 196 additions & 0 deletions one_password.go
@@ -0,0 +1,196 @@
//go:build darwin && cgo
// +build darwin,cgo

package keyring

import (
"encoding/json"
"fmt"
"os/exec"
"strings"
"time"
)

func init() {
supportedBackends[OnePasswordBackend] = opener(func(cfg Config) (Keyring, error) {
return &onePasswordKeyring{
account: cfg.OnePasswordAccount,
vault: cfg.OnePasswordVault,
prefix: cfg.OnePasswordPrefix,
}, nil
})
}

type onePasswordKeyring struct {
account string
vault string
prefix string
}

type onePasswordField struct {
Id string `json:"id"`
Value string `json:"value"`
}

type onePasswordItem struct {
Id string `json:"id"`
Fields []onePasswordField `json:"fields,omitempty"`
Title string `json:"title"`
UpdatedAt time.Time `json:"updated_at"`
}

const onePasswordKeyNotFoundFragmentMessage = "isn't an item"
const onePasswordItemCategory = "Secure Note"
const onePasswordItemField = "notesPlain"

func (k *onePasswordKeyring) retrieveOnePasswordItem(key string) (onePasswordItem, error) {
args := []string{
"item",
"get",
k.prefix + key,
"--format=json",
}

if k.account != "" {
args = append(args, "--account", k.account)
}

if k.vault != "" {
args = append(args, "--vault", k.vault)
}

cmd := exec.Command("op", args...)
output, err := cmd.CombinedOutput()

if err != nil {
if strings.Contains(string(output), onePasswordKeyNotFoundFragmentMessage) {
return onePasswordItem{}, ErrKeyNotFound
}

return onePasswordItem{}, err
}

var decoded onePasswordItem
err = json.Unmarshal(output, &decoded)

return decoded, err
}

func (k *onePasswordKeyring) Get(key string) (Item, error) {
onePasswordItem, err := k.retrieveOnePasswordItem(key)

if err != nil {
return Item{}, err
}

var value string
for _, field := range onePasswordItem.Fields {
if field.Id == onePasswordItemField {
value = field.Value
break
}
}

item := Item{
Key: strings.TrimPrefix(onePasswordItem.Title, k.prefix),
Data: []byte(fmt.Sprintf("%v", value)),
Label: strings.TrimPrefix(onePasswordItem.Title, k.prefix),
}
return item, nil
}

func (k *onePasswordKeyring) GetMetadata(key string) (Metadata, error) {
onePasswordItem, err := k.retrieveOnePasswordItem(key)

if err != nil {
return Metadata{}, err
}

metadata := Metadata{
ModificationTime: onePasswordItem.UpdatedAt,
}

return metadata, nil
}

func (k *onePasswordKeyring) Set(i Item) error {
k.Remove(i.Key)

args := []string{
"item",
"create",
}

if k.account != "" {
args = append(args, "--account", k.account)
}

if k.vault != "" {
args = append(args, "--vault", k.vault)
}

args = append(args, "--category", onePasswordItemCategory)
args = append(args, "--title", k.prefix+i.Key)

if i.Label != "" {
args = append(args, fmt.Sprintf("%s=%s", "label", i.Label))
}

if i.Description != "" {
args = append(args, fmt.Sprintf("%s=%s", "Description", i.Description))
}

args = append(args, fmt.Sprintf("%s=%s", onePasswordItemField, string(i.Data)))

return exec.Command("op", args...).Run()
}

func (k *onePasswordKeyring) Remove(key string) error {
output, err := exec.Command("op", "item", "delete", k.prefix+key).CombinedOutput()

if err != nil && strings.Contains(string(output), onePasswordKeyNotFoundFragmentMessage) {
return ErrKeyNotFound
}

return err
}

func (k *onePasswordKeyring) Keys() ([]string, error) {
args := []string{
"item",
"list",
"--format=json",
}

if k.account != "" {
args = append(args, "--account", k.account)
}

if k.vault != "" {
args = append(args, "--vault", k.vault)
}

output, err := exec.Command("op", args...).CombinedOutput()

if err != nil {
if strings.Contains(string(output), onePasswordKeyNotFoundFragmentMessage) {
return nil, ErrKeyNotFound
}

return nil, err
}

var decoded []onePasswordItem
err = json.Unmarshal(output, &decoded)

if err != nil {
return nil, err
}

keys := []string{}
for _, item := range decoded {
keys = append(keys, strings.TrimPrefix(item.Title, k.prefix))
}

return keys, nil
}