Skip to content

Commit 2877070

Browse files
authored
Merge pull request #1 from plumber-cd/init
Init
2 parents 889e0d4 + 6553b77 commit 2877070

11 files changed

+731
-0
lines changed

.github/workflows/release.yml

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
on:
2+
push:
3+
tags:
4+
- "v*"
5+
6+
name: Create Release
7+
8+
jobs:
9+
release:
10+
name: Draft Release
11+
runs-on: ubuntu-latest
12+
steps:
13+
- name: Checkout Code
14+
uses: actions/checkout@v3
15+
- name: Release
16+
uses: docker://antonyurchenko/git-release:v4
17+
env:
18+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
19+
DRAFT_RELEASE: "true"
20+
PRE_RELEASE: "false"
21+
CHANGELOG_FILE: "CHANGELOG.md"
22+
ALLOW_EMPTY_CHANGELOG: "false"

CHANGELOG.md

+14
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [Unreleased]
9+
10+
## [0.0.1] - 2022-05-22
11+
12+
### Added
13+
14+
- Initial release

README.md

+68
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,70 @@
11
# go-credentials
22
Cross Platform Go Credentials Provider
3+
4+
This simple library provides common interface for working with credentials.
5+
6+
By default it uses the following interfaces:
7+
8+
- MacOS: https://github.com/keybase/go-keychain
9+
- Linux: https://github.com/keybase/dbus
10+
- Windows: https://github.com/danieljoos/wincred
11+
12+
You can substitute it with your own interface per platform.
13+
14+
# Example
15+
16+
```go
17+
package main
18+
19+
import (
20+
"fmt"
21+
22+
"github.com/plumber-cd/go-credentials"
23+
)
24+
25+
func main() {
26+
domain := &credentials.Domain{
27+
Service: "My App Name",
28+
AccessGroup: "github.com/plumber-cd/go-credentials", // Define some unique for your app instance value
29+
}
30+
31+
if err := credentials.SetDomain(domain); err != nil {
32+
panic(err)
33+
}
34+
35+
if err := err = credentials.Create("http://example.com", "<name/username/title/display name>", "password"); err != nil {
36+
panic(err)
37+
}
38+
39+
name, secret, err := credentials.Retrieve("http://example.com")
40+
if err != nil {
41+
panic(err)
42+
}
43+
fmt.Printf("Name: %s\n", name)
44+
fmt.Printf("Secret: %s\n", secret)
45+
46+
if err := credentials.Update("http://example.com", "new title", "new password"); err != nil {
47+
panic(err)
48+
}
49+
50+
if err := credentials.Delete("http://example.com"); err {
51+
panic(err)
52+
}
53+
}
54+
```
55+
56+
# Custom provider
57+
58+
You need to create new `struct` than implements `credentials.Provider` interface. Somewhere in your app, you then will need to:
59+
60+
```go
61+
package main
62+
63+
import "github.com/plumber-cd/go-credentials"
64+
65+
func init() {
66+
credentials.Current = &MyCustomProvider{}
67+
}
68+
```
69+
70+
That's it. All other code that is using this same library - will now use your custom provider instead of default.

credentials.go

+96
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
package credentials
2+
3+
// Domain is the configuration for provider.
4+
// It is used to avoid collisions with other applications.
5+
type Domain struct {
6+
// Service is a display name or a title for your credentials.
7+
Service string
8+
9+
// AccessGroup is a label on your credentials.
10+
// Some provider implementations will allow you to have multiple credentials for the same URL and Service while they have different Group.
11+
AccessGroup string
12+
}
13+
14+
// Provider interface is the main CRUD interface for your credentials.
15+
type Provider interface {
16+
// This has to be called only once per provider instance.
17+
SetDomain(domain *Domain)
18+
19+
// Returns true if provider is ready to be used.
20+
IsConfigured() bool
21+
22+
// Create creates a new secret. URL is the ultimate key for it.
23+
Create(url, name, secret string) error
24+
25+
// Retrieve returns credentials entry by URL.
26+
Retrieve(url string) (name, secret string, err error)
27+
28+
// Update finds existing credentials for URL and updates name and secret on it.
29+
Update(url, name, secret string) error
30+
31+
// Delete credentials for this URL.
32+
Delete(url string) error
33+
34+
// This function should convert downstream libraries errors to common error interfaces.
35+
ErrorWrap(url string, err error) error
36+
}
37+
38+
var (
39+
Current Provider
40+
)
41+
42+
func SetDomain(domain *Domain) error {
43+
if Current == nil {
44+
return ErrProviderUndefined
45+
}
46+
Current.SetDomain(domain)
47+
return nil
48+
}
49+
50+
func IsDefined() bool {
51+
return Current != nil
52+
}
53+
54+
func IsConfigured() bool {
55+
return IsDefined() && Current.IsConfigured()
56+
}
57+
58+
func Create(name, url, secret string) error {
59+
if !IsDefined() {
60+
return ErrProviderUndefined
61+
}
62+
if !IsConfigured() {
63+
return ErrNotConfigured
64+
}
65+
return Current.Create(name, url, secret)
66+
}
67+
68+
func Retrieve(url string) (name, secret string, err error) {
69+
if !IsDefined() {
70+
return "", "", ErrProviderUndefined
71+
}
72+
if !IsConfigured() {
73+
return "", "", ErrNotConfigured
74+
}
75+
return Current.Retrieve(url)
76+
}
77+
78+
func Update(name, url, secret string) error {
79+
if !IsDefined() {
80+
return ErrProviderUndefined
81+
}
82+
if !IsConfigured() {
83+
return ErrNotConfigured
84+
}
85+
return Current.Update(name, url, secret)
86+
}
87+
88+
func Delete(url string) error {
89+
if !IsDefined() {
90+
return ErrProviderUndefined
91+
}
92+
if !IsConfigured() {
93+
return ErrNotConfigured
94+
}
95+
return Current.Delete(url)
96+
}

credentials_darwin.go

+112
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//go:build darwin
2+
// +build darwin
3+
4+
package credentials
5+
6+
import (
7+
"errors"
8+
"fmt"
9+
10+
"github.com/keybase/go-keychain"
11+
)
12+
13+
func init() {
14+
Current = &DarwinProvider{}
15+
}
16+
17+
type DarwinProvider struct {
18+
domain *Domain
19+
}
20+
21+
func (p *DarwinProvider) ErrorWrap(url string, err error) error {
22+
if err == nil {
23+
return nil
24+
}
25+
if errors.Is(err, keychain.ErrorDuplicateItem) {
26+
return fmt.Errorf("%w: %s: %v", ErrDuplicate, url, err)
27+
}
28+
if errors.Is(err, keychain.ErrorItemNotFound) {
29+
return fmt.Errorf("%w: %s: %v", ErrNotFound, url, err)
30+
}
31+
return fmt.Errorf("%w: %s", err, url)
32+
}
33+
34+
func (p *DarwinProvider) SetDomain(domain *Domain) {
35+
p.domain = domain
36+
}
37+
38+
func (p *DarwinProvider) IsConfigured() bool {
39+
return p.domain != nil
40+
}
41+
42+
func (p *DarwinProvider) NewCreateItem(url, name, secret string) keychain.Item {
43+
item := keychain.NewItem()
44+
item.SetSecClass(keychain.SecClassGenericPassword)
45+
item.SetService(p.domain.Service)
46+
item.SetAccessGroup(p.domain.AccessGroup)
47+
item.SetAccount(url)
48+
item.SetLabel(name)
49+
item.SetData([]byte(secret))
50+
item.SetSynchronizable(keychain.SynchronizableNo)
51+
item.SetAccessible(keychain.AccessibleWhenUnlocked)
52+
return item
53+
}
54+
55+
func (p *DarwinProvider) Create(url, name, secret string) error {
56+
item := p.NewCreateItem(url, name, secret)
57+
err := keychain.AddItem(item)
58+
return p.ErrorWrap(url, err)
59+
}
60+
61+
func (p *DarwinProvider) NewRetrieveItem(url string) keychain.Item {
62+
item := keychain.NewItem()
63+
item.SetSecClass(keychain.SecClassGenericPassword)
64+
item.SetService(p.domain.Service)
65+
item.SetAccessGroup(p.domain.AccessGroup)
66+
item.SetAccount(url)
67+
item.SetMatchLimit(keychain.MatchLimitOne)
68+
item.SetReturnAttributes(true)
69+
item.SetReturnData(true)
70+
return item
71+
}
72+
73+
func (p *DarwinProvider) QueryItem(query keychain.Item) (keychain.QueryResult, error) {
74+
secret, err := keychain.QueryItem(query)
75+
if err != nil {
76+
return keychain.QueryResult{}, err
77+
} else if len(secret) < 1 {
78+
return keychain.QueryResult{}, keychain.ErrorItemNotFound
79+
} else if len(secret) > 1 {
80+
return keychain.QueryResult{}, keychain.ErrorDuplicateItem
81+
}
82+
return secret[0], nil
83+
}
84+
85+
func (p *DarwinProvider) Retrieve(url string) (string, string, error) {
86+
query := p.NewRetrieveItem(url)
87+
secret, err := p.QueryItem(query)
88+
if err != nil {
89+
return "", "", p.ErrorWrap(url, err)
90+
}
91+
return secret.Label, string(secret.Data), nil
92+
}
93+
94+
func (p *DarwinProvider) Update(url, name, secret string) error {
95+
oldItem := p.NewDeleteItem(url)
96+
newItem := p.NewCreateItem(url, name, secret)
97+
return p.ErrorWrap(url, keychain.UpdateItem(oldItem, newItem))
98+
}
99+
100+
func (p *DarwinProvider) NewDeleteItem(url string) keychain.Item {
101+
item := keychain.NewItem()
102+
item.SetSecClass(keychain.SecClassGenericPassword)
103+
item.SetService(p.domain.Service)
104+
item.SetAccessGroup(p.domain.AccessGroup)
105+
item.SetAccount(url)
106+
return item
107+
}
108+
109+
func (p *DarwinProvider) Delete(url string) error {
110+
item := p.NewDeleteItem(url)
111+
return p.ErrorWrap(url, keychain.DeleteItem(item))
112+
}

0 commit comments

Comments
 (0)