Files
kopia/repo/initialize.go
Jarek Kowalski 8b2b91f9f9 content: fixed repo upgrade version (#1286)
* content: fixed repo upgrade version

Previously upgrade would enable epoch manager and index v2 but would
not set the version of the format itself. Everything worked fine
but it would not protect from old kopia opening the repository.

* ci: added compatibility test that uses real 0.8 and current binaries
2021-09-10 22:51:51 -07:00

176 lines
4.8 KiB
Go

package repo
import (
"context"
"crypto/rand"
"io"
"os"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/gather"
"github.com/kopia/kopia/repo/blob"
"github.com/kopia/kopia/repo/content"
"github.com/kopia/kopia/repo/encryption"
"github.com/kopia/kopia/repo/hashing"
"github.com/kopia/kopia/repo/object"
"github.com/kopia/kopia/repo/splitter"
)
// BuildInfo is the build information of Kopia.
var (
BuildInfo = "unknown"
BuildVersion = "v0-unofficial"
BuildGitHubRepo = ""
)
const (
hmacSecretLength = 32
masterKeyLength = 32
uniqueIDLength = 32
)
// NewRepositoryOptions specifies options that apply to newly created repositories.
// All fields are optional, when not provided, reasonable defaults will be used.
type NewRepositoryOptions struct {
UniqueID []byte `json:"uniqueID"` // force the use of particular unique ID
BlockFormat content.FormattingOptions `json:"blockFormat"`
DisableHMAC bool `json:"disableHMAC"`
ObjectFormat object.Format `json:"objectFormat"` // object format
}
// ErrAlreadyInitialized indicates that repository has already been initialized.
var ErrAlreadyInitialized = errors.Errorf("repository already initialized")
// Initialize creates initial repository data structures in the specified storage with given credentials.
func Initialize(ctx context.Context, st blob.Storage, opt *NewRepositoryOptions, password string) error {
if opt == nil {
opt = &NewRepositoryOptions{}
}
// get the blob - expect ErrNotFound
var tmp gather.WriteBuffer
defer tmp.Close()
err := st.GetBlob(ctx, FormatBlobID, 0, -1, &tmp)
if err == nil {
return ErrAlreadyInitialized
}
if !errors.Is(err, blob.ErrBlobNotFound) {
return errors.Wrap(err, "unexpected error when checking for format blob")
}
format := formatBlobFromOptions(opt)
formatEncryptionKey, err := format.deriveFormatEncryptionKeyFromPassword(password)
if err != nil {
return errors.Wrap(err, "unable to derive format encryption key")
}
f, err := repositoryObjectFormatFromOptions(opt)
if err != nil {
return errors.Wrap(err, "invalid parameters")
}
if err := f.MutableParameters.Validate(); err != nil {
return errors.Wrap(err, "invalid parameters")
}
if err := encryptFormatBytes(format, f, formatEncryptionKey, format.UniqueID); err != nil {
return errors.Wrap(err, "unable to encrypt format bytes")
}
if err := writeFormatBlob(ctx, st, format); err != nil {
return errors.Wrap(err, "unable to write format blob")
}
return nil
}
func formatBlobFromOptions(opt *NewRepositoryOptions) *formatBlob {
return &formatBlob{
Tool: "https://github.com/kopia/kopia",
BuildInfo: BuildInfo,
BuildVersion: BuildVersion,
KeyDerivationAlgorithm: defaultKeyDerivationAlgorithm,
UniqueID: applyDefaultRandomBytes(opt.UniqueID, uniqueIDLength),
Version: "1",
EncryptionAlgorithm: defaultFormatEncryption,
}
}
func repositoryObjectFormatFromOptions(opt *NewRepositoryOptions) (*repositoryObjectFormat, error) {
fv := opt.BlockFormat.Version
if fv == 0 {
switch os.Getenv("KOPIA_REPOSITORY_FORMAT_VERSION") {
case "1":
fv = content.FormatVersion1
case "2":
fv = content.FormatVersion2
default:
fv = content.FormatVersion2
}
}
f := &repositoryObjectFormat{
FormattingOptions: content.FormattingOptions{
Hash: applyDefaultString(opt.BlockFormat.Hash, hashing.DefaultAlgorithm),
Encryption: applyDefaultString(opt.BlockFormat.Encryption, encryption.DefaultAlgorithm),
HMACSecret: applyDefaultRandomBytes(opt.BlockFormat.HMACSecret, hmacSecretLength),
MasterKey: applyDefaultRandomBytes(opt.BlockFormat.MasterKey, masterKeyLength),
MutableParameters: content.MutableParameters{
Version: fv,
MaxPackSize: applyDefaultInt(opt.BlockFormat.MaxPackSize, 20<<20), //nolint:gomnd
IndexVersion: applyDefaultInt(opt.BlockFormat.IndexVersion, content.DefaultIndexVersion),
EpochParameters: opt.BlockFormat.EpochParameters,
},
EnablePasswordChange: opt.BlockFormat.EnablePasswordChange,
},
Format: object.Format{
Splitter: applyDefaultString(opt.ObjectFormat.Splitter, splitter.DefaultAlgorithm),
},
}
if opt.DisableHMAC {
f.HMACSecret = nil
}
if err := f.FormattingOptions.ResolveFormatVersion(); err != nil {
return nil, errors.Wrap(err, "error resolving format version")
}
return f, nil
}
func randomBytes(n int) []byte {
b := make([]byte, n)
io.ReadFull(rand.Reader, b) //nolint:errcheck
return b
}
func applyDefaultInt(v, def int) int {
if v == 0 {
return def
}
return v
}
func applyDefaultString(v, def string) string {
if v == "" {
return def
}
return v
}
func applyDefaultRandomBytes(b []byte, n int) []byte {
if b == nil {
return randomBytes(n)
}
return b
}