Skip to content

Commit 60d00f2

Browse files
authored
Replace SHA1 password hash and salts with argon2id (#384)
* Added argon2 support and migration * Require re-entering of download password after changing * Fixed tests * Force logout for all users * Updated docs
1 parent 5a41718 commit 60d00f2

16 files changed

Lines changed: 384 additions & 90 deletions

File tree

docs/advanced.rst

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -589,24 +589,13 @@ Contains the server configuration. If you want to deploy Gokapi in multiple inst
589589
Modifying config.json to deploy without setup
590590
====================================================
591591

592-
If you want to deploy Gokapi to multiple instances that contain different data, you have to modify the config.json. Open it and change the following fields:
593-
594-
+-----------+------------------------------------------------------------+----------------------+
595-
| Field | Operation | Example |
596-
+===========+============================================================+======================+
597-
| SaltAdmin | Change to empty value | "SaltAdmin": "", |
598-
+-----------+------------------------------------------------------------+----------------------+
599-
| SaltFiles | Change to empty value | "SaltFiles": "", |
600-
+-----------+------------------------------------------------------------+----------------------+
601-
| Username | Change to the username of your preference, | "Username": "admin", |
602-
| | | |
603-
| | if you are using internal username/password authentication | |
604-
+-----------+------------------------------------------------------------+----------------------+
592+
If you are deploying Gokapi across multiple instances with different datasets, there is no need to modify the ``config.json`` file anymore. The only time you should update it is when using internal username/password authentication and you want to change the admin username. In that case, simply update the ``Username`` field in the file.
593+
605594

606595
Setting an admin password
607596
====================================================
608597

609-
If you are using internal username/password authentication, run the binary with the parameter ``--deployment-password [YOUR_PASSWORD]``. This sets the password and also generates a new salt for the password. This has to be done before Gokapi is run for the first time on the new instance. Alternatively you can do this on the orchestrating machine and then copy the configuration file and database to the new instance.
598+
If you are using internal username/password authentication, run the binary with the parameter ``--deployment-password [YOUR_PASSWORD]``. This sets the password in the database. This has to be done before Gokapi is run for the first time on the new instance. Alternatively you can do this on the orchestrating machine and then copy the configuration file and database to the new instance.
610599

611600
If you are using a Docker image, this has to be done by starting a container with the entrypoint ``/app/run.sh``, for example: ::
612601

internal/configuration/Configuration.go

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import (
2323
"github.com/forceu/gokapi/internal/logging"
2424
"github.com/forceu/gokapi/internal/models"
2525
"github.com/forceu/gokapi/internal/storage/filesystem"
26+
"golang.org/x/crypto/argon2"
2627
)
2728

2829
// parsedEnvironment is an object containing the environment variables
@@ -195,7 +196,7 @@ func SetDeploymentPassword(newPassword string) {
195196
os.Exit(1)
196197
}
197198
serverSettings.Authentication.SaltAdmin = helper.GenerateRandomString(30)
198-
err := database.EditSuperAdmin(serverSettings.Authentication.Username, hashUserPassword(newPassword))
199+
err := database.EditSuperAdmin(serverSettings.Authentication.Username, HashPassword(newPassword, false, ""))
199200
if err != nil {
200201
fmt.Println("No super-admin user found, but database contains other users. Aborting.")
201202
os.Exit(1)
@@ -207,34 +208,88 @@ func SetDeploymentPassword(newPassword string) {
207208
os.Exit(0)
208209
}
209210

210-
// HashPassword hashes a string with SHA1 the file salt or admin user salt
211-
func HashPassword(password string, useFileSalt bool) string {
212-
if useFileSalt {
213-
return hashFilePassword(password)
214-
}
215-
return hashUserPassword(password)
216-
}
217-
218-
func hashFilePassword(password string) string {
219-
return HashPasswordCustomSalt(password, serverSettings.Authentication.SaltFiles)
211+
// Deprecated: SHA1 is not secure, this is only used for migrating
212+
// passwords from <v2.2.5 to the current version
213+
// Will be removed soon.
214+
func hashSha1(password, salt string) string {
215+
pwBytes := []byte(password + salt)
216+
hash := sha1.New()
217+
hash.Write(pwBytes)
218+
return hex.EncodeToString(hash.Sum(nil))
220219
}
221220

222-
func hashUserPassword(password string) string {
223-
return HashPasswordCustomSalt(password, serverSettings.Authentication.SaltAdmin)
224-
}
221+
const (
222+
argonTime = 2
223+
argonMemory = 28 * 1024 // 28 MB
224+
argonThreads = 1
225+
argonKeyLen = 32
226+
argonSaltLen = 16
227+
)
225228

226-
// HashPasswordCustomSalt hashes a password with SHA1 and the provided salt
227-
func HashPasswordCustomSalt(password, salt string) string {
229+
// HashPassword hashes a password with Argon2id.
230+
// useOldHash is used for migrating from <v2.2.5 to the current version
231+
// Will be removed soon.
232+
// legacySalt is only used for migrating from <v2.2.5 to the current version
233+
func HashPassword(password string, useOldHash bool, legacySalt string) string {
228234
if password == "" {
229235
return ""
230236
}
231-
if salt == "" {
232-
panic(errors.New("no salt provided"))
237+
pwBytes := []byte(password + legacySalt)
238+
if useOldHash {
239+
if legacySalt == "" {
240+
panic(errors.New("no salt provided for legacy hash"))
241+
}
242+
hash := sha1.New()
243+
hash.Write(pwBytes)
244+
return hex.EncodeToString(hash.Sum(nil))
233245
}
234-
pwBytes := []byte(password + salt)
235-
hash := sha1.New()
236-
hash.Write(pwBytes)
237-
return hex.EncodeToString(hash.Sum(nil))
246+
// Argon2id: generate a fresh random salt, ignore the global salt
247+
randomSalt := []byte(helper.GenerateRandomString(argonSaltLen))
248+
hash := argon2.IDKey(
249+
[]byte(password),
250+
randomSalt,
251+
argonTime,
252+
argonMemory,
253+
argonThreads,
254+
argonKeyLen,
255+
)
256+
257+
return fmt.Sprintf("argon2id$%s$%s",
258+
hex.EncodeToString(randomSalt),
259+
hex.EncodeToString(hash),
260+
)
261+
}
262+
263+
// VerifyPassword checks a plaintext password against a stored hash.
264+
// If hash is still SHA1, it will check the sha1 hash and return the second parameter as true, to indicate
265+
// that the hash was generated with the old hash function and requires rehashing
266+
// Oherwise argon2 will be used and the second parameter will be false
267+
func VerifyPassword(password, storedHash, legacySalt string) (bool, bool) {
268+
if len(storedHash) == 40 {
269+
hashedPassword := hashSha1(password, legacySalt)
270+
return helper.IsEqualStringConstantTime(hashedPassword, storedHash), true
271+
}
272+
273+
parts := strings.Split(storedHash, "$")
274+
if len(parts) != 3 || parts[0] != "argon2id" {
275+
return false, false
276+
}
277+
278+
salt, err := hex.DecodeString(parts[1])
279+
if err != nil {
280+
return false, false
281+
}
282+
283+
hash := argon2.IDKey(
284+
[]byte(password),
285+
salt,
286+
argonTime,
287+
argonMemory,
288+
argonThreads,
289+
argonKeyLen,
290+
)
291+
hashedPassword := hex.EncodeToString(hash)
292+
return helper.IsEqualStringConstantTime(hashedPassword, parts[2]), false
238293
}
239294

240295
// End2EndReconfigParameters contains values on how to reset E2E, if requested

0 commit comments

Comments
 (0)