You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
339 lines
6.8 KiB
339 lines
6.8 KiB
// 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 credentials |
|
|
|
import ( |
|
"encoding/base64" |
|
"encoding/json" |
|
"errors" |
|
"os" |
|
"sort" |
|
"sync" |
|
|
|
"github.com/sirupsen/logrus" |
|
) |
|
|
|
var ( |
|
ErrNotFound = errors.New("Credentials not found") |
|
ErrLocked = errors.New("Credentials are locked") |
|
ErrDecryptionFailed = errors.New("Decryption of credentials failed") |
|
ErrEncryptionFailed = errors.New("Encryption of credentials failed") |
|
ErrUnauthorized = errors.New("Bridge credentials checking failed") |
|
ErrAlreadyExists = errors.New("Credential already exists") |
|
ErrCantRemoveMainSlot = errors.New("Cannot remove the main key slot") |
|
log = logrus.WithField("pkg", "credentials") |
|
) |
|
|
|
// Store is an encrypted credentials store. |
|
type Store struct { |
|
lock sync.RWMutex |
|
creds map[string]*Credentials |
|
filePath string |
|
} |
|
|
|
// NewStore creates a new encrypted credentials store. |
|
func NewStore(filePath string) (*Store, error) { |
|
s := &Store{ |
|
creds: make(map[string]*Credentials), |
|
filePath: filePath, |
|
} |
|
|
|
if err := s.loadCredentials(); err != nil { |
|
return nil, err |
|
} |
|
|
|
return s, nil |
|
} |
|
|
|
func (s *Store) Add(userID, userName, uid, ref string, mailboxPassword []byte, emails []string) (*Credentials, []byte, error) { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
log.WithFields(logrus.Fields{ |
|
"user": userID, |
|
"username": userName, |
|
"emails": emails, |
|
}).Trace("Adding new credentials") |
|
|
|
creds := &Credentials{ |
|
UserID: userID, |
|
Name: userName, |
|
Emails: emails, |
|
Secret: Secret{ |
|
APIToken: uid + ":" + ref, |
|
MailboxPassword: mailboxPassword, |
|
}, |
|
SealedKeys: make(map[string][]byte), |
|
} |
|
|
|
_, ok := s.creds[userID] |
|
if ok { |
|
return nil, nil, ErrAlreadyExists |
|
} |
|
|
|
copy(creds.Key[:], GenerateKey(32)) |
|
|
|
var mainKey [32]byte |
|
copy(mainKey[:], GenerateKey(32)) |
|
if err := creds.SealKey("main", mainKey); err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
if err := creds.Encrypt(); err != nil { |
|
return nil, nil, err |
|
} |
|
|
|
s.creds[userID] = creds |
|
|
|
if err := s.saveCredentials(); err != nil { |
|
delete(s.creds, userID) |
|
return nil, nil, err |
|
} |
|
|
|
return creds, mainKey[:], nil |
|
} |
|
|
|
func (s *Store) UpdateEmails(userID string, emails []string) (*Credentials, error) { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
credentials, ok := s.creds[userID] |
|
if !ok { |
|
return nil, ErrNotFound |
|
} |
|
|
|
credentials.Emails = emails |
|
|
|
return credentials, s.saveCredentials() |
|
} |
|
|
|
func (s *Store) UpdatePassword(userID string, password []byte) (*Credentials, error) { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
credentials, ok := s.creds[userID] |
|
if !ok { |
|
return nil, ErrNotFound |
|
} |
|
|
|
if credentials.Locked() { |
|
return nil, ErrLocked |
|
} |
|
|
|
credentials.Secret.MailboxPassword = password |
|
if err := credentials.Encrypt(); err != nil { |
|
return nil, err |
|
} |
|
|
|
return credentials, s.saveCredentials() |
|
} |
|
|
|
func (s *Store) UpdateToken(userID, uid, ref string) (*Credentials, error) { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
credentials, ok := s.creds[userID] |
|
if !ok { |
|
return nil, ErrNotFound |
|
} |
|
|
|
if credentials.Locked() { |
|
return nil, ErrLocked |
|
} |
|
|
|
credentials.Secret.APIToken = uid + ":" + ref |
|
if err := credentials.Encrypt(); err != nil { |
|
return nil, err |
|
} |
|
|
|
return credentials, s.saveCredentials() |
|
} |
|
|
|
func (s *Store) ListKeySlots(userID string) ([]string, error) { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
credentials, ok := s.creds[userID] |
|
if !ok { |
|
return nil, ErrNotFound |
|
} |
|
|
|
slots := []string{} |
|
for k := range credentials.SealedKeys { |
|
if k != "main" { |
|
slots = append(slots, k) |
|
} |
|
} |
|
|
|
sort.Strings(slots) |
|
slots = append([]string{"main"}, slots...) |
|
|
|
return slots, nil |
|
} |
|
|
|
func (s *Store) RemoveKeySlot(userID, slot string) error { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
credentials, ok := s.creds[userID] |
|
if !ok { |
|
return ErrNotFound |
|
} |
|
|
|
if slot == "main" { |
|
return ErrCantRemoveMainSlot |
|
} |
|
|
|
key, ok := credentials.SealedKeys[slot] |
|
if !ok { |
|
return ErrNotFound |
|
} |
|
|
|
delete(credentials.SealedKeys, slot) |
|
|
|
if err := s.saveCredentials(); err != nil { |
|
credentials.SealedKeys[slot] = key |
|
return err |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func (s *Store) AddKeySlot(userID, slot, mainKey string) (string, error) { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
credentials, ok := s.creds[userID] |
|
if !ok { |
|
return "", ErrNotFound |
|
} |
|
|
|
_, ok = credentials.SealedKeys[slot] |
|
if ok { |
|
return "", ErrAlreadyExists |
|
} |
|
|
|
err := credentials.Unlock("main", mainKey) |
|
if err != nil { |
|
return "", err |
|
} |
|
|
|
var key [32]byte |
|
copy(key[:], GenerateKey(32)) |
|
if err := credentials.SealKey(slot, key); err != nil { |
|
return "", err |
|
} |
|
|
|
if err := s.saveCredentials(); err != nil { |
|
delete(credentials.SealedKeys, slot) |
|
return "", err |
|
} |
|
|
|
return base64.StdEncoding.EncodeToString(key[:]), nil |
|
} |
|
|
|
func (s *Store) Logout(userID string) (*Credentials, error) { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
credentials, ok := s.creds[userID] |
|
if !ok { |
|
return nil, ErrNotFound |
|
} |
|
|
|
if credentials.Locked() { |
|
return nil, ErrLocked |
|
} |
|
|
|
if err := credentials.Encrypt(); err != nil { |
|
return nil, err |
|
} |
|
|
|
credentials.logout() |
|
|
|
return credentials, s.saveCredentials() |
|
} |
|
|
|
// List returns a list of usernames that have credentials stored. |
|
func (s *Store) List() ([]string, error) { |
|
s.lock.RLock() |
|
defer s.lock.RUnlock() |
|
|
|
log.Trace("Listing credentials in credentials store") |
|
|
|
userIDs := []string{} |
|
for id := range s.creds { |
|
userIDs = append(userIDs, id) |
|
} |
|
sort.Strings(userIDs) |
|
|
|
return userIDs, nil |
|
} |
|
|
|
func (s *Store) Get(userID string) (creds *Credentials, err error) { |
|
s.lock.RLock() |
|
defer s.lock.RUnlock() |
|
|
|
creds, ok := s.creds[userID] |
|
if !ok { |
|
return nil, ErrNotFound |
|
} |
|
return creds, nil |
|
} |
|
|
|
// Delete removes credentials from the store. |
|
func (s *Store) Delete(userID string) (err error) { |
|
s.lock.Lock() |
|
defer s.lock.Unlock() |
|
|
|
_, ok := s.creds[userID] |
|
if !ok { |
|
return ErrNotFound |
|
} |
|
|
|
delete(s.creds, userID) |
|
return nil |
|
} |
|
|
|
func (s *Store) saveCredentials() error { |
|
f, err := os.Create(s.filePath) |
|
if err != nil { |
|
return err |
|
} |
|
defer f.Close() |
|
|
|
return json.NewEncoder(f).Encode(s.creds) |
|
} |
|
|
|
func (s *Store) loadCredentials() error { |
|
f, err := os.Open(s.filePath) |
|
if os.IsNotExist(err) { |
|
return nil |
|
} |
|
|
|
if err != nil { |
|
return err |
|
} |
|
defer f.Close() |
|
|
|
if err := json.NewDecoder(f).Decode(&s.creds); err != nil { |
|
return err |
|
} |
|
|
|
return nil |
|
}
|
|
|