parent
612659eecb
commit
6aecc72d8b
15 changed files with 126 additions and 814 deletions
@ -1,22 +0,0 @@ |
||||
// Copied from github.com/docker/docker-credential-helpers to aviod dependency
|
||||
// on cgo. MIT License. Copyright (c) 2016 David Calavera
|
||||
|
||||
package keychain |
||||
|
||||
type Credentials struct { |
||||
ServerURL string |
||||
Username string |
||||
Secret string |
||||
} |
||||
|
||||
type Helper interface { |
||||
// Add appends credentials to the store.
|
||||
Add(*Credentials) error |
||||
// Delete removes credentials from the store.
|
||||
Delete(serverURL string) error |
||||
// Get retrieves credentials from the store.
|
||||
// It returns username and secret as strings.
|
||||
Get(serverURL string) (string, string, error) |
||||
// List returns the stored serverURLs and their associated usernames.
|
||||
List() (map[string]string, error) |
||||
} |
||||
@ -1,113 +0,0 @@ |
||||
// Copyright (c) 2021 Proton Technologies AG
|
||||
// Copyright (c) 2022 Lukasz Janyst <lukasz@jany.st>
|
||||
//
|
||||
// This file is part of Peroxide.
|
||||
//
|
||||
// Peroxide is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Peroxide is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Peroxide. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package keychain |
||||
|
||||
import ( |
||||
"fmt" |
||||
"sync" |
||||
) |
||||
|
||||
// Version is the keychain data version.
|
||||
const Version = "k11" |
||||
|
||||
// NewKeychain creates a new native keychain.
|
||||
func NewKeychain(keychainName string) (*Keychain, error) { |
||||
helper, err := newStaticKeychain() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
return newKeychain(helper, fmt.Sprintf("protonmail/%v/users", keychainName)), nil |
||||
} |
||||
|
||||
func newKeychain(helper Helper, url string) *Keychain { |
||||
return &Keychain{ |
||||
helper: helper, |
||||
url: url, |
||||
locker: &sync.Mutex{}, |
||||
} |
||||
} |
||||
|
||||
type Keychain struct { |
||||
helper Helper |
||||
url string |
||||
locker sync.Locker |
||||
} |
||||
|
||||
func (kc *Keychain) List() ([]string, error) { |
||||
kc.locker.Lock() |
||||
defer kc.locker.Unlock() |
||||
|
||||
userIDsByURL, err := kc.helper.List() |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
var userIDs []string // nolint[prealloc]
|
||||
|
||||
for url, userID := range userIDsByURL { |
||||
if url != kc.secretURL(userID) { |
||||
continue |
||||
} |
||||
|
||||
userIDs = append(userIDs, userID) |
||||
} |
||||
|
||||
return userIDs, nil |
||||
} |
||||
|
||||
func (kc *Keychain) Delete(userID string) error { |
||||
kc.locker.Lock() |
||||
defer kc.locker.Unlock() |
||||
|
||||
userIDsByURL, err := kc.helper.List() |
||||
if err != nil { |
||||
return err |
||||
} |
||||
|
||||
if _, ok := userIDsByURL[kc.secretURL(userID)]; !ok { |
||||
return nil |
||||
} |
||||
|
||||
return kc.helper.Delete(kc.secretURL(userID)) |
||||
} |
||||
|
||||
// Get returns the username and secret for the given userID.
|
||||
func (kc *Keychain) Get(userID string) (string, string, error) { |
||||
kc.locker.Lock() |
||||
defer kc.locker.Unlock() |
||||
|
||||
return kc.helper.Get(kc.secretURL(userID)) |
||||
} |
||||
|
||||
func (kc *Keychain) Put(userID, secret string) error { |
||||
kc.locker.Lock() |
||||
defer kc.locker.Unlock() |
||||
|
||||
return kc.helper.Add(&Credentials{ |
||||
ServerURL: kc.secretURL(userID), |
||||
Username: userID, |
||||
Secret: secret, |
||||
}) |
||||
} |
||||
|
||||
// secretURL returns the URL referring to a userID's secrets.
|
||||
func (kc *Keychain) secretURL(userID string) string { |
||||
return fmt.Sprintf("%v/%v", kc.url, userID) |
||||
} |
||||
@ -1,148 +0,0 @@ |
||||
// Copyright (c) 2022 Proton Technologies AG
|
||||
//
|
||||
// This file is part of ProtonMail Bridge.
|
||||
//
|
||||
// ProtonMail Bridge is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ProtonMail Bridge is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with ProtonMail Bridge. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package keychain |
||||
|
||||
import ( |
||||
"encoding/base64" |
||||
"testing" |
||||
|
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
var suffix = []byte("\x00avoidFix\x00\x00\x00\x00\x00\x00\x00") //nolint[gochecknoglobals]
|
||||
|
||||
var testData = map[string]string{ //nolint[gochecknoglobals]
|
||||
"user1": base64.StdEncoding.EncodeToString(append([]byte("data1"), suffix...)), |
||||
"user2": base64.StdEncoding.EncodeToString(append([]byte("data2"), suffix...)), |
||||
} |
||||
|
||||
func TestInsertReadRemove(t *testing.T) { |
||||
keychain := newKeychain(newTestHelper(), "protonmail/bridge/users") |
||||
|
||||
for id, secret := range testData { |
||||
expectedList, _ := keychain.List() |
||||
// Add expected secrets.
|
||||
expectedSecret := secret |
||||
require.NoError(t, keychain.Put(id, expectedSecret)) |
||||
|
||||
// Check list.
|
||||
actualList, err := keychain.List() |
||||
require.NoError(t, err) |
||||
expectedList = append(expectedList, id) |
||||
require.ElementsMatch(t, expectedList, actualList) |
||||
|
||||
// Get and check what was inserted.
|
||||
_, actualSecret, err := keychain.Get(id) |
||||
require.NoError(t, err) |
||||
require.Equal(t, expectedSecret, actualSecret) |
||||
|
||||
// Put what changed.
|
||||
|
||||
expectedSecret = "edited_" + id |
||||
expectedSecret = base64.StdEncoding.EncodeToString(append([]byte(expectedSecret), suffix...)) |
||||
|
||||
nJobs := 100 |
||||
nWorkers := 3 |
||||
jobs := make(chan interface{}, nJobs) |
||||
done := make(chan interface{}) |
||||
for i := 0; i < nWorkers; i++ { |
||||
go func() { |
||||
for { |
||||
_, more := <-jobs |
||||
if more { |
||||
require.NoError(t, keychain.Put(id, expectedSecret)) |
||||
} else { |
||||
done <- nil |
||||
return |
||||
} |
||||
} |
||||
}() |
||||
} |
||||
|
||||
for i := 0; i < nJobs; i++ { |
||||
jobs <- nil |
||||
} |
||||
close(jobs) |
||||
for i := 0; i < nWorkers; i++ { |
||||
<-done |
||||
} |
||||
|
||||
// Check list.
|
||||
actualList, err = keychain.List() |
||||
require.NoError(t, err) |
||||
require.ElementsMatch(t, expectedList, actualList) |
||||
|
||||
// Get and check what changed.
|
||||
_, actualSecret, err = keychain.Get(id) |
||||
require.NoError(t, err) |
||||
require.Equal(t, expectedSecret, actualSecret) |
||||
|
||||
if id != "user1" { |
||||
// Remove.
|
||||
err = keychain.Delete(id) |
||||
require.NoError(t, err) |
||||
|
||||
// Check removed.
|
||||
actualList, err = keychain.List() |
||||
require.NoError(t, err) |
||||
expectedList = expectedList[:len(expectedList)-1] |
||||
require.ElementsMatch(t, expectedList, actualList) |
||||
} |
||||
} |
||||
|
||||
// Clear first.
|
||||
require.NoError(t, keychain.Delete("user1")) |
||||
|
||||
actualList, err := keychain.List() |
||||
require.NoError(t, err) |
||||
for id := range testData { |
||||
require.NotContains(t, actualList, id) |
||||
} |
||||
} |
||||
|
||||
type testHelper map[string]*Credentials |
||||
|
||||
func newTestHelper() testHelper { |
||||
return make(testHelper) |
||||
} |
||||
|
||||
func (h testHelper) Add(creds *Credentials) error { |
||||
h[creds.ServerURL] = creds |
||||
return nil |
||||
} |
||||
|
||||
func (h testHelper) Delete(url string) error { |
||||
delete(h, url) |
||||
return nil |
||||
} |
||||
|
||||
func (h testHelper) Get(url string) (string, string, error) { |
||||
creds := h[url] |
||||
|
||||
return creds.Username, creds.Secret, nil |
||||
} |
||||
|
||||
func (h testHelper) List() (map[string]string, error) { |
||||
list := make(map[string]string) |
||||
|
||||
for url, creds := range h { |
||||
list[url] = creds.Username |
||||
} |
||||
|
||||
return list, nil |
||||
} |
||||
@ -1,178 +0,0 @@ |
||||
// Copyright (c) 2022 Lukasz Janyst <lukasz@jany.st>
|
||||
//
|
||||
// This file is part of Peroxide.
|
||||
//
|
||||
// Peroxide is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// Peroxide is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with Peroxide. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package keychain |
||||
|
||||
import ( |
||||
"crypto/rand" |
||||
"encoding/base64" |
||||
"encoding/json" |
||||
"fmt" |
||||
"io" |
||||
"io/ioutil" |
||||
"os" |
||||
|
||||
"golang.org/x/crypto/nacl/secretbox" |
||||
|
||||
"github.com/ljanyst/peroxide/pkg/files" |
||||
) |
||||
|
||||
const path = "~/.peroxide-creds" |
||||
|
||||
var secretKey [32]byte |
||||
|
||||
// Stores credentials in a file in user's home directory
|
||||
type Static struct { |
||||
credentials map[string]Credentials |
||||
} |
||||
|
||||
func (s *Static) Add(c *Credentials) error { |
||||
s.credentials[c.ServerURL] = *c |
||||
return dumpStaticStore(path, s.credentials) |
||||
} |
||||
|
||||
func (s *Static) Delete(serverURL string) error { |
||||
delete(s.credentials, serverURL) |
||||
return dumpStaticStore(path, s.credentials) |
||||
} |
||||
|
||||
func (s *Static) Get(serverURL string) (string, string, error) { |
||||
if cred, ok := s.credentials[serverURL]; ok { |
||||
return cred.Username, cred.Secret, nil |
||||
} |
||||
return "", "", fmt.Errorf("no usernames for %s", serverURL) |
||||
} |
||||
|
||||
func (s *Static) List() (map[string]string, error) { |
||||
list := make(map[string]string) |
||||
for _, v := range s.credentials { |
||||
list[v.ServerURL] = v.Username |
||||
} |
||||
return list, nil |
||||
} |
||||
|
||||
func decryptSecrets(credentials map[string]Credentials) (map[string]Credentials, error) { |
||||
creds := make(map[string]Credentials) |
||||
for k, v := range credentials { |
||||
encrypted, err := base64.StdEncoding.DecodeString(v.Secret) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("Unable to decode the secret for server %s: %s", k, err) |
||||
} |
||||
|
||||
// Read the nonce
|
||||
var nonce [24]byte |
||||
copy(nonce[:], encrypted[:24]) |
||||
|
||||
// Decrypt
|
||||
decrypted, ok := secretbox.Open(nil, encrypted[24:], &nonce, &secretKey) |
||||
if !ok { |
||||
return nil, fmt.Errorf("Unable to decrypt the secret for server %s", k) |
||||
} |
||||
|
||||
creds[k] = Credentials{ |
||||
ServerURL: v.ServerURL, |
||||
Username: v.Username, |
||||
Secret: string(decrypted), |
||||
} |
||||
} |
||||
return creds, nil |
||||
} |
||||
|
||||
func encryptSecrets(credentials map[string]Credentials) (map[string]Credentials, error) { |
||||
creds := make(map[string]Credentials) |
||||
for k, v := range credentials { |
||||
// Nonce must be different for each message
|
||||
var nonce [24]byte |
||||
if _, err := io.ReadFull(rand.Reader, nonce[:]); err != nil { |
||||
return nil, err |
||||
} |
||||
|
||||
// Encrypt the secret and append the result to the nonce
|
||||
encrypted := secretbox.Seal(nonce[:], []byte(v.Secret), &nonce, &secretKey) |
||||
creds[k] = Credentials{ |
||||
ServerURL: v.ServerURL, |
||||
Username: v.Username, |
||||
Secret: base64.StdEncoding.EncodeToString(encrypted), |
||||
} |
||||
} |
||||
return creds, nil |
||||
} |
||||
|
||||
func loadStaticStore(path string) (map[string]Credentials, error) { |
||||
fileName := files.ExpandTilde(path) |
||||
data, err := ioutil.ReadFile(fileName) |
||||
if err != nil && !os.IsNotExist(err) { |
||||
return nil, fmt.Errorf("Unable to read the credential store file %s: %s", fileName, err) |
||||
} |
||||
|
||||
creds := make(map[string]Credentials) |
||||
|
||||
if os.IsNotExist(err) { |
||||
return creds, nil |
||||
} |
||||
|
||||
err = json.Unmarshal(data, &creds) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("Malformed credential store %s: %s", fileName, err) |
||||
} |
||||
|
||||
return decryptSecrets(creds) |
||||
} |
||||
|
||||
func dumpStaticStore(path string, credentials map[string]Credentials) error { |
||||
creds, err := encryptSecrets(credentials) |
||||
fileName := files.ExpandTilde(path) |
||||
if err != nil { |
||||
return fmt.Errorf("Unable to encrypt the credential store %s: %s", fileName, err) |
||||
} |
||||
|
||||
data, err := json.MarshalIndent(creds, "", " ") |
||||
if err != nil { |
||||
return fmt.Errorf("Unable to serialize the credential store %s: %s", fileName, err) |
||||
} |
||||
|
||||
err = ioutil.WriteFile(fileName, data, 0600) |
||||
if err != nil { |
||||
return fmt.Errorf("Unable to write the credential store file %s: %s", fileName, err) |
||||
} |
||||
|
||||
return nil |
||||
} |
||||
|
||||
func newStaticKeychain() (Helper, error) { |
||||
key := os.Getenv("PEROXIDE_CREDENTIALS_KEY") |
||||
if key == "" { |
||||
return nil, fmt.Errorf("PEROXIDE_CREDENTIALS_KEY envvar not set") |
||||
} |
||||
|
||||
keyBytes, err := base64.StdEncoding.DecodeString(key) |
||||
if err != nil { |
||||
return nil, fmt.Errorf("Unable to decode the credentials key: %s", err) |
||||
} |
||||
|
||||
if len(keyBytes) != len(secretKey) { |
||||
return nil, fmt.Errorf("Decoded credentials key is not %d bytes long", len(secretKey)) |
||||
} |
||||
|
||||
copy(secretKey[:], keyBytes) |
||||
|
||||
creds, err := loadStaticStore(path) |
||||
if err != nil { |
||||
return nil, err |
||||
} |
||||
return &Static{creds}, nil |
||||
} |
||||
Loading…
Reference in new issue