-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* add passwd package Signed-off-by: Sarah Funkhouser <[email protected]> * add passwd package Signed-off-by: Sarah Funkhouser <[email protected]> --------- Signed-off-by: Sarah Funkhouser <[email protected]>
- Loading branch information
1 parent
01f8521
commit d7ff20b
Showing
8 changed files
with
436 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
package passwd | ||
|
||
import ( | ||
"bytes" | ||
"crypto/rand" | ||
"encoding/base64" | ||
"fmt" | ||
"regexp" | ||
"strconv" | ||
|
||
"golang.org/x/crypto/argon2" | ||
) | ||
|
||
// =========================================================================== | ||
// Derived Key Algorithm | ||
// =========================================================================== | ||
|
||
// Argon2 constants for the derived key (dk) algorithm | ||
// See: https://cryptobook.nakov.com/mac-and-key-derivation/argon2 | ||
const ( | ||
dkAlg = "argon2id" // the derived key algorithm | ||
dkTime = uint32(1) // draft RFC recommends time = 1 | ||
dkMem = uint32(64 * 1024) // draft RFC recommends memory as ~64MB (or as much as possible) | ||
dkProc = uint8(2) // can be set to the number of available CPUs | ||
dkSLen = 16 // the length of the salt to generate per user | ||
dkKLen = uint32(32) // the length of the derived key (32 bytes is the required key size for AES-256) | ||
) | ||
|
||
// Argon2 variables for the derived key (dk) algorithm | ||
var ( | ||
dkParse = regexp.MustCompile(`^\$(?P<alg>[\w\d]+)\$v=(?P<ver>\d+)\$m=(?P<mem>\d+),t=(?P<time>\d+),p=(?P<procs>\d+)\$(?P<salt>[\+\/\=a-zA-Z0-9]+)\$(?P<key>[\+\/\=a-zA-Z0-9]+)$`) | ||
) | ||
|
||
// CreateDerivedKey creates an encoded derived key with a random hash for the password. | ||
func CreateDerivedKey(password string) (string, error) { | ||
if password == "" { | ||
return "", ErrCannotCreateDK | ||
} | ||
|
||
salt := make([]byte, dkSLen) | ||
if _, err := rand.Read(salt); err != nil { | ||
return "", ErrCouldNotGenerate | ||
} | ||
|
||
dk := argon2.IDKey([]byte(password), salt, dkTime, dkMem, dkProc, dkKLen) | ||
b64salt := base64.StdEncoding.EncodeToString(salt) | ||
b64dk := base64.StdEncoding.EncodeToString(dk) | ||
|
||
return fmt.Sprintf("$%s$v=%d$m=%d,t=%d,p=%d$%s$%s", dkAlg, argon2.Version, dkMem, dkTime, dkProc, b64salt, b64dk), nil | ||
} | ||
|
||
// VerifyDerivedKey checks that the submitted password matches the derived key. | ||
func VerifyDerivedKey(dk, password string) (bool, error) { | ||
if dk == "" || password == "" { | ||
return false, ErrUnableToVerify | ||
} | ||
|
||
dkb, salt, t, m, p, err := ParseDerivedKey(dk) | ||
if err != nil { | ||
return false, err | ||
} | ||
|
||
vdk := argon2.IDKey([]byte(password), salt, t, m, p, uint32(len(dkb))) // nolint:gosec | ||
|
||
return bytes.Equal(dkb, vdk), nil | ||
} | ||
|
||
// ParseDerivedKey returns the parts of the encoded derived key string. | ||
func ParseDerivedKey(encoded string) (dk, salt []byte, time, memory uint32, threads uint8, err error) { | ||
if !dkParse.MatchString(encoded) { | ||
return nil, nil, 0, 0, 0, ErrCannotParseDK | ||
} | ||
|
||
parts := dkParse.FindStringSubmatch(encoded) | ||
|
||
if len(parts) != 8 { //nolint:mnd | ||
return nil, nil, 0, 0, 0, ErrCannotParseEncodedEK | ||
} | ||
|
||
// check the algorithm | ||
if parts[1] != dkAlg { | ||
return nil, nil, 0, 0, 0, newParseError("dkAlg", parts[1], dkAlg) | ||
} | ||
|
||
// check the version | ||
if version, err := strconv.Atoi(parts[2]); err != nil || version != argon2.Version { | ||
return nil, nil, 0, 0, 0, newParseError("version", parts[2], fmt.Sprintf("%d", argon2.Version)) | ||
} | ||
|
||
var ( | ||
time64 uint64 | ||
memory64 uint64 | ||
threads64 uint64 | ||
) | ||
|
||
if memory64, err = strconv.ParseUint(parts[3], 10, 32); err != nil { | ||
return nil, nil, 0, 0, 0, newParseError("memory", parts[3], err.Error()) | ||
} | ||
|
||
memory = uint32(memory64) // nolint:gosec | ||
|
||
if time64, err = strconv.ParseUint(parts[4], 10, 32); err != nil { | ||
return nil, nil, 0, 0, 0, newParseError("time", parts[4], err.Error()) | ||
} | ||
|
||
time = uint32(time64) // nolint:gosec | ||
|
||
if threads64, err = strconv.ParseUint(parts[5], 10, 8); err != nil { | ||
return nil, nil, 0, 0, 0, newParseError("threads", parts[5], err.Error()) | ||
} | ||
|
||
threads = uint8(threads64) // nolint:gosec | ||
|
||
if salt, err = base64.StdEncoding.DecodeString(parts[6]); err != nil { | ||
return nil, nil, 0, 0, 0, newParseError("salt", parts[6], err.Error()) | ||
} | ||
|
||
if dk, err = base64.StdEncoding.DecodeString(parts[7]); err != nil { | ||
return nil, nil, 0, 0, 0, newParseError("dk", parts[7], err.Error()) | ||
} | ||
|
||
return dk, salt, time, memory, threads, nil | ||
} | ||
|
||
func IsDerivedKey(s string) bool { | ||
return dkParse.MatchString(s) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,115 @@ | ||
package passwd_test | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/require" | ||
|
||
"github.com/theopenlane/utils/passwd" | ||
) | ||
|
||
func TestDerivedKey(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
passwordCreate string | ||
passwordVerify string | ||
verified bool | ||
}{ | ||
{ | ||
"happy path, matching", | ||
"supersafesa$#%asaf!", | ||
"supersafesa$#%asaf!", | ||
true, | ||
}, | ||
{ | ||
"not matching", | ||
"supersafesa$#%asaf!", | ||
"notthesamething", | ||
false, | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
// Create a derived key from a password | ||
password, err := passwd.CreateDerivedKey(tc.passwordCreate) | ||
require.NoError(t, err) | ||
|
||
// verify key | ||
verified, err := passwd.VerifyDerivedKey(password, tc.passwordVerify) | ||
require.NoError(t, err) | ||
require.Equal(t, tc.verified, verified) | ||
}) | ||
} | ||
} | ||
|
||
func TestDerivedKeyErrors(t *testing.T) { | ||
testCases := []struct { | ||
name string | ||
dk string | ||
password string | ||
expectedError string | ||
}{ | ||
{ | ||
"cannot verify empty derived key or password", | ||
"", | ||
"foo", | ||
"cannot verify empty derived key or password", | ||
}, | ||
{ | ||
"cannot verify empty derived key or password, take 2", | ||
"foo", | ||
"", | ||
"cannot verify empty derived key or password", | ||
}, | ||
{ | ||
"cannot parse encoded derived key, does not match regular expression", | ||
"notarealkey", | ||
"supersecretpassword", | ||
"cannot parse encoded derived key, does not match regular expression", | ||
}, | ||
{ | ||
"could not parse version", | ||
"$argon2id$v=13212$m=65536,t=1,p=2$FrAEw4rWRDpyIZXR/QSzpg==$chQikgApfQfSaPZ7idk6caqBk79xRalpPUs4Ro/hywM=", | ||
"supersecretpassword", | ||
"could not parse version", | ||
}, | ||
{ | ||
"could not parse time", | ||
"$argon2id$v=19$m=65536,t=999999999999999999,p=2$FrAEw4rWRDpyIZXR/QSzpg==$chQikgApfQfSaPZ7idk6caqBk79xRalpPUs4Ro/hywM=", | ||
"supersecretpassword", | ||
"could not parse time", | ||
}, | ||
{ | ||
"could not parse memory", | ||
"$argon2id$v=19$m=999999999999999999,t=1,p=2$FrAEw4rWRDpyIZXR/QSzpg==$chQikgApfQfSaPZ7idk6caqBk79xRalpPUs4Ro/hywM=", | ||
"supersecretpassword", | ||
"could not parse memory", | ||
}, | ||
{ | ||
"could not parse threads", | ||
"$argon2id$v=19$m=65536,t=1,p=999999999999999999$FrAEw4rWRDpyIZXR/QSzpg==$chQikgApfQfSaPZ7idk6caqBk79xRalpPUs4Ro/hywM=", | ||
"supersecretpassword", | ||
"could not parse threads", | ||
}, | ||
{ | ||
"could not parse salt", | ||
"$argon2id$v=19$m=65536,t=1,p=2$==FrAEw4rWRDpyIZXR/QSzpg==$chQikgApfQfSaPZ7idk6caqBk79xRalpPUs4Ro/hywM=", | ||
"supersecretpassword", | ||
"could not parse salt", | ||
}, | ||
{ | ||
"could not parse dk", | ||
"$argon2id$v=19$m=65536,t=1,p=2$FrAEw4rWRDpyIZXR/QSzpg==$==chQikgApfQfSaPZ7idk6caqBk79xRalpPUs4Ro/hywM=", | ||
"supersecretpassword", | ||
"could not parse dk", | ||
}, | ||
} | ||
|
||
for _, tc := range testCases { | ||
t.Run(tc.name, func(t *testing.T) { | ||
_, err := passwd.VerifyDerivedKey(tc.dk, tc.password) | ||
require.ErrorContains(t, err, tc.expectedError) | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
// Package passwd provides fancy crypto shit for passwords | ||
package passwd |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
package passwd | ||
|
||
import ( | ||
"errors" | ||
"fmt" | ||
) | ||
|
||
// Error constants | ||
var ( | ||
// ErrCannotCreateDK is returned when the provided password is empty or the derived key creation failed | ||
ErrCannotCreateDK = errors.New("cannot create derived key for empty password") | ||
|
||
// ErrCouldNotGenerate is returned when a derived key of specified length failed to be generated | ||
ErrCouldNotGenerate = fmt.Errorf("could not generate %d length", dkSLen) | ||
|
||
// ErrUnableToVerify is returned when attempting to verify an empty derived key or empty password | ||
ErrUnableToVerify = errors.New("cannot verify empty derived key or password") | ||
|
||
// ErrCannotParseDK is returned when the encoded derived key fails to be parsed due to part(s) mismatch | ||
ErrCannotParseDK = errors.New("cannot parse encoded derived key, does not match regular expression") | ||
|
||
// ErrCannotParseEncodedEK is returned when the derived key parts do not match the desired part length | ||
ErrCannotParseEncodedEK = errors.New("cannot parse encoded derived key, matched expression does not contain enough subgroups") | ||
) | ||
|
||
// ParseError is defining a custom error type called `ParseError`. It is a struct | ||
// that holds intermediary values for comparison in errors | ||
type ParseError struct { | ||
Object string | ||
Value string | ||
ExpectedValue string | ||
} | ||
|
||
// Error returns the ParseError in string format | ||
func (e *ParseError) Error() string { | ||
return fmt.Sprintf("could not parse %s %s, got %s", e.Object, e.ExpectedValue, e.Value) | ||
} | ||
|
||
func newParseError(o string, v string, ev string) *ParseError { | ||
return &ParseError{ | ||
Object: o, | ||
Value: v, | ||
ExpectedValue: ev, | ||
} | ||
} |
Oops, something went wrong.