Skip to content

Commit

Permalink
add passwd pkg (#7)
Browse files Browse the repository at this point in the history
* 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
golanglemonade authored Aug 29, 2024
1 parent 01f8521 commit d7ff20b
Show file tree
Hide file tree
Showing 8 changed files with 436 additions and 5 deletions.
6 changes: 4 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/oklog/ulid/v2 v2.1.0
github.com/olekukonko/tablewriter v0.0.5
github.com/sendgrid/rest v2.6.9+incompatible
github.com/sendgrid/sendgrid-go v3.15.0+incompatible
github.com/sendgrid/sendgrid-go v3.16.0+incompatible
github.com/stretchr/testify v1.9.0
github.com/theopenlane/echox v0.1.0
golang.org/x/crypto v0.26.0
Expand All @@ -18,7 +18,9 @@ require (

require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/mattn/go-runewidth v0.0.9 // indirect
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sys v0.24.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
12 changes: 9 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/oklog/ulid/v2 v2.1.0 h1:+9lhoxAP56we25tyYETBBY1YLA2SaoLvUFgrP2miPJU=
Expand All @@ -15,10 +16,13 @@ github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6
github.com/pborman/getopt v0.0.0-20170112200414-7148bc3a4c30/go.mod h1:85jBQOZwpVEaDAr341tbn15RS4fCAsIst0qp7i8ex1o=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
github.com/sendgrid/sendgrid-go v3.15.0+incompatible h1:oB6ujJD2aFcQRjmZLmmXiiUF9CBYKzsvYdPAS/71cSU=
github.com/sendgrid/sendgrid-go v3.15.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw=
github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/theopenlane/echox v0.1.0 h1:y4Z2shaODCLwXHsHBrY/EkH/2sIuo49xdIfxx7h+Zvg=
Expand All @@ -27,6 +31,8 @@ golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
Expand Down
127 changes: 127 additions & 0 deletions passwd/dk.go
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)
}
115 changes: 115 additions & 0 deletions passwd/dk_test.go
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)
})
}
}
2 changes: 2 additions & 0 deletions passwd/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package passwd provides fancy crypto shit for passwords
package passwd
45 changes: 45 additions & 0 deletions passwd/errors.go
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,
}
}
Loading

0 comments on commit d7ff20b

Please sign in to comment.