Skip to content

Commit 47d2b04

Browse files
committed
Setting params for Argon2 requires info file v5
This is because the change was actually not forward-compatible, so it required a new info file version. Also, improved how we deal with the legacy keys (that used older Argon2 params)
1 parent 41c0dc2 commit 47d2b04

File tree

10 files changed

+125
-47
lines changed

10 files changed

+125
-47
lines changed

Encryption.md

+5-5
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ The default method of operation of prvt uses a passphrase to derive the wrapping
101101

102102
In this mode of operation, the wrapping key is a 256-bit symmetric key that is derived from the user's passphrase using the [Argon2](https://en.wikipedia.org/wiki/Argon2) algorithm, in the Argon2id variant.
103103

104-
prvt uses the [golang.org/x/crypto/argon2](https://golang.org/x/crypto/argon2) implementation of Argon2id, which is part of the Go project. prvt 0.5.1 and higher use Argon2id with iterations=4, memory=80MB, and parallelism=2.
104+
prvt uses the [golang.org/x/crypto/argon2](https://golang.org/x/crypto/argon2) implementation of Argon2id, which is part of the Go project. prvt 0.6 and higher use Argon2id with iterations=4, memory=80MB, and parallelism=2.
105105

106106
It's possible to override the parameters used for Argon2 at runtime, when a new passphrase is added as key (i.e. with `prvt repo key add` or the equivalent REST call), by setting the environmental variables `PRVT_ARGON2_ITERATIONS`, `PRVT_ARGON2_MEMORY` (whose value is in KB) and `PRVT_ARGON2_PARALLELISM`.
107107

@@ -155,7 +155,7 @@ The `_info.json` file is the only file in the repository that is not encrypted.
155155
This file is a JSON document containing four keys:
156156

157157
- The name of the app (`app`) that created it. This is always `prvt`.
158-
- The version (`ver`) of the info file. The latest value, from prvt version 0.5, is `4`.
158+
- The version (`ver`) of the info file. The latest value, from prvt version 0.6, is `5`.
159159
- A unique ID for the repo (`id`). This is a randomly-generated UUID and it's used to identify which repo you're working on.
160160
- The data path (`dp`), which is the name of the sub-folder where the encrypted data is stored. The default value is `data`. (This value can't be set using the prvt CLI, but it's defined here to enable backwards compatibility with repositories created by previous versions of prvt.)
161161
- The list of passphrases and keys (`k`).
@@ -173,9 +173,9 @@ For master keys that are wrapped with wrapping keys derived from a passphrase, t
173173
- The options for the key derivation function (`o`). This is a dictionary that, for Argon2, contains the following keys:
174174
- `a2`: variant to use: this can only be `argon2id`, which is also the default value if the key is omitted.
175175
- `a2v`: version of Argon2: this can only be `19`, which is also the default value if the key is omitted.
176-
- `a2m`: memory parameter, in KB; for backwards-compatibility, if the key is omitted the default value is `65536` (64 MB), which is different from the new default value of 80 MB.
177-
- `a2t`: iterations parameter; for backwards-compatibility, if the key is omitted the default value is `1`, which is different from the new default value of 4.
178-
- `a2p`: parallelism parameter; for backwards-compatibility, if the key is omitted the default value is `4`, which is different from the new default value of 2.
176+
- `a2m`: memory parameter, in KB; for backwards-compatibility, if the key is omitted the default value is `8192`, or 80 MB (this was 64 MB in prvt 0.5 and lower).
177+
- `a2t`: iterations parameter; for backwards-compatibility, if the key is omitted the default value is `4` (this was `1` in prvt 0.5 and lower).
178+
- `a2p`: parallelism parameter; for backwards-compatibility, if the key is omitted the default value is `2` (this was `4` in prvt 0.5 and lower).
179179

180180
For example, for a repository that allows only one passphrase to unlock it (the document below has been pretty-printed for clarity for this example only):
181181

cmd/zz-keys.go

+22-7
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,8 @@ func NewInfoFile(gpgKey string) (info *infofile.InfoFile, errMessage string, err
8585

8686
// UpgradeInfoFile upgrades an info file to the latest version
8787
func UpgradeInfoFile(info *infofile.InfoFile) (errMessage string, err error) {
88-
// Can only upgrade info files versions 1-3
89-
if info.Version < 1 || info.Version > 3 {
88+
// Can only upgrade info files versions 1-4
89+
if info.Version < 1 || info.Version > 4 {
9090
return "Unsupported repository version", errors.New("This repository has already been upgraded or is using an unsupported version")
9191
}
9292

@@ -114,8 +114,21 @@ func UpgradeInfoFile(info *infofile.InfoFile) (errMessage string, err error) {
114114
info.RepoId = repoId.String()
115115
}
116116

117+
// Upgrade 4 -> 5
118+
if info.Version < 5 {
119+
// Set the legacy values for Argon2 for each key
120+
for i := 0; i < len(info.Keys); i++ {
121+
// Ignore GPG keys and ignore keys that have already been upgraded (e.g. from v1)
122+
if info.Keys[i].GPGKey != "" || info.Keys[i].KDFOptions != nil {
123+
continue
124+
}
125+
info.Keys[i].KDF = "argon2"
126+
info.Keys[i].KDFOptions = crypto.LegacyArgon2Options()
127+
}
128+
}
129+
117130
// Update the version
118-
info.Version = 4
131+
info.Version = 5
119132

120133
return "", nil
121134
}
@@ -132,8 +145,7 @@ func upgradeInfoFileV1(info *infofile.InfoFile) (errMessage string, err error) {
132145
}
133146

134147
// Get the default parameters for Argon2
135-
kdfOptions := &crypto.Argon2Options{}
136-
_ = kdfOptions.Validate()
148+
kdfOptions := crypto.LegacyArgon2Options()
137149
if err != nil {
138150
return "Error validating Argon2 parameters", err
139151
}
@@ -144,8 +156,11 @@ func upgradeInfoFileV1(info *infofile.InfoFile) (errMessage string, err error) {
144156
return "Cannot unlock the repository", errors.New("Invalid passphrase")
145157
}
146158

147-
// Now, tune the parameters for Argon2 before wrapping the key again
148-
kdfOptions.Setup()
159+
// Now, set up the parameters for Argon2 before wrapping the key again
160+
err = kdfOptions.Setup()
161+
if err != nil {
162+
return "Error setting up Argon2 parameters", err
163+
}
149164

150165
// Create a new salt
151166
newSalt, err := crypto.NewSalt()

crypto/key.go

+20-12
Original file line numberDiff line numberDiff line change
@@ -40,16 +40,11 @@ type Argon2Options struct {
4040
}
4141

4242
// Setup sets the parameters for the key derivation function
43-
func (o *Argon2Options) Setup() {
43+
func (o *Argon2Options) Setup() error {
4444
// Set variant and version
4545
o.Variant = "argon2id"
4646
o.Version = 0x13
4747

48-
// Set parameters to some values that should be reasonable for most systems, including low-power ones
49-
o.Iterations = 4
50-
o.Memory = 80 << 10
51-
o.Parallelism = 2
52-
5348
// Check if we have the tune function, which is included with a build tag only
5449
method := reflect.ValueOf(o).MethodByName("Tune")
5550
if method.IsValid() {
@@ -69,6 +64,8 @@ func (o *Argon2Options) Setup() {
6964
if err == nil && parallelism > 0 {
7065
o.Parallelism = uint8(parallelism)
7166
}
67+
68+
return o.Validate()
7269
}
7370

7471
// Validate the values and sets the defaults for the missing ones
@@ -89,17 +86,17 @@ func (o *Argon2Options) Validate() error {
8986
if argon2.Version != 0x13 {
9087
panic("argon2 library uses a different version that expected")
9188
}
92-
// Default memory is 64MB (in KB), for backwards compatibility
89+
// Default memory is 80MB (in KB)
9390
if o.Memory == 0 {
94-
o.Memory = 64 * 1024
91+
o.Memory = 80 * 1024
9592
}
96-
// Default iterations is 1, for backwards compatibility
93+
// Default iterations is 4
9794
if o.Iterations == 0 {
98-
o.Iterations = 1
95+
o.Iterations = 4
9996
}
100-
// Default parallelism is 4, for backwards compatibility
97+
// Default parallelism is 2
10198
if o.Parallelism == 0 {
102-
o.Parallelism = 4
99+
o.Parallelism = 2
103100
}
104101
return nil
105102
}
@@ -111,6 +108,17 @@ func (o *Argon2Options) timeExecution() int64 {
111108
return time.Since(start).Milliseconds()
112109
}
113110

111+
// LegacyArgon2Options returns an object with the parameters for Argon2 used by prvt version 4 and below
112+
func LegacyArgon2Options() *Argon2Options {
113+
return &Argon2Options{
114+
Variant: "argon2id",
115+
Version: 19,
116+
Memory: 64 << 10,
117+
Iterations: 1,
118+
Parallelism: 4,
119+
}
120+
}
121+
114122
// KeyFromPassphrase returns the 32-byte key derived from a passphrase and a salt using Argon2id
115123
// It also returns a "confirmation hash" that can be used to ensure the passphrase is correct
116124
func KeyFromPassphrase(passphrase string, salt []byte, kd *Argon2Options) (key []byte, confirmationHash []byte, err error) {

infofile/infofile.go

+9-7
Original file line numberDiff line numberDiff line change
@@ -38,14 +38,16 @@ type InfoFileKey struct {
3838
// GPG key id for when using a GPG key
3939
GPGKey string `json:"g,omitempty"`
4040

41-
// Key Derivation Function for when using a passphrase (only "argon2" is currently supported)
42-
KDF string `json:"f,omitempty"`
43-
// Options for the Key Derivation Function (Argon2 only)
44-
KDFOptions *crypto.Argon2Options `json:"o,omitempty"`
4541
// ConfirmationHash for when using a passphrase
4642
ConfirmationHash []byte `json:"p,omitempty"`
4743
// Salt for when using a passphrase
4844
Salt []byte `json:"s,omitempty"`
45+
46+
// Fields for version 5+
47+
// Key Derivation Function for when using a passphrase (only "argon2" is currently supported)
48+
KDF string `json:"f,omitempty"`
49+
// Options for the Key Derivation Function (Argon2 only)
50+
KDFOptions *crypto.Argon2Options `json:"o,omitempty"`
4951
}
5052

5153
// InfoFile is the content of the info file
@@ -79,7 +81,7 @@ func New() (*InfoFile, error) {
7981
// Info file
8082
info := &InfoFile{
8183
App: "prvt",
82-
Version: 4,
84+
Version: 5,
8385
RepoId: repoId.String(),
8486
DataPath: "data",
8587
}
@@ -206,8 +208,8 @@ func (info *InfoFile) Validate() error {
206208
return errors.New("invalid confirmation hash in info file")
207209
}
208210
}
209-
} else if info.Version >= 2 && info.Version <= 4 {
210-
// Parse version 2 to 4
211+
} else if info.Version >= 2 && info.Version <= 5 {
212+
// Parse version 2 to 5
211213
if len(info.Keys) == 0 {
212214
return errors.New("repository does not have any key")
213215
}

keys/keys.go

+10-12
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ func GetMasterKeyWithPassphrase(info *infofile.InfoFile, passphrase string) (mas
5151
if len(info.Salt) != 0 && len(info.ConfirmationHash) != 0 {
5252
var confirmationHash []byte
5353
// Default KDF options
54-
kdfOptions := &crypto.Argon2Options{}
55-
_ = kdfOptions.Validate()
54+
kdfOptions := crypto.LegacyArgon2Options()
5655
masterKey, confirmationHash, err = crypto.KeyFromPassphrase(passphrase, info.Salt, kdfOptions)
5756
if err == nil && subtle.ConstantTimeCompare(info.ConfirmationHash, confirmationHash) == 1 {
5857
return masterKey, "LegacyKey", "", nil
@@ -76,13 +75,13 @@ func GetMasterKeyWithPassphrase(info *infofile.InfoFile, passphrase string) (mas
7675
if k.KDF != "argon2" && k.KDF != "" {
7776
continue
7877
}
79-
// For backwards compatibility, create the KDF options if empty
78+
// For backwards compatibility, create the KDF options if empty and set the values to the legacy ones
8079
if k.KDFOptions == nil {
81-
k.KDFOptions = &crypto.Argon2Options{}
82-
// Skip invalid keys
83-
if k.KDFOptions.Validate() != nil {
84-
continue
85-
}
80+
k.KDFOptions = crypto.LegacyArgon2Options()
81+
}
82+
// Skip invalid keys
83+
if k.KDFOptions.Validate() != nil {
84+
continue
8685
}
8786

8887
// Try this key
@@ -111,13 +110,12 @@ func AddKeyPassphrase(info *infofile.InfoFile, masterKey []byte, passphrase stri
111110
return "", "Key already added", errors.New("This passphrase has already been added to the repository")
112111
}
113112

114-
// Tune parameters for Argon2
113+
// Set up parameters for Argon2
115114
kdfOptions := &crypto.Argon2Options{}
116-
err = kdfOptions.Validate()
115+
err = kdfOptions.Setup()
117116
if err != nil {
118-
return "", "Error validating Argon2 parameters", err
117+
return "", "Error setting up Argon2 parameters", err
119118
}
120-
kdfOptions.Setup()
121119

122120
// Derive the wrapping key, after generating a new salt
123121
salt, err = crypto.NewSalt()
192 Bytes
Binary file not shown.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"app":"prvt","ver":4,"dp":"data","k":[{"m":"o7B/BE0HBhW6m6BP4ntjY7S4qNLXFqjSqrecH1yAULpIqrbxZA4puA==","s":"O0ZJv3+NDOyx4ZdkMfd5Gg==","p":"klfIuohyvZULyfibnuzxQMSrlODUsLhqwy9BYqIh2lQ="}],"id":"9e52eb06-a44a-4f89-819c-57dfdf8feee4"}

tests/functional-cli_test.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ func (s *funcTestSuite) cmdRepoInit(t *testing.T) {
104104
err = json.Unmarshal(read, info)
105105
assert.NoError(t, err)
106106
assert.Equal(t, "prvt", info.App)
107-
assert.Equal(t, uint16(4), info.Version)
107+
assert.Equal(t, uint16(5), info.Version)
108108
assert.NotEmpty(t, info.RepoId)
109109
assert.Len(t, info.Keys, 1)
110110
assert.Len(t, info.Keys[0].ConfirmationHash, 32)
@@ -126,7 +126,7 @@ func (s *funcTestSuite) cmdRepoInit(t *testing.T) {
126126
err = json.Unmarshal(read, info)
127127
assert.NoError(t, err)
128128
assert.Equal(t, "prvt", info.App)
129-
assert.Equal(t, uint16(4), info.Version)
129+
assert.Equal(t, uint16(5), info.Version)
130130
assert.NotEmpty(t, info.RepoId)
131131
assert.Len(t, info.Keys, 1)
132132
assert.Len(t, info.Keys[0].ConfirmationHash, 0)
@@ -241,7 +241,7 @@ func (s *funcTestSuite) cmdRepoKey(t *testing.T) {
241241
err = json.Unmarshal(read, info)
242242
assert.NoError(t, err)
243243
assert.Equal(t, "prvt", info.App)
244-
assert.Equal(t, uint16(4), info.Version)
244+
assert.Equal(t, uint16(5), info.Version)
245245
assert.NotEmpty(t, info.RepoId)
246246
assert.Len(t, info.Keys, 2)
247247
assert.Len(t, info.Keys[0].ConfirmationHash, 0)

tests/functional-previous-versions_test.go

+55-1
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ func (s *funcTestSuite) RunPreviousVersions(t *testing.T) {
4141
t.Run("prvt 0.2", s.previousVersion_0_2)
4242
t.Run("prvt 0.3", s.previousVersion_0_3)
4343
t.Run("prvt 0.4", s.previousVersion_0_4)
44+
t.Run("prvt 0.5", s.previousVersion_0_5)
4445
}
4546

4647
// Tests for working with repositories created by version 0.2
@@ -228,6 +229,59 @@ func (s *funcTestSuite) previousVersion_0_4(t *testing.T) {
228229
close()
229230
}
230231

232+
// Tests for working with repositories created by version 0.5
233+
func (s *funcTestSuite) previousVersion_0_5(t *testing.T) {
234+
// Start the server
235+
s.promptPwd.SetPasswords("hello world")
236+
path := filepath.Join(s.fixtures, "previous-versions", "v0.5")
237+
close := s.startServer(t, "--store", "local:"+path)
238+
239+
// Should be able to list files
240+
list, err := s.listRequest("/")
241+
if err != nil {
242+
close()
243+
t.Fatal(err)
244+
return
245+
}
246+
assert.Len(t, list, 1)
247+
assert.Equal(t, "pg1000.txt", list[0].Path)
248+
assert.NotEmpty(t, list[0].MimeType)
249+
assert.NotEmpty(t, list[0].Date)
250+
251+
// Should be able to request the file
252+
s.previousVersionRequestFile(t, list[0].FileId)
253+
254+
// Stop the server
255+
close()
256+
257+
// Upgrade the repo
258+
s.promptPwd.SetPasswords("hello world")
259+
runCmd(t,
260+
[]string{"repo", "upgrade", "--store", "local:" + path},
261+
nil,
262+
func(stdout string) {
263+
assert.Equal(t, "Repository upgraded\n", stdout)
264+
},
265+
nil,
266+
)
267+
268+
// Check that the repo has been upgraded
269+
s.previousVersionCheckInfoFile(t, path, 4)
270+
271+
// Try unlocking the repo again
272+
s.promptPwd.SetPasswords("hello world")
273+
close = s.startServer(t, "--store", "local:"+path)
274+
275+
// Should be able to list files
276+
newList, err := s.listRequest("/")
277+
assert.NoError(t, err)
278+
assert.Len(t, newList, 1)
279+
assert.True(t, reflect.DeepEqual(list, newList))
280+
281+
// Stop the server
282+
close()
283+
}
284+
231285
func (s *funcTestSuite) previousVersionRequestFile(t *testing.T, fileId string) {
232286
t.Helper()
233287

@@ -304,7 +358,7 @@ func (s *funcTestSuite) previousVersionCheckInfoFile(t *testing.T, path string,
304358
t.Error(err)
305359
return
306360
}
307-
assert.Equal(t, uint16(4), info.Version)
361+
assert.Equal(t, uint16(5), info.Version)
308362
assert.Len(t, info.Keys, 1)
309363
assert.NotEmpty(t, info.RepoId)
310364

0 commit comments

Comments
 (0)