refactor(general): user password hashing and key derivation helpers (#3821)

Code movement and simplification, no functional changes.

Objectives:
- Allow callers specifying the needed key (or hash) size, instead of
hard-coding it in the registered PBK derivers. Conceptually, the caller
needs to specify the key size, since that is a requirement of the
(encryption) algorithm being used in the caller. Now, the code changes
here do not result in any functional changes since the key size is
always 32 bytes.
- Remove a global definition for the default PB key deriver to use.
Instead, each of the 3 use case sets the default value.

Changes:
- `crypto.DeriveKeyFromPassword` now takes a key size.
- Adds new constants for the key sizes at the callers.
- Removes the global `crypto.MasterKeySize` const.
- Removes the global `crypto.DefaultKeyDerivationAlgorithm` const.
- Adds const for the default derivation algorithms for each use case.
- Adds a const for the salt length in the `internal/user` package, to ensure
  the same salt length is used in both hash versions.
- Unexports various functions, variables and constants in the `internal/crypto`
  & `internal/user` packages.
- Renames various constants for consistency.
- Removes unused functions and symbols.
- Renames files to be consistent and better reflect the structure of the code.
- Adds a couple of tests to ensure the const values are in sync and supported.
- Fixes a couple of typos

Followups to:
- #3725
- #3770
- #3779
- #3799
- #3816

The individual commits show the code transformations to simplify the
review of the changes.
This commit is contained in:
Julio López
2024-04-26 23:30:56 -07:00
committed by GitHub
parent 2db8b20ed9
commit ca1962f6e4
19 changed files with 184 additions and 181 deletions

View File

@@ -6,7 +6,6 @@
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/crypto"
"github.com/kopia/kopia/internal/passwordpersist"
"github.com/kopia/kopia/repo"
)
@@ -31,14 +30,14 @@ func (c *commandRepositoryConnectServer) setup(svc advancedAppServices, parent c
cmd.Flag("url", "Server URL").Required().StringVar(&c.connectAPIServerURL)
cmd.Flag("server-cert-fingerprint", "Server certificate fingerprint").StringVar(&c.connectAPIServerCertFingerprint)
//nolint:lll
cmd.Flag("local-cache-key-derivation-algorithm", "Key derivation algorithm used to derive the local cache encryption key").Hidden().Default(repo.DefaultKeyDerivationAlgorithm).EnumVar(&c.connectAPIServerLocalCacheKeyDerivationAlgorithm, crypto.AllowedKeyDerivationAlgorithms()...)
cmd.Flag("local-cache-key-derivation-algorithm", "Key derivation algorithm used to derive the local cache encryption key").Hidden().Default(repo.DefaultServerRepoCacheKeyDerivationAlgorithm).EnumVar(&c.connectAPIServerLocalCacheKeyDerivationAlgorithm, repo.SupportedLocalCacheKeyDerivationAlgorithms()...)
cmd.Action(svc.noRepositoryAction(c.run))
}
func (c *commandRepositoryConnectServer) run(ctx context.Context) error {
localCacheKeyDerivationAlgorithm := c.connectAPIServerLocalCacheKeyDerivationAlgorithm
if localCacheKeyDerivationAlgorithm == "" {
localCacheKeyDerivationAlgorithm = repo.DefaultKeyDerivationAlgorithm
localCacheKeyDerivationAlgorithm = repo.DefaultServerRepoCacheKeyDerivationAlgorithm
}
as := &repo.APIServerInfo{

View File

@@ -7,7 +7,6 @@
"github.com/alecthomas/kingpin/v2"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/crypto"
"github.com/kopia/kopia/repo"
"github.com/kopia/kopia/repo/blob"
"github.com/kopia/kopia/repo/ecc"
@@ -54,7 +53,7 @@ func (c *commandRepositoryCreate) setup(svc advancedAppServices, parent commandP
cmd.Flag("retention-mode", "Set the blob retention-mode for supported storage backends.").EnumVar(&c.retentionMode, blob.Governance.String(), blob.Compliance.String())
cmd.Flag("retention-period", "Set the blob retention-period for supported storage backends.").DurationVar(&c.retentionPeriod)
//nolint:lll
cmd.Flag("format-block-key-derivation-algorithm", "Algorithm to derive the encryption key for the format block from the repository password").Default(crypto.DefaultKeyDerivationAlgorithm).EnumVar(&c.createBlockKeyDerivationAlgorithm, crypto.AllowedKeyDerivationAlgorithms()...)
cmd.Flag("format-block-key-derivation-algorithm", "Algorithm to derive the encryption key for the format block from the repository password").Default(format.DefaultKeyDerivationAlgorithm).EnumVar(&c.createBlockKeyDerivationAlgorithm, format.SupportedFormatBlobKeyDerivationAlgorithms()...)
c.co.setup(svc, cmd)
c.svc = svc

View File

@@ -7,7 +7,6 @@
"github.com/alecthomas/kingpin/v2"
"github.com/pkg/errors"
"github.com/kopia/kopia/internal/crypto"
"github.com/kopia/kopia/internal/user"
"github.com/kopia/kopia/repo"
)
@@ -37,7 +36,7 @@ func (c *commandServerUserAddSet) setup(svc appServices, parent commandParent, i
cmd.Flag("ask-password", "Ask for user password").BoolVar(&c.userAskPassword)
cmd.Flag("user-password", "Password").StringVar(&c.userSetPassword)
cmd.Flag("user-password-hash", "Password hash").StringVar(&c.userSetPasswordHash)
cmd.Flag("user-password-hashing-algorithm", "[Experimental] Password hashing algorithm").Hidden().Default(crypto.DefaultKeyDerivationAlgorithm).EnumVar(&c.userSetPasswordHashAlgorithm, crypto.AllowedKeyDerivationAlgorithms()...)
cmd.Flag("user-password-hashing-algorithm", "[Experimental] Password hashing algorithm").Hidden().Default(user.DefaultPasswordHashingAlgorithm).EnumVar(&c.userSetPasswordHashAlgorithm, user.PasswordHashingAlgorithms()...)
cmd.Arg("username", "Username").Required().StringVar(&c.userSetName)
cmd.Action(svc.repositoryWriterAction(c.runServerUserAddSet))

View File

@@ -9,25 +9,16 @@
"github.com/pkg/errors"
)
const (
// MasterKeyLength describes the length of the master key.
MasterKeyLength = 32
)
// DefaultKeyDerivationAlgorithm is the key derivation algorithm for new configurations.
const DefaultKeyDerivationAlgorithm = ScryptAlgorithm
// KeyDeriver is an interface that contains methods for deriving a key from a password.
type KeyDeriver interface {
DeriveKeyFromPassword(password string, salt []byte) ([]byte, error)
RecommendedSaltLength() int
// passwordBasedKeyDeriver is an interface that contains methods for deriving a key from a password.
type passwordBasedKeyDeriver interface {
deriveKeyFromPassword(password string, salt []byte, keySize int) ([]byte, error)
}
//nolint:gochecknoglobals
var keyDerivers = map[string]KeyDeriver{}
var keyDerivers = map[string]passwordBasedKeyDeriver{}
// RegisterKeyDerivers registers various key derivation functions.
func RegisterKeyDerivers(name string, keyDeriver KeyDeriver) {
// registerPBKeyDeriver registers a password-based key deriver.
func registerPBKeyDeriver(name string, keyDeriver passwordBasedKeyDeriver) {
if _, ok := keyDerivers[name]; ok {
panic(fmt.Sprintf("key deriver (%s) is already registered", name))
}
@@ -36,28 +27,18 @@ func RegisterKeyDerivers(name string, keyDeriver KeyDeriver) {
}
// DeriveKeyFromPassword derives encryption key using the provided password and per-repository unique ID.
func DeriveKeyFromPassword(password string, salt []byte, algorithm string) ([]byte, error) {
func DeriveKeyFromPassword(password string, salt []byte, keySize int, algorithm string) ([]byte, error) {
kd, ok := keyDerivers[algorithm]
if !ok {
return nil, errors.Errorf("unsupported key derivation algorithm: %v, supported algorithms %v", algorithm, AllowedKeyDerivationAlgorithms())
return nil, errors.Errorf("unsupported key derivation algorithm: %v, supported algorithms %v", algorithm, supportedPBKeyDerivationAlgorithms())
}
//nolint:wrapcheck
return kd.DeriveKeyFromPassword(password, salt)
return kd.deriveKeyFromPassword(password, salt, keySize)
}
// RecommendedSaltLength returns the recommended salt length of a given key derivation algorithm.
func RecommendedSaltLength(algorithm string) (int, error) {
kd, ok := keyDerivers[algorithm]
if !ok {
return 0, errors.Errorf("failed to get salt length for unsupported key derivation algorithm: %v, supported algorithms %v", algorithm, AllowedKeyDerivationAlgorithms())
}
return kd.RecommendedSaltLength(), nil
}
// AllowedKeyDerivationAlgorithms returns a slice of the allowed key derivation algorithms.
func AllowedKeyDerivationAlgorithms() []string {
// supportedPBKeyDerivationAlgorithms returns a slice of the allowed key derivation algorithms.
func supportedPBKeyDerivationAlgorithms() []string {
kdAlgorithms := make([]string, 0, len(keyDerivers))
for k := range keyDerivers {
kdAlgorithms = append(kdAlgorithms, k)

View File

@@ -10,23 +10,13 @@
)
const (
// MasterKeyLength describes the length of the master key.
MasterKeyLength = 32
V1SaltLength = 32
ScryptAlgorithm = "scrypt-65536-8-1"
Pbkdf2Algorithm = "pbkdf2-sha256-600000"
// DefaultKeyDerivationAlgorithm is the key derivation algorithm for new configurations.
DefaultKeyDerivationAlgorithm = ScryptAlgorithm
)
// DeriveKeyFromPassword derives encryption key using the provided password and per-repository unique ID.
func DeriveKeyFromPassword(password string, salt []byte, algorithm string) ([]byte, error) {
const masterKeySize = 32
func DeriveKeyFromPassword(password string, salt []byte, keySize int, algorithm string) ([]byte, error) {
switch algorithm {
case ScryptAlgorithm, Pbkdf2Algorithm:
h := sha256.New()
@@ -41,11 +31,3 @@ func DeriveKeyFromPassword(password string, salt []byte, algorithm string) ([]by
return nil, errors.Errorf("unsupported key algorithm: %v", algorithm)
}
}
func RecommendedSaltLength(algorithm string) (int, error) {
return V1SaltLength, nil
}
func AllowedKeyDerivationAlgorithms() []string {
return []string{DefaultKeyDerivationAlgorithm}
}

View File

@@ -11,43 +11,37 @@
)
const (
// Pbkdf2Algorithm is the key for the pbkdf algorithm.
Pbkdf2Algorithm = "pbkdf2-sha256-600000"
// The NIST recommended minimum size for a salt for pbkdf2 is 16 bytes.
//
// TBD: However, a good rule of thumb is to use a salt that is the same size
// However, a good rule of thumb is to use a salt that is the same size
// as the output of the hash function. For example, the output of SHA256
// is 256 bits (32 bytes), so the salt should be at least 32 random bytes.
// See: https://crackstation.net/hashing-security.htm
minPbkdfSha256SaltSize = 32 // size in bytes == 128 bits
pbkdf2Sha256MinSaltLength = 32 // 256 bits
// The NIST recommended iterations for PBKDF2 with SHA256 hash is 600,000.
pbkdf2Sha256Iterations = 600_000
// Pbkdf2Algorithm is the key for the pbkdf algorithm.
Pbkdf2Algorithm = "pbkdf2-sha256-600000"
)
func init() {
RegisterKeyDerivers(Pbkdf2Algorithm, &pbkdf2KeyDeriver{
iterations: pbkdf2Sha256Iterations,
recommendedSaltLength: minPbkdfSha256SaltSize,
minSaltLength: minPbkdfSha256SaltSize,
registerPBKeyDeriver(Pbkdf2Algorithm, &pbkdf2KeyDeriver{
iterations: pbkdf2Sha256Iterations,
minSaltLength: pbkdf2Sha256MinSaltLength,
})
}
type pbkdf2KeyDeriver struct {
iterations int
recommendedSaltLength int
minSaltLength int
iterations int
minSaltLength int
}
func (s *pbkdf2KeyDeriver) DeriveKeyFromPassword(password string, salt []byte) ([]byte, error) {
func (s *pbkdf2KeyDeriver) deriveKeyFromPassword(password string, salt []byte, keySize int) ([]byte, error) {
if len(salt) < s.minSaltLength {
return nil, errors.Errorf("required salt size is atleast %d bytes", s.minSaltLength)
}
return pbkdf2.Key([]byte(password), salt, s.iterations, MasterKeyLength, sha256.New), nil
}
func (s *pbkdf2KeyDeriver) RecommendedSaltLength() int {
return s.recommendedSaltLength
return pbkdf2.Key([]byte(password), salt, s.iterations, keySize, sha256.New), nil
}

View File

@@ -0,0 +1,52 @@
//go:build !testing
// +build !testing
package crypto
import (
"github.com/pkg/errors"
"golang.org/x/crypto/scrypt"
)
const (
// ScryptAlgorithm is the registration name for the scrypt algorithm instance.
ScryptAlgorithm = "scrypt-65536-8-1"
// The recommended minimum size for a salt to be used for scrypt.
// Currently set to 16 bytes (128 bits).
//
// A good rule of thumb is to use a salt that is the same size
// as the output of the hash function. For example, the output of SHA256
// is 256 bits (32 bytes), so the salt should be at least 32 random bytes.
// Scrypt uses a SHA256 hash function.
// https://crackstation.net/hashing-security.htm
scryptMinSaltLength = 16 // 128 bits
)
func init() {
registerPBKeyDeriver(ScryptAlgorithm, &scryptKeyDeriver{
n: 65536, //nolint:gomnd
r: 8, //nolint:gomnd
p: 1,
minSaltLength: scryptMinSaltLength,
})
}
type scryptKeyDeriver struct {
// n scryptCostParameterN is scrypt's CPU/memory cost parameter.
n int
// r scryptCostParameterR is scrypt's work factor.
r int
// p scryptCostParameterP is scrypt's parallelization parameter.
p int
minSaltLength int
}
func (s *scryptKeyDeriver) deriveKeyFromPassword(password string, salt []byte, keySize int) ([]byte, error) {
if len(salt) < s.minSaltLength {
return nil, errors.Errorf("required salt size is at least %d bytes", s.minSaltLength)
}
//nolint:wrapcheck
return scrypt.Key([]byte(password), salt, s.n, s.r, s.p, keySize)
}

View File

@@ -1,61 +0,0 @@
//go:build !testing
// +build !testing
package crypto
import (
"github.com/pkg/errors"
"golang.org/x/crypto/scrypt"
)
// The recommended minimum size for a salt to be used for scrypt.
// Currently set to 16 bytes (128 bits).
//
// TBD: A good rule of thumb is to use a salt that is the same size
// as the output of the hash function. For example, the output of SHA256
// is 256 bits (32 bytes), so the salt should be at least 32 random bytes.
// Scrypt uses a SHA256 hash function.
// https://crackstation.net/hashing-security.htm
const (
minScryptSha256SaltSize = 16 // size in bytes == 128 bits
// ScryptAlgorithm is the key for the scrypt algorithm.
ScryptAlgorithm = "scrypt-65536-8-1"
// Legacy hash version salt length.
V1SaltLength = 32
)
func init() {
RegisterKeyDerivers(ScryptAlgorithm, &scryptKeyDeriver{
n: 65536, //nolint:gomnd
r: 8, //nolint:gomnd
p: 1,
recommendedSaltLength: V1SaltLength,
minSaltLength: minScryptSha256SaltSize,
})
}
type scryptKeyDeriver struct {
// n scryptCostParameterN is scrypt's CPU/memory cost parameter.
n int
// r scryptCostParameterR is scrypt's work factor.
r int
// p scryptCostParameterP is scrypt's parallelization parameter.
p int
recommendedSaltLength int
minSaltLength int
}
func (s *scryptKeyDeriver) DeriveKeyFromPassword(password string, salt []byte) ([]byte, error) {
if len(salt) < s.minSaltLength {
return nil, errors.Errorf("required salt size is atleast %d bytes", s.minSaltLength)
}
//nolint:wrapcheck
return scrypt.Key([]byte(password), salt, s.n, s.r, s.p, MasterKeyLength)
}
func (s *scryptKeyDeriver) RecommendedSaltLength() int {
return s.recommendedSaltLength
}

View File

@@ -79,7 +79,7 @@ func StartServer(t *testing.T, env *repotesting.Environment, tls bool) *repo.API
asi.BaseURL = hs.URL
}
asi.LocalCacheKeyDerivationAlgorithm = repo.DefaultKeyDerivationAlgorithm
asi.LocalCacheKeyDerivationAlgorithm = repo.DefaultServerRepoCacheKeyDerivationAlgorithm
t.Cleanup(hs.Close)

View File

@@ -0,0 +1,9 @@
package user
// DefaultPasswordHashingAlgorithm is the default password hashing scheme for user profiles.
const DefaultPasswordHashingAlgorithm = scryptHashAlgorithm
// PasswordHashingAlgorithms returns the supported algorithms for user password hashing.
func PasswordHashingAlgorithms() []string {
return []string{scryptHashAlgorithm, pbkdf2HashAlgorithm}
}

View File

@@ -0,0 +1,32 @@
package user
import (
"testing"
"github.com/stretchr/testify/require"
"github.com/kopia/kopia/internal/crypto"
)
// The password hashing constants defined in this package are used as "lookup
// keys" for the register password-based key derivers in the crypto package.
// This trivial test is a change detector to ensure that the constants defined
// in the user package match those defined in the crypto package.
func TestPasswordHashingConstantMatchCryptoPackage(t *testing.T) {
require.Equal(t, crypto.ScryptAlgorithm, scryptHashAlgorithm)
require.Equal(t, crypto.Pbkdf2Algorithm, pbkdf2HashAlgorithm)
}
// The passwordHashSaltLength constant defines the salt length used in this
// package for password hashing. This trivial test ensures that this hash length
// meets the minimum requirement for the instantiations of the registered
// password hashers (PB key derivers in the crypto package).
func TestSaltLengthIsSupported(t *testing.T) {
const badPwd = "password"
var salt [passwordHashSaltLength]byte
for _, h := range PasswordHashingAlgorithms() {
_, err := computePasswordHash(badPwd, salt[:], h)
require.NoError(t, err)
}
}

View File

@@ -3,7 +3,6 @@
import (
"math/rand"
"github.com/kopia/kopia/internal/crypto"
"github.com/kopia/kopia/repo/manifest"
"github.com/pkg/errors"
@@ -12,13 +11,16 @@
const (
// ScryptHashVersion is the version representation of the scrypt algorithm.
ScryptHashVersion = 1
// ScryptHashAlgorithm is the scrypt password hashing algorithm. This must match crypto.ScryptAlgorithm.
ScryptHashAlgorithm = "scrypt-65536-8-1"
// scryptHashAlgorithm is the scrypt password hashing algorithm. This must match crypto.ScryptAlgorithm.
scryptHashAlgorithm = "scrypt-65536-8-1"
// Pbkdf2HashVersion is the version representation of the pbkdf2 algorithm.
Pbkdf2HashVersion = 2
// Pbkdf2HashAlgorithm is the pbkdf2 password hashing algorithm. This must match crypto.Pbkdf2Algorithm.
Pbkdf2HashAlgorithm = "pbkdf2-sha256-600000"
// pbkdf2HashAlgorithm is the pbkdf2 password hashing algorithm. This must match crypto.Pbkdf2Algorithm.
pbkdf2HashAlgorithm = "pbkdf2-sha256-600000"
passwordHashLength = 32
passwordHashSaltLength = 32
)
// Profile describes information about a single user.
@@ -46,17 +48,18 @@ func (p *Profile) IsValidPassword(password string) bool {
if p == nil {
invalidProfile = true
} else {
passwordHashAlgorithm, err = GetPasswordHashAlgorithm(p.PasswordHashVersion)
passwordHashAlgorithm, err = getPasswordHashAlgorithm(p.PasswordHashVersion)
if err != nil {
invalidProfile = true
}
}
if invalidProfile {
algorithms := crypto.AllowedKeyDerivationAlgorithms()
// if the Username is invalid, return false but use the same amount of time as when we
algorithms := PasswordHashingAlgorithms()
// if the user profile is invalid, either a non-existing user name or password
// hash version, then return false but use the same amount of time as when we
// compare against valid user to avoid revealing whether the user account exists.
isValidPassword(password, dummyV1HashThatNeverMatchesAnyPassword, algorithms[rand.Intn(len(algorithms))]) //nolint:gosec
isValidPassword(password, dummyHashThatNeverMatchesAnyPassword, algorithms[rand.Intn(len(algorithms))]) //nolint:gosec
return false
}
@@ -64,13 +67,13 @@ func (p *Profile) IsValidPassword(password string) bool {
return isValidPassword(password, p.PasswordHash, passwordHashAlgorithm)
}
// GetPasswordHashAlgorithm returns the password hash algorithm given a version.
func GetPasswordHashAlgorithm(passwordHashVersion int) (string, error) {
// getPasswordHashAlgorithm returns the password hash algorithm given a version.
func getPasswordHashAlgorithm(passwordHashVersion int) (string, error) {
switch passwordHashVersion {
case ScryptHashVersion:
return ScryptHashAlgorithm, nil
return scryptHashAlgorithm, nil
case Pbkdf2HashVersion:
return Pbkdf2HashAlgorithm, nil
return pbkdf2HashAlgorithm, nil
default:
return "", errors.Errorf("unsupported hash version (%d)", passwordHashVersion)
}
@@ -79,9 +82,9 @@ func GetPasswordHashAlgorithm(passwordHashVersion int) (string, error) {
// GetPasswordHashVersion returns the password hash version given an algorithm.
func GetPasswordHashVersion(passwordHashAlgorithm string) (int, error) {
switch passwordHashAlgorithm {
case ScryptHashAlgorithm:
case scryptHashAlgorithm:
return ScryptHashVersion, nil
case Pbkdf2HashAlgorithm:
case pbkdf2HashAlgorithm:
return Pbkdf2HashVersion, nil
default:
return 0, errors.Errorf("unsupported hash algorithm (%s)", passwordHashAlgorithm)

View File

@@ -11,20 +11,15 @@
)
//nolint:gochecknoglobals
var dummyV1HashThatNeverMatchesAnyPassword = make([]byte, crypto.MasterKeyLength+crypto.V1SaltLength)
var dummyHashThatNeverMatchesAnyPassword = make([]byte, passwordHashSaltLength+passwordHashLength)
func (p *Profile) setPassword(password string) error {
passwordHashAlgorithm, err := GetPasswordHashAlgorithm(p.PasswordHashVersion)
passwordHashAlgorithm, err := getPasswordHashAlgorithm(p.PasswordHashVersion)
if err != nil {
return err
}
saltLength, err := crypto.RecommendedSaltLength(passwordHashAlgorithm)
if err != nil {
return errors.Wrap(err, "error getting recommended salt length")
}
salt := make([]byte, saltLength)
salt := make([]byte, passwordHashSaltLength)
if _, err := io.ReadFull(rand.Reader, salt); err != nil {
return errors.Wrap(err, "error generating salt")
}
@@ -35,7 +30,7 @@ func (p *Profile) setPassword(password string) error {
}
func computePasswordHash(password string, salt []byte, keyDerivationAlgorithm string) ([]byte, error) {
key, err := crypto.DeriveKeyFromPassword(password, salt, keyDerivationAlgorithm)
key, err := crypto.DeriveKeyFromPassword(password, salt, passwordHashLength, keyDerivationAlgorithm)
if err != nil {
return nil, errors.Wrap(err, "error deriving key from password")
}
@@ -46,16 +41,11 @@ func computePasswordHash(password string, salt []byte, keyDerivationAlgorithm st
}
func isValidPassword(password string, hashedPassword []byte, keyDerivationAlgorithm string) bool {
saltLength, err := crypto.RecommendedSaltLength(keyDerivationAlgorithm)
if err != nil {
panic(err)
}
if len(hashedPassword) != saltLength+crypto.MasterKeyLength {
if len(hashedPassword) != passwordHashSaltLength+passwordHashLength {
return false
}
salt := hashedPassword[0:saltLength]
salt := hashedPassword[0:passwordHashSaltLength]
h, err := computePasswordHash(password, salt, keyDerivationAlgorithm)
if err != nil {

View File

@@ -17,9 +17,6 @@
// DefaultFormatEncryption is the identifier of the default format blob encryption algorithm.
const DefaultFormatEncryption = "AES256_GCM"
// DefaultKeyDerivationAlgorithm is the key derivation algorithm for new configurations.
const DefaultKeyDerivationAlgorithm = crypto.DefaultKeyDerivationAlgorithm
const (
aes256GcmEncryption = "AES256_GCM"
lengthOfRecoverBlockLength = 2 // number of bytes used to store recover block length
@@ -27,6 +24,7 @@
maxRecoverChunkLength = 65536
minRecoverableChunkLength = lengthOfRecoverBlockLength + 2
formatBlobChecksumSize = sha256.Size
formatBlobEncryptionKeySize = 32
)
// KopiaRepositoryBlobID is the identifier of a BLOB that describes repository format.
@@ -72,7 +70,7 @@ func ParseKopiaRepositoryJSON(b []byte) (*KopiaRepositoryJSON, error) {
// DeriveFormatEncryptionKeyFromPassword derives encryption key using the provided password and per-repository unique ID.
func (f *KopiaRepositoryJSON) DeriveFormatEncryptionKeyFromPassword(password string) ([]byte, error) {
res, err := crypto.DeriveKeyFromPassword(password, f.UniqueID, f.KeyDerivationAlgorithm)
res, err := crypto.DeriveKeyFromPassword(password, f.UniqueID, formatBlobEncryptionKeySize, f.KeyDerivationAlgorithm)
if err != nil {
return nil, errors.Wrap(err, "Failed to derive format encryption key")
}

View File

@@ -0,0 +1,13 @@
package format
import "github.com/kopia/kopia/internal/crypto"
// DefaultKeyDerivationAlgorithm is the derivation algorithm for format encryption for new repositories.
const DefaultKeyDerivationAlgorithm = crypto.ScryptAlgorithm
// SupportedFormatBlobKeyDerivationAlgorithms returns the supported algorithms
// for deriving the local cache encryption key when connecting to a repository
// via the kopia API server.
func SupportedFormatBlobKeyDerivationAlgorithms() []string {
return []string{crypto.ScryptAlgorithm, crypto.Pbkdf2Algorithm}
}

View File

@@ -58,9 +58,6 @@
// localCacheIntegrityHMACSecretLength length of HMAC secret protecting local cache items.
const localCacheIntegrityHMACSecretLength = 16
// DefaultKeyDerivationAlgorithm is the default key derivation algorithm used to derive a cache encryption key.
const DefaultKeyDerivationAlgorithm = crypto.DefaultKeyDerivationAlgorithm
//nolint:gochecknoglobals
var localCacheIntegrityPurpose = []byte("local-cache-integrity")
@@ -146,7 +143,9 @@ func getContentCacheOrNil(ctx context.Context, si *APIServerInfo, opt *content.C
// derive content cache key from the password & HMAC secret
saltWithPurpose := append([]byte("content-cache-protection"), opt.HMACSecret...)
cacheEncryptionKey, err := crypto.DeriveKeyFromPassword(password, saltWithPurpose, si.LocalCacheKeyDerivationAlgorithm)
const cacheEncryptionKeySize = 32
cacheEncryptionKey, err := crypto.DeriveKeyFromPassword(password, saltWithPurpose, cacheEncryptionKeySize, si.LocalCacheKeyDerivationAlgorithm)
if err != nil {
return nil, errors.Wrap(err, "unable to derive cache encryption key from password")
}

View File

@@ -0,0 +1,15 @@
package repo
import "github.com/kopia/kopia/internal/crypto"
// DefaultServerRepoCacheKeyDerivationAlgorithm is the default algorithm used to
// derive an encryption key for the local cache when connecting to a repository
// through the kopia API server.
const DefaultServerRepoCacheKeyDerivationAlgorithm = crypto.ScryptAlgorithm
// SupportedLocalCacheKeyDerivationAlgorithms returns the supported algorithms
// for deriving the local cache encryption key when connecting to a repository
// via the kopia API server.
func SupportedLocalCacheKeyDerivationAlgorithms() []string {
return []string{crypto.ScryptAlgorithm, crypto.Pbkdf2Algorithm}
}

View File

@@ -136,7 +136,7 @@ func testAPIServerRepository(t *testing.T, allowRepositoryUsers bool) {
rep, err := servertesting.ConnectAndOpenAPIServer(t, ctx2, &repo.APIServerInfo{
BaseURL: sp.BaseURL,
TrustedServerCertificateFingerprint: sp.SHA256Fingerprint,
LocalCacheKeyDerivationAlgorithm: repo.DefaultKeyDerivationAlgorithm,
LocalCacheKeyDerivationAlgorithm: repo.DefaultServerRepoCacheKeyDerivationAlgorithm,
}, repo.ClientOptions{
Username: "foo",
Hostname: "bar",
@@ -259,7 +259,7 @@ func testAPIServerRepository(t *testing.T, allowRepositoryUsers bool) {
servertesting.ConnectAndOpenAPIServer(t, ctx, &repo.APIServerInfo{
BaseURL: sp.BaseURL,
TrustedServerCertificateFingerprint: sp.SHA256Fingerprint,
LocalCacheKeyDerivationAlgorithm: repo.DefaultKeyDerivationAlgorithm,
LocalCacheKeyDerivationAlgorithm: repo.DefaultServerRepoCacheKeyDerivationAlgorithm,
}, repo.ClientOptions{
Username: "foo",
Hostname: "bar",
@@ -330,7 +330,7 @@ func TestFindManifestsPaginationOverGRPC(t *testing.T) {
rep, err := servertesting.ConnectAndOpenAPIServer(t, ctx, &repo.APIServerInfo{
BaseURL: sp.BaseURL,
TrustedServerCertificateFingerprint: sp.SHA256Fingerprint,
LocalCacheKeyDerivationAlgorithm: repo.DefaultKeyDerivationAlgorithm,
LocalCacheKeyDerivationAlgorithm: repo.DefaultServerRepoCacheKeyDerivationAlgorithm,
}, repo.ClientOptions{
Username: "foo",
Hostname: "bar",

View File

@@ -10,7 +10,6 @@
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/kopia/kopia/internal/crypto"
"github.com/kopia/kopia/internal/testutil"
"github.com/kopia/kopia/repo/format"
"github.com/kopia/kopia/tests/testenv"
@@ -110,7 +109,7 @@ func TestReconnectUsingToken(t *testing.T) {
func TestRepoConnectKeyDerivationAlgorithm(t *testing.T) {
t.Parallel()
for _, algorithm := range crypto.AllowedKeyDerivationAlgorithms() {
for _, algorithm := range format.SupportedFormatBlobKeyDerivationAlgorithms() {
runner := testenv.NewInProcRunner(t)
e := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner)
@@ -133,7 +132,7 @@ func TestRepoConnectBadKeyDerivationAlgorithm(t *testing.T) {
runner := testenv.NewInProcRunner(t)
e := testenv.NewCLITest(t, testenv.RepoFormatNotImportant, runner)
e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir, "--format-block-key-derivation-algorithm", crypto.DefaultKeyDerivationAlgorithm)
e.RunAndExpectSuccess(t, "repo", "create", "filesystem", "--path", e.RepoDir, "--format-block-key-derivation-algorithm", format.DefaultKeyDerivationAlgorithm)
e.RunAndExpectSuccess(t, "repo", "disconnect")
kopiaRepoPath := filepath.Join(e.RepoDir, "kopia.repository.f")