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 }