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

// 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
}