GODT-2034: Basic vault migration ability (proof of concept)

This commit is contained in:
James Houlahan 2022-11-03 17:38:55 +01:00
parent 4c4c592f31
commit 6bf67917fb
11 changed files with 567 additions and 147 deletions

View File

@ -15,6 +15,9 @@ issues:
- at least one file in a package should have a package comment
# Package comments.
- "package-comments: should have a package comment"
# Migration uses underscores to make versions clearer.
- "var-naming: don't use underscores in Go names"
- "ST1003: should not use underscores in Go names"
exclude-rules:
- path: _test\.go

47
internal/vault/migrate.go Normal file
View File

@ -0,0 +1,47 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package vault
import "fmt"
type Version int
const (
v2_3_x Version = iota
v2_4_x
v2_5_x
Current = v2_5_x
)
// upgrade migrates the vault from the given version to the next version.
func upgrade(v Version, b []byte) ([]byte, error) {
switch v {
case v2_3_x:
return upgrade_2_3_x(b)
case v2_4_x:
return upgrade_2_4_x(b)
case Current:
return nil, fmt.Errorf("already at current version %d", Current)
default:
return nil, fmt.Errorf("unknown version %d", v)
}
}

View File

@ -0,0 +1,96 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package vault
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"os"
"path/filepath"
"testing"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/stretchr/testify/require"
"github.com/vmihailenco/msgpack/v5"
)
func TestMigrate(t *testing.T) {
dir := t.TempDir()
// Create a v2.3.x vault.
b := newLegacyVault(t, []byte("my secret key"), v2_3_x, Data_2_3_x{
Settings: Settings_2_3_x{
GluonDir: "v2.3.x-gluon-dir",
IMAPPort: "1234", // string in v2.3.x, current version uses int
SMTPPort: "5678", // string in v2.3.x, current version uses int
},
Users: []UserData_2_3_x{{
ID: "user-id", // called "ID" in v2.3.x, current version has "UserID"
Name: "user-name", // called "Name" in v2.3.x, current version has "Username"
GluonKey: []byte("gluon-key"), // []byte in v2.3.x and current version, string in v2.4.x (intermediate)
SplitMode: true, // bool in v2.3.x and v2.4.x, enum in current version
}},
})
// Write the vault to disk.
require.NoError(t, os.WriteFile(filepath.Join(dir, "vault.enc"), b, 0600))
// Migrate the vault.
s, corrupt, err := New(dir, "default-gluon-dir", []byte("my secret key"))
require.NoError(t, err)
require.False(t, corrupt)
// Check the migrated vault.
require.Equal(t, "v2.3.x-gluon-dir", s.GetGluonDir())
require.Equal(t, 1234, s.GetIMAPPort())
require.Equal(t, 5678, s.GetSMTPPort())
// The user should be migrated.
userIDs := s.GetUserIDs()
require.Len(t, userIDs, 1)
// The migrated user should be correct.
require.NoError(t, s.GetUser("user-id", func(user *User) {
require.Equal(t, "user-id", user.UserID())
require.Equal(t, "user-name", user.Username())
require.Equal(t, []byte("gluon-key"), user.GluonKey())
require.Equal(t, SplitMode, user.AddressMode())
}))
}
func newLegacyVault[T any](t *testing.T, key []byte, version Version, data T) []byte {
hash256 := sha256.Sum256(key)
aes, err := aes.NewCipher(hash256[:])
require.NoError(t, err)
gcm, err := cipher.NewGCM(aes)
require.NoError(t, err)
b, err := msgpack.Marshal(data)
require.NoError(t, err)
dec, err := msgpack.Marshal(File{Version: version, Data: b})
require.NoError(t, err)
nonce, err := crypto.RandomToken(gcm.NonceSize())
require.NoError(t, err)
return gcm.Seal(nonce, nonce, dec, nil)
}

View File

@ -0,0 +1,90 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package vault
import (
"strconv"
"github.com/bradenaw/juniper/xslices"
"github.com/vmihailenco/msgpack/v5"
)
type Data_2_3_x struct {
Settings Settings_2_3_x
Users []UserData_2_3_x
}
func (data Data_2_3_x) migrate() Data_2_4_x {
return Data_2_4_x{
Settings: data.Settings.migrate(),
Users: xslices.Map(data.Users, func(user UserData_2_3_x) UserData_2_4_x { return user.migrate() }),
}
}
type Settings_2_3_x struct {
GluonDir string
IMAPPort string
SMTPPort string
}
func (settings Settings_2_3_x) migrate() Settings_2_4_x {
imapPort, err := strconv.Atoi(settings.IMAPPort)
if err != nil {
panic(err)
}
smtpPort, err := strconv.Atoi(settings.SMTPPort)
if err != nil {
panic(err)
}
return Settings_2_4_x{
GluonDir: settings.GluonDir,
IMAPPort: imapPort,
SMTPPort: smtpPort,
}
}
type UserData_2_3_x struct {
ID string
Name string
GluonKey []byte
SplitMode bool
}
func (user UserData_2_3_x) migrate() UserData_2_4_x {
return UserData_2_4_x{
UserID: user.ID,
Username: user.Name,
GluonKey: string(user.GluonKey),
SplitMode: user.SplitMode,
}
}
func upgrade_2_3_x(b []byte) ([]byte, error) {
var old Data_2_3_x
if err := msgpack.Unmarshal(b, &old); err != nil {
return nil, err
}
return msgpack.Marshal(old.migrate())
}

View File

@ -0,0 +1,86 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package vault
import (
"github.com/bradenaw/juniper/xslices"
"github.com/vmihailenco/msgpack/v5"
)
type Data_2_4_x struct {
Settings Settings_2_4_x
Users []UserData_2_4_x
}
func (data Data_2_4_x) migrate() Data {
return Data{
Settings: data.Settings.migrate(),
Users: xslices.Map(data.Users, func(user UserData_2_4_x) UserData { return user.migrate() }),
}
}
type Settings_2_4_x struct {
GluonDir string
IMAPPort int
SMTPPort int
}
func (settings Settings_2_4_x) migrate() Settings {
newSettings := newDefaultSettings(settings.GluonDir)
newSettings.IMAPPort = settings.IMAPPort
newSettings.SMTPPort = settings.SMTPPort
return newSettings
}
type UserData_2_4_x struct {
UserID string
Username string
GluonKey string
SplitMode bool
}
func (user UserData_2_4_x) migrate() UserData {
var mode AddressMode
if user.SplitMode {
mode = SplitMode
} else {
mode = CombinedMode
}
return UserData{
UserID: user.UserID,
Username: user.Username,
GluonKey: []byte(user.GluonKey),
AddressMode: mode,
}
}
func upgrade_2_4_x(b []byte) ([]byte, error) {
var old Data_2_4_x
if err := msgpack.Unmarshal(b, &old); err != nil {
return nil, err
}
return msgpack.Marshal(old.migrate())
}

View File

@ -0,0 +1,52 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package vault
import "github.com/ProtonMail/proton-bridge/v2/internal/certs"
type Certs struct {
Bridge Cert
Installed bool
}
type Cert struct {
Cert, Key []byte
}
func newDefaultCerts() Certs {
return Certs{
Bridge: newTLSCert(),
}
}
func newTLSCert() Cert {
template, err := certs.NewTLSTemplate()
if err != nil {
panic(err)
}
certPEM, keyPEM, err := certs.GenerateCert(template)
if err != nil {
panic(err)
}
return Cert{
Cert: certPEM,
Key: keyPEM,
}
}

View File

@ -0,0 +1,32 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package vault
type Data struct {
Settings Settings
Users []UserData
Cookies []byte
Certs Certs
}
func newDefaultData(gluonDir string) Data {
return Data{
Settings: newDefaultSettings(gluonDir),
Certs: newDefaultCerts(),
}
}

View File

@ -0,0 +1,78 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package vault
import (
"crypto/cipher"
"github.com/ProtonMail/gopenpgp/v2/crypto"
"github.com/vmihailenco/msgpack/v5"
)
// File holds a versioned, serialized data.
type File struct {
Version Version
Data []byte
}
func unmarshalFile[T any](gcm cipher.AEAD, enc []byte, data *T) error {
dec, err := gcm.Open(nil, enc[:gcm.NonceSize()], enc[gcm.NonceSize():], nil)
if err != nil {
return err
}
var f File
if err := msgpack.Unmarshal(dec, &f); err != nil {
return err
}
for v := f.Version; v < Current; v++ {
b, err := upgrade(v, f.Data)
if err != nil {
return err
}
f.Data = b
}
if err := msgpack.Unmarshal(f.Data, data); err != nil {
return err
}
return nil
}
func marshalFile[T any](gcm cipher.AEAD, t T) ([]byte, error) {
b, err := msgpack.Marshal(t)
if err != nil {
return nil, err
}
dec, err := msgpack.Marshal(File{Version: Current, Data: b})
if err != nil {
return nil, err
}
nonce, err := crypto.RandomToken(gcm.NonceSize())
if err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, dec, nil), nil
}

View File

@ -0,0 +1,77 @@
// Copyright (c) 2022 Proton AG
//
// This file is part of Proton Mail Bridge.
//
// Proton Mail 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.
//
// Proton Mail 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 Proton Mail Bridge. If not, see <https://www.gnu.org/licenses/>.
package vault
import (
"math/rand"
"runtime"
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
)
type Settings struct {
GluonDir string
IMAPPort int
SMTPPort int
IMAPSSL bool
SMTPSSL bool
UpdateChannel updater.Channel
UpdateRollout float64
ColorScheme string
ProxyAllowed bool
ShowAllMail bool
Autostart bool
AutoUpdate bool
LastVersion string
FirstStart bool
FirstStartGUI bool
SyncWorkers int
SyncBuffer int
}
func newDefaultSettings(gluonDir string) Settings {
return Settings{
GluonDir: gluonDir,
IMAPPort: 1143,
SMTPPort: 1025,
IMAPSSL: false,
SMTPSSL: false,
UpdateChannel: updater.DefaultUpdateChannel,
UpdateRollout: rand.Float64(), //nolint:gosec
ColorScheme: "",
ProxyAllowed: true,
ShowAllMail: true,
Autostart: false,
AutoUpdate: true,
LastVersion: "0.0.0",
FirstStart: true,
FirstStartGUI: true,
SyncWorkers: runtime.NumCPU(),
SyncBuffer: runtime.NumCPU(),
}
}

View File

@ -17,81 +17,7 @@
package vault
import (
"math/rand"
"runtime"
"github.com/ProtonMail/gluon/imap"
"github.com/ProtonMail/proton-bridge/v2/internal/updater"
)
type Data struct {
Settings Settings
Users []UserData
Cookies []byte
Certs Certs
}
type Certs struct {
Bridge Cert
Installed bool
}
type Cert struct {
Cert, Key []byte
}
type Settings struct {
GluonDir string
IMAPPort int
SMTPPort int
IMAPSSL bool
SMTPSSL bool
UpdateChannel updater.Channel
UpdateRollout float64
ColorScheme string
ProxyAllowed bool
ShowAllMail bool
Autostart bool
AutoUpdate bool
LastVersion string
FirstStart bool
FirstStartGUI bool
SyncWorkers int
SyncBuffer int
}
func newDefaultSettings(gluonDir string) Settings {
return Settings{
GluonDir: gluonDir,
IMAPPort: 1143,
SMTPPort: 1025,
IMAPSSL: false,
SMTPSSL: false,
UpdateChannel: updater.DefaultUpdateChannel,
UpdateRollout: rand.Float64(), //nolint:gosec
ColorScheme: "",
ProxyAllowed: true,
ShowAllMail: true,
Autostart: false,
AutoUpdate: true,
LastVersion: "0.0.0",
FirstStart: true,
FirstStartGUI: true,
SyncWorkers: runtime.NumCPU(),
SyncBuffer: runtime.NumCPU(),
}
}
import "github.com/ProtonMail/gluon/imap"
// UserData holds information about a single bridge user.
// The user may or may not be logged in.

View File

@ -20,7 +20,6 @@ package vault
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"errors"
"fmt"
@ -29,10 +28,8 @@ import (
"path/filepath"
"sync"
"github.com/ProtonMail/proton-bridge/v2/internal/certs"
"github.com/bradenaw/juniper/xslices"
"github.com/sirupsen/logrus"
"github.com/vmihailenco/msgpack/v5"
)
// Vault is an encrypted data vault that stores bridge and user data.
@ -228,9 +225,7 @@ func newVault(path, gluonDir string, gcm cipher.AEAD) (*Vault, bool, error) {
var corrupt bool
if dec, err := decrypt(gcm, enc); err != nil {
corrupt = true
} else if err := msgpack.Unmarshal(dec, new(Data)); err != nil {
if err := unmarshalFile(gcm, enc, new(Data)); err != nil {
corrupt = true
}
@ -255,14 +250,9 @@ func (vault *Vault) get() Data {
vault.encLock.RLock()
defer vault.encLock.RUnlock()
dec, err := decrypt(vault.gcm, vault.enc)
if err != nil {
panic(err)
}
var data Data
if err := msgpack.Unmarshal(dec, &data); err != nil {
if err := unmarshalFile(vault.gcm, vault.enc, &data); err != nil {
panic(err)
}
@ -273,25 +263,15 @@ func (vault *Vault) mod(fn func(data *Data)) error {
vault.encLock.Lock()
defer vault.encLock.Unlock()
dec, err := decrypt(vault.gcm, vault.enc)
if err != nil {
return err
}
var data Data
if err := msgpack.Unmarshal(dec, &data); err != nil {
if err := unmarshalFile(vault.gcm, vault.enc, &data); err != nil {
return err
}
fn(&data)
mod, err := msgpack.Marshal(data)
if err != nil {
return err
}
enc, err := encrypt(vault.gcm, mod)
enc, err := marshalFile(vault.gcm, data)
if err != nil {
return err
}
@ -318,23 +298,7 @@ func (vault *Vault) modUser(userID string, fn func(userData *UserData)) error {
}
func initVault(path, gluonDir string, gcm cipher.AEAD) ([]byte, error) {
bridgeCert, err := newTLSCert()
if err != nil {
return nil, err
}
dec, err := msgpack.Marshal(Data{
Settings: newDefaultSettings(gluonDir),
Certs: Certs{
Bridge: bridgeCert,
},
})
if err != nil {
return nil, err
}
enc, err := encrypt(gcm, dec)
enc, err := marshalFile(gcm, newDefaultData(gluonDir))
if err != nil {
return nil, err
}
@ -345,34 +309,3 @@ func initVault(path, gluonDir string, gcm cipher.AEAD) ([]byte, error) {
return enc, nil
}
func decrypt(gcm cipher.AEAD, enc []byte) ([]byte, error) {
return gcm.Open(nil, enc[:gcm.NonceSize()], enc[gcm.NonceSize():], nil)
}
func encrypt(gcm cipher.AEAD, data []byte) ([]byte, error) {
nonce := make([]byte, gcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
return gcm.Seal(nonce, nonce, data, nil), nil
}
func newTLSCert() (Cert, error) {
template, err := certs.NewTLSTemplate()
if err != nil {
return Cert{}, err
}
certPEM, keyPEM, err := certs.GenerateCert(template)
if err != nil {
return Cert{}, err
}
return Cert{
Cert: certPEM,
Key: keyPEM,
}, nil
}