diff --git a/mcrypto/mcrypto.go b/mcrypto/mcrypto.go index 633a359..a02b0de 100644 --- a/mcrypto/mcrypto.go +++ b/mcrypto/mcrypto.go @@ -7,12 +7,6 @@ import ( "strings" ) -// TODO rather than have the NewSignerVerifier methods, it might be better to -// have a Secret type, which implements Signer/Verifier. That way when there's -// Encrypter/Decrypter interfaces then Secret can implement those too, and -// PublicKey/PrivateKey can implement their respective ones. There'll be a nice -// symmetry there, rather than having NewEncrypterDecrypter functions. - // Instead of outputing opaque hex garbage, this package opts to add a prefix to // the garbage. Each "type" of string returned has its own character which is // not found in the hex range (0-9, a-f), and in addition each also has a diff --git a/mcrypto/secret.go b/mcrypto/secret.go new file mode 100644 index 0000000..1511a90 --- /dev/null +++ b/mcrypto/secret.go @@ -0,0 +1,84 @@ +package mcrypto + +import ( + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "io" + "time" + + "github.com/mediocregopher/mediocre-go-lib/mlog" +) + +// Secret contains a set of bytes which are inteded to remain secret within some +// context (e.g. a backend application keeping a secret from the frontend). +// +// Secret inherently implements the Signer and Verifier interfaces. +// +// Secret can be initialized with NewSecret or NewWeakSecret. The Signatures +// produced by these will be of differing lengths, but either can Verify a +// Signature made by the other as long as the secret bytes they are initialized +// with are the same. +type Secret struct { + sigSize uint8 // in bytes, shouldn't be more than 32, cause sha256 + secret []byte + + // only used during tests + testNow time.Time +} + +// NewSecret initializes and returns an instance of Secret which uses the given +// bytes as the underlying secret. +func NewSecret(secret []byte) Secret { + return Secret{sigSize: 20, secret: secret} +} + +// NewWeakSecret is like NewSecret but the Signatures it produces will be +// shorter and weaker (though still secure enough for most applications). +// Signatures produced by either normal or weak Secrets can be Verified by the +// other. +func NewWeakSecret(secret []byte) Secret { + return Secret{sigSize: 8, secret: secret} +} + +func (s Secret) now() time.Time { + if !s.testNow.IsZero() { + return s.testNow + } + return time.Now() +} + +func (s Secret) signRaw( + r io.Reader, + sigLen uint8, salt []byte, t time.Time, +) ( + []byte, error, +) { + h := hmac.New(sha256.New, s.secret) + r = sigPrefixReader(r, sigLen, salt, t) + if _, err := io.Copy(h, r); err != nil { + return nil, err + } + return h.Sum(nil)[:sigLen], nil +} + +func (s Secret) sign(r io.Reader) (Signature, error) { + salt := make([]byte, 8) + if _, err := rand.Read(salt); err != nil { + panic(err) + } + + t := s.now() + sig, err := s.signRaw(r, s.sigSize, salt, t) + return Signature{sig: sig, salt: salt, t: t}, err +} + +func (s Secret) verify(sig Signature, r io.Reader) error { + sigB, err := s.signRaw(r, uint8(len(sig.sig)), sig.salt, sig.t) + if err != nil { + return mlog.ErrWithKV(err, sig) + } else if !hmac.Equal(sigB, sig.sig) { + return mlog.ErrWithKV(ErrInvalidSig, sig) + } + return nil +} diff --git a/mcrypto/secret_test.go b/mcrypto/secret_test.go new file mode 100644 index 0000000..5311ef6 --- /dev/null +++ b/mcrypto/secret_test.go @@ -0,0 +1,54 @@ +package mcrypto + +import ( + . "testing" + "time" + + "github.com/ansel1/merry" + "github.com/mediocregopher/mediocre-go-lib/mtest" + "github.com/stretchr/testify/assert" +) + +func TestSecretSignVerify(t *T) { + secretRaw := mtest.RandBytes(16) + secret := NewSecret(secretRaw) + weakSecret := NewWeakSecret(secretRaw) + var prevStr string + var prevSig, prevWeakSig Signature + for i := 0; i < 10000; i++ { + now := time.Now().Round(0) + secret.testNow = now + weakSecret.testNow = now + + thisStr := mtest.RandHex(512) + thisSig := SignString(secret, thisStr) + thisWeakSig := SignString(weakSecret, thisStr) + thisSigStr, thisWeakSigStr := thisSig.String(), thisWeakSig.String() + + // sanity checks + assert.Equal(t, now, thisSig.Time()) + assert.Equal(t, now, thisWeakSig.Time()) + assert.NotEmpty(t, thisSigStr) + assert.NotEmpty(t, thisWeakSigStr) + assert.NotEqual(t, thisSigStr, thisWeakSigStr) + assert.True(t, len(thisSigStr) > len(thisWeakSigStr)) + + // Either secret should be able to verify either signature + assert.NoError(t, VerifyString(secret, thisSig, thisStr)) + assert.NoError(t, VerifyString(weakSecret, thisWeakSig, thisStr)) + assert.NoError(t, VerifyString(secret, thisWeakSig, thisStr)) + assert.NoError(t, VerifyString(weakSecret, thisSig, thisStr)) + + if prevStr != "" { + assert.NotEqual(t, prevSig.String(), thisSigStr) + assert.NotEqual(t, prevWeakSig.String(), thisWeakSigStr) + err := VerifyString(secret, prevSig, thisStr) + assert.True(t, merry.Is(err, ErrInvalidSig)) + err = VerifyString(secret, prevWeakSig, thisStr) + assert.True(t, merry.Is(err, ErrInvalidSig)) + } + prevStr = thisStr + prevSig = thisSig + prevWeakSig = thisWeakSig + } +} diff --git a/mcrypto/sig.go b/mcrypto/sig.go index b1dec22..8da6f48 100644 --- a/mcrypto/sig.go +++ b/mcrypto/sig.go @@ -2,9 +2,6 @@ package mcrypto import ( "bytes" - "crypto/hmac" - "crypto/rand" - "crypto/sha256" "encoding/binary" "encoding/hex" "encoding/json" @@ -187,73 +184,3 @@ func VerifyBytes(v Verifier, s Signature, b []byte) error { func VerifyString(v Verifier, s Signature, in string) error { return VerifyBytes(v, s, []byte(in)) } - -//////////////////////////////////////////////////////////////////////////////// - -type signVerifier struct { - outSize uint8 // in bytes, shouldn't be more than 32, cause sha256 - secret []byte - - // only used during tests - testNow time.Time -} - -// NewSignerVerifier returns Signer and Verifier instances which will use the -// given secret to sign and verify all Signatures -func NewSignerVerifier(secret []byte) (Signer, Verifier) { - sv := signVerifier{outSize: 20, secret: secret} - return sv, sv -} - -// NewWeakSignerVerifier returns Signer and Verifier instances, similar to how -// NewSignVerifier does. The Signatures generated by this Signer will be smaller -// in text size, and therefore weaker, but are still fine for most applications. -// -// The Verifiers returned by both NewSignVerifier and NewWeakSignVerifier can -// verify each-other's signatures, as long as the secret is the same. -func NewWeakSignerVerifier(secret []byte) (Signer, Verifier) { - sv := signVerifier{outSize: 8, secret: secret} - return sv, sv -} - -func (sv signVerifier) now() time.Time { - if !sv.testNow.IsZero() { - return sv.testNow - } - return time.Now() -} - -func (sv signVerifier) signRaw( - r io.Reader, - sigLen uint8, salt []byte, t time.Time, -) ( - []byte, error, -) { - h := hmac.New(sha256.New, sv.secret) - r = sigPrefixReader(r, sigLen, salt, t) - if _, err := io.Copy(h, r); err != nil { - return nil, err - } - return h.Sum(nil)[:sigLen], nil -} - -func (sv signVerifier) sign(r io.Reader) (Signature, error) { - salt := make([]byte, 8) - if _, err := rand.Read(salt); err != nil { - panic(err) - } - - t := sv.now() - sig, err := sv.signRaw(r, sv.outSize, salt, t) - return Signature{sig: sig, salt: salt, t: t}, err -} - -func (sv signVerifier) verify(s Signature, r io.Reader) error { - sig, err := sv.signRaw(r, uint8(len(s.sig)), s.salt, s.t) - if err != nil { - return mlog.ErrWithKV(err, s) - } else if !hmac.Equal(sig, s.sig) { - return mlog.ErrWithKV(ErrInvalidSig, s) - } - return nil -} diff --git a/mcrypto/sig_test.go b/mcrypto/sig_test.go index 400c211..18d5715 100644 --- a/mcrypto/sig_test.go +++ b/mcrypto/sig_test.go @@ -10,58 +10,35 @@ import ( ) func TestSignerVerifier(t *T) { - secret := mtest.RandBytes(16) - sigI, ver := NewSignerVerifier(secret) - sig := sigI.(signVerifier) - weakSigI, weakVer := NewWeakSignerVerifier(secret) - weakSig := weakSigI.(signVerifier) + secret := NewSecret(mtest.RandBytes(16)) var prevStr string - var prevSig, prevWeakSig Signature + var prevSig Signature for i := 0; i < 10000; i++ { now := time.Now().Round(0) - sig.testNow = now - weakSig.testNow = now + secret.testNow = now thisStr := mtest.RandHex(512) - thisSig := SignString(sig, thisStr) - thisWeakSig := SignString(weakSig, thisStr) - thisSigStr, thisWeakSigStr := thisSig.String(), thisWeakSig.String() - - // checking the times made it - assert.Equal(t, now, thisSig.Time()) - assert.Equal(t, now, thisWeakSig.Time()) + thisSig := SignString(secret, thisStr) + thisSigStr := thisSig.String() // sanity checks assert.NotEmpty(t, thisSigStr) - assert.NotEmpty(t, thisWeakSigStr) - assert.NotEqual(t, thisSigStr, thisWeakSigStr) - assert.True(t, len(thisSigStr) > len(thisWeakSigStr)) + assert.Equal(t, now, thisSig.Time()) + assert.NoError(t, VerifyString(secret, thisSig, thisStr)) // marshaling/unmarshaling - var thisSig2, thisWeakSig2 Signature + var thisSig2 Signature assert.NoError(t, thisSig2.UnmarshalText([]byte(thisSigStr))) assert.Equal(t, thisSigStr, thisSig2.String()) - assert.NoError(t, thisWeakSig2.UnmarshalText([]byte(thisWeakSigStr))) - assert.Equal(t, thisWeakSigStr, thisWeakSig2.String()) assert.Equal(t, now, thisSig2.Time()) - assert.Equal(t, now, thisWeakSig2.Time()) - - // Either sigVer should be able to verify either signature - assert.NoError(t, VerifyString(ver, thisSig, thisStr)) - assert.NoError(t, VerifyString(weakVer, thisWeakSig, thisStr)) - assert.NoError(t, VerifyString(ver, thisWeakSig, thisStr)) - assert.NoError(t, VerifyString(weakVer, thisSig, thisStr)) + assert.NoError(t, VerifyString(secret, thisSig2, thisStr)) if prevStr != "" { assert.NotEqual(t, prevSig.String(), thisSigStr) - assert.NotEqual(t, prevWeakSig.String(), thisWeakSigStr) - err := VerifyString(ver, prevSig, thisStr) - assert.True(t, merry.Is(err, ErrInvalidSig)) - err = VerifyString(ver, prevWeakSig, thisStr) + err := VerifyString(secret, prevSig, thisStr) assert.True(t, merry.Is(err, ErrInvalidSig)) } prevStr = thisStr prevSig = thisSig - prevWeakSig = thisWeakSig } }