├── test_assets ├── privatekey.der └── publickey.der ├── signer.go ├── verifier.go ├── isokey.go ├── sym_key_service_test.go ├── check_mac.go ├── key_test.go ├── unpack.go ├── asym_key_verifier_signer_test.go ├── key.go ├── errors.go ├── LICENSE ├── signer_verifier_test.go ├── asym_key_signer.go ├── sym_key_service.go ├── asym_key_verifier.go ├── coverage.out └── README.md /test_assets/privatekey.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ammario/isokey/HEAD/test_assets/privatekey.der -------------------------------------------------------------------------------- /test_assets/publickey.der: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ammario/isokey/HEAD/test_assets/publickey.der -------------------------------------------------------------------------------- /signer.go: -------------------------------------------------------------------------------- 1 | package isokey 2 | 3 | //Signer defines a service that signs keys 4 | type Signer interface { 5 | Sign(key *Key) (digest string, err error) 6 | } 7 | -------------------------------------------------------------------------------- /verifier.go: -------------------------------------------------------------------------------- 1 | package isokey 2 | 3 | //Verifier defines a service that verifies key digests 4 | type Verifier interface { 5 | Verify(digest string) (*Key, error) 6 | } 7 | -------------------------------------------------------------------------------- /isokey.go: -------------------------------------------------------------------------------- 1 | //Package isokey allows you to make and verify API keys without a database connection via HMAC/ECDSA signatures. 2 | //All information needed to verify the key is stored within the key. 3 | package isokey -------------------------------------------------------------------------------- /sym_key_service_test.go: -------------------------------------------------------------------------------- 1 | package isokey_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ammario/isokey" 7 | ) 8 | 9 | func TestSymKeyDigest(t *testing.T) { 10 | ks := isokey.NewSymKeyService([]byte("super_secure111")) 11 | testSignerVerifier(t, ks, ks) 12 | } 13 | -------------------------------------------------------------------------------- /check_mac.go: -------------------------------------------------------------------------------- 1 | package isokey 2 | 3 | import ( 4 | "crypto/hmac" 5 | "crypto/sha256" 6 | ) 7 | 8 | func checkMAC(message, messageMAC, key []byte) bool { 9 | mac := hmac.New(sha256.New, key) 10 | mac.Write(message) 11 | expectedMAC := mac.Sum(nil)[:16] 12 | return hmac.Equal(messageMAC, expectedMAC) 13 | } 14 | -------------------------------------------------------------------------------- /key_test.go: -------------------------------------------------------------------------------- 1 | package isokey 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | func TestKey(t *testing.T) { 11 | t.Run("TruncateNanoseconds()", func(t *testing.T) { 12 | 13 | key := &Key{ 14 | MadeAt: time.Now(), 15 | ExpiresAt: time.Now(), 16 | } 17 | 18 | key.TruncateNanoseconds() 19 | 20 | assert.Zero(t, key.MadeAt.Nanosecond()) 21 | assert.Zero(t, key.ExpiresAt.Nanosecond()) 22 | }) 23 | } 24 | -------------------------------------------------------------------------------- /unpack.go: -------------------------------------------------------------------------------- 1 | package isokey 2 | 3 | import ( 4 | "encoding/binary" 5 | "time" 6 | ) 7 | 8 | func unpack(byt []byte) *Key { 9 | key := &Key{} 10 | key.MadeAt = time.Unix(int64(binary.BigEndian.Uint32(byt[:4])), 0) 11 | key.ExpiresAt = time.Unix(int64(binary.BigEndian.Uint32(byt[4:8])), 0) 12 | key.SecretVersion = binary.BigEndian.Uint32(byt[8:12]) 13 | key.UserID = binary.BigEndian.Uint32(byt[12:16]) 14 | key.Flags = binary.BigEndian.Uint32(byt[16:20]) 15 | return key 16 | } 17 | -------------------------------------------------------------------------------- /asym_key_verifier_signer_test.go: -------------------------------------------------------------------------------- 1 | package isokey_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/ammario/isokey" 7 | "github.com/stretchr/testify/require" 8 | ) 9 | 10 | func TestAsymKeySignerVerifier(t *testing.T) { 11 | privkey, err := isokey.LoadPrivateKey("test_assets/privatekey.der") 12 | require.Nil(t, err) 13 | pubkey, err := isokey.LoadPublicKey("test_assets/publickey.der") 14 | require.Nil(t, err) 15 | 16 | signer := isokey.NewAsymKeySigner(privkey) 17 | verifier := isokey.NewAsymKeyVerifier(pubkey) 18 | 19 | testSignerVerifier(t, signer, verifier) 20 | } 21 | -------------------------------------------------------------------------------- /key.go: -------------------------------------------------------------------------------- 1 | package isokey 2 | 3 | import "time" 4 | 5 | //Key is a self-contained service agnostic API key 6 | type Key struct { 7 | MadeAt time.Time 8 | ExpiresAt time.Time 9 | SecretVersion uint32 10 | UserID uint32 11 | Flags uint32 12 | } 13 | 14 | //TruncateNanoseconds truncates nanosecond precision from k.MadeAt and k.ExpiresAt 15 | func (key *Key) TruncateNanoseconds() { 16 | trunc := func(t time.Time) time.Time { 17 | return t.Add(-time.Duration(t.Nanosecond())) 18 | } 19 | 20 | key.MadeAt = trunc(key.MadeAt) 21 | key.ExpiresAt = trunc(key.ExpiresAt) 22 | } 23 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package isokey 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | ) 7 | 8 | //Common errors 9 | var ( 10 | ErrNoSecret = errors.New("No secret was found for the key.") 11 | ErrSymKeySize = fmt.Errorf("Key is not %v bytes long.", symKeyDigestSize) 12 | ErrInvalid = errors.New("Key is expired or invalid.") 13 | ) 14 | 15 | //Asymmetric key errors 16 | var ( 17 | ErrNoPubKey = errors.New("No public key was found for the key") 18 | ErrNotECPublicKey = errors.New("Not elliptic curve public key") 19 | ErrAsymMessageSize = errors.New("Message portion not 20 bytes") 20 | ErrBadSignature = errors.New("Bad signature or message.") 21 | ) 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Ammar Bandukwala 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /signer_verifier_test.go: -------------------------------------------------------------------------------- 1 | package isokey_test 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | 7 | "github.com/ammario/isokey" 8 | "github.com/stretchr/testify/require" 9 | ) 10 | 11 | //a generic test function pluggable for all signers and verifiers 12 | func testSignerVerifier(t *testing.T, signer isokey.Signer, verifier isokey.Verifier) { 13 | key := func() *isokey.Key { 14 | key := &isokey.Key{ 15 | UserID: 1, 16 | ExpiresAt: time.Now().AddDate(0, 1, 0), 17 | } 18 | return key 19 | } 20 | 21 | t.Run("simple digest and verify", func(t *testing.T) { 22 | k := key() 23 | digest, err := signer.Sign(k) 24 | k.TruncateNanoseconds() 25 | require.Nil(t, err) 26 | require.NotZero(t, digest) 27 | 28 | got, err := verifier.Verify(digest) 29 | require.Equal(t, k, got) 30 | require.Nil(t, err) 31 | }) 32 | 33 | t.Run("bad digest and verify", func(t *testing.T) { 34 | k := key() 35 | digest, err := signer.Sign(k) 36 | require.Nil(t, err) 37 | 38 | t.Logf(" digest: %v\n", digest) 39 | digest = digest[:10] + string(digest[10]+1) + digest[11:] 40 | t.Logf("bad digest: %v\n", digest) 41 | 42 | _, err = verifier.Verify(digest) 43 | require.NotNil(t, err) 44 | }) 45 | 46 | t.Run("expired key and verify", func(t *testing.T) { 47 | k := key() 48 | k.ExpiresAt = time.Now().Add(-time.Second) 49 | digest, err := signer.Sign(k) 50 | require.Nil(t, err) 51 | 52 | _, err = verifier.Verify(digest) 53 | require.Equal(t, isokey.ErrInvalid, err) 54 | }) 55 | } 56 | -------------------------------------------------------------------------------- /asym_key_signer.go: -------------------------------------------------------------------------------- 1 | package isokey 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/sha256" 7 | "encoding/binary" 8 | "io/ioutil" 9 | "time" 10 | 11 | "crypto/rand" 12 | 13 | "crypto/x509" 14 | 15 | "github.com/jbenet/go-base58" 16 | ) 17 | 18 | var _ = Signer(new(AsymKeySigner)) 19 | 20 | //AsymKeySigner facilitates the creation ECDSA API keys 21 | type AsymKeySigner struct { 22 | //GetPrivateKey allows you to dynamically use secrets. 23 | //Returning nil indicates that no secret was found for the key 24 | GetPrivateKey func(key *Key) *ecdsa.PrivateKey 25 | } 26 | 27 | //NewAsymKeySigner returns an instantiated asymkeysigner which always uses privkey 28 | func NewAsymKeySigner(privkey *ecdsa.PrivateKey) *AsymKeySigner { 29 | return &AsymKeySigner{ 30 | GetPrivateKey: func(key *Key) *ecdsa.PrivateKey { 31 | return privkey 32 | }, 33 | } 34 | } 35 | 36 | //LoadPrivateKey loads an ASN.1 ECDSA private key from a file. 37 | func LoadPrivateKey(filename string) (privKey *ecdsa.PrivateKey, err error) { 38 | byt, err := ioutil.ReadFile(filename) 39 | if err != nil { 40 | return nil, err 41 | } 42 | return x509.ParseECPrivateKey(byt) 43 | } 44 | 45 | //Sign signs the API key and provides it's base58 digest. 46 | //An error will be returned if the corresponding private key cannot be located. 47 | //if key.Made is zero it is set to the current time. 48 | func (ks *AsymKeySigner) Sign(key *Key) (digest string, err error) { 49 | message := &bytes.Buffer{} 50 | 51 | if key.MadeAt.IsZero() { 52 | key.MadeAt = time.Now() 53 | } 54 | 55 | binary.Write(message, binary.BigEndian, uint32(key.MadeAt.Unix())) 56 | binary.Write(message, binary.BigEndian, uint32(key.ExpiresAt.Unix())) 57 | binary.Write(message, binary.BigEndian, key.SecretVersion) 58 | binary.Write(message, binary.BigEndian, key.UserID) 59 | binary.Write(message, binary.BigEndian, key.Flags) 60 | 61 | privKey := ks.GetPrivateKey(key) 62 | 63 | if privKey == nil { 64 | return "", ErrNoSecret 65 | } 66 | 67 | checksum := sha256.Sum256(message.Bytes()) 68 | 69 | signhash := checksum[:16] 70 | 71 | r, s, err := ecdsa.Sign(rand.Reader, privKey, signhash) 72 | if err != nil { 73 | return "", err 74 | } 75 | 76 | digestBuf := &bytes.Buffer{} 77 | 78 | digestBuf.WriteByte(uint8(len(r.Bytes()))) 79 | digestBuf.Write(r.Bytes()) 80 | digestBuf.WriteByte(uint8(len(s.Bytes()))) 81 | digestBuf.Write(s.Bytes()) 82 | 83 | //fmt.Printf("r %x s %x buf %x pkXY %s %s\n", r.Bytes(), s.Bytes(), message.Bytes(), privKey.PublicKey.X, privKey.PublicKey.Y) 84 | 85 | digestBuf.Write(message.Bytes()) 86 | 87 | return base58.Encode(digestBuf.Bytes()), nil 88 | } 89 | -------------------------------------------------------------------------------- /sym_key_service.go: -------------------------------------------------------------------------------- 1 | package isokey 2 | 3 | import ( 4 | "bytes" 5 | "crypto/hmac" 6 | "crypto/sha256" 7 | "encoding/binary" 8 | "time" 9 | 10 | "github.com/jbenet/go-base58" 11 | ) 12 | 13 | const symKeyDigestSize = 16 + 4 + 4 + 4 + 4 + 4 14 | 15 | var _ = Signer(new(SymKeyService)) 16 | var _ = Verifier(new(SymKeyService)) 17 | 18 | //SymKeyService facilitates the creation and verification of symmetricly signed (HMAC) keys 19 | type SymKeyService struct { 20 | //GetSecret allows you to dynamically use secrets. 21 | //Returning nil indicates that no secret was found for the key 22 | GetSecret func(key *Key) (secret []byte) 23 | 24 | //Invalidator allows you to dynamically invalidate a key. 25 | //Invalidator is ran after the key's signature has been validated. 26 | Invalidator func(*Key) bool 27 | } 28 | 29 | //NewSymKeyService returns a new sym key service using a single key 30 | func NewSymKeyService(secret []byte) *SymKeyService { 31 | return &SymKeyService{ 32 | GetSecret: func(key *Key) []byte { 33 | return secret 34 | }, 35 | } 36 | } 37 | 38 | //Invalid returns true if the key is invalid. 39 | func (ks *SymKeyService) Invalid(key *Key) bool { 40 | if ks.Invalidator == nil { 41 | return key.ExpiresAt.Before(time.Now()) 42 | } 43 | return key.ExpiresAt.Before(time.Now()) || ks.Invalidator(key) 44 | } 45 | 46 | //Verify securely validates a digest. 47 | func (ks *SymKeyService) Verify(digest string) (*Key, error) { 48 | key := &Key{} 49 | 50 | rawDigest := base58.Decode(digest) 51 | if len(rawDigest) != symKeyDigestSize { 52 | return key, ErrSymKeySize 53 | } 54 | signature := rawDigest[:16] 55 | 56 | key = unpack(rawDigest[16:]) 57 | 58 | secret := ks.GetSecret(key) 59 | if secret == nil { 60 | return nil, ErrNoSecret 61 | } 62 | 63 | if !checkMAC(rawDigest[16:], signature, secret) { 64 | return key, ErrBadSignature 65 | } 66 | 67 | if ks.Invalid(key) { 68 | return key, ErrInvalid 69 | } 70 | 71 | return key, nil 72 | } 73 | 74 | //Sign converts the key into it's base58 form. 75 | //the only error that will be returned is ErrNoSecret. 76 | //if key.MadeAt is zero it is set to the current time. 77 | func (ks *SymKeyService) Sign(key *Key) (digest string, err error) { 78 | message := &bytes.Buffer{} 79 | 80 | if key.MadeAt.IsZero() { 81 | key.MadeAt = time.Now() 82 | } 83 | 84 | binary.Write(message, binary.BigEndian, int32(key.MadeAt.Unix())) 85 | binary.Write(message, binary.BigEndian, int32(key.ExpiresAt.Unix())) 86 | 87 | binary.Write(message, binary.BigEndian, key.SecretVersion) 88 | binary.Write(message, binary.BigEndian, key.UserID) 89 | binary.Write(message, binary.BigEndian, key.Flags) 90 | 91 | secret := ks.GetSecret(key) 92 | if secret == nil { 93 | return "", ErrNoSecret 94 | } 95 | mac := hmac.New(sha256.New, secret) 96 | mac.Write(message.Bytes()) 97 | 98 | signature := mac.Sum(nil)[:16] 99 | 100 | return base58.Encode(append(signature, message.Bytes()...)), nil 101 | } 102 | -------------------------------------------------------------------------------- /asym_key_verifier.go: -------------------------------------------------------------------------------- 1 | package isokey 2 | 3 | import ( 4 | "bytes" 5 | "crypto/ecdsa" 6 | "crypto/x509" 7 | "io/ioutil" 8 | "time" 9 | 10 | "math/big" 11 | 12 | "crypto/sha256" 13 | 14 | "github.com/jbenet/go-base58" 15 | ) 16 | 17 | var _ = Verifier(new(AsymKeyVerifier)) 18 | 19 | //AsymKeyVerifier verifies ECDSA signed API keys 20 | type AsymKeyVerifier struct { 21 | //GetPublicKey allows you to dynamically use public keys based on the contents of a key. 22 | //Returning nil indicates that no pubkey was found for key 23 | GetPublicKey func(key *Key) *ecdsa.PublicKey 24 | 25 | //Invalidator allows you to invalidate certain keys based off the Key's parameters (e.g when it was made.) 26 | //Invalidator is ran after the key's signature has been validated. 27 | //This is useful to deal with cases revolving compromised users. 28 | Invalidator func(*Key) bool 29 | } 30 | 31 | //NewAsymKeyVerifier returns an instantiated AsymKeyVerifier which always uses pubkey 32 | func NewAsymKeyVerifier(pubkey *ecdsa.PublicKey) *AsymKeyVerifier { 33 | return &AsymKeyVerifier{ 34 | GetPublicKey: func(key *Key) *ecdsa.PublicKey { 35 | return pubkey 36 | }, 37 | } 38 | } 39 | 40 | //LoadPublicKey loads an ASN.1 ECDSA public key from a file. 41 | func LoadPublicKey(filename string) (publicKey *ecdsa.PublicKey, err error) { 42 | byt, err := ioutil.ReadFile(filename) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | pubKeyI, err := x509.ParsePKIXPublicKey(byt) 48 | if err != nil { 49 | return 50 | } 51 | 52 | pubKey, ok := pubKeyI.(*ecdsa.PublicKey) 53 | 54 | if !ok { 55 | return nil, ErrNotECPublicKey 56 | } 57 | 58 | return pubKey, err 59 | } 60 | 61 | //Invalidate invalidates a key 62 | func (kv *AsymKeyVerifier) Invalidate(key *Key) bool { 63 | if kv.Invalidator == nil { 64 | return key.ExpiresAt.Before(time.Now()) 65 | } 66 | return key.ExpiresAt.Before(time.Now()) || kv.Invalidator(key) 67 | } 68 | 69 | //Verify verifies and parses a key. 70 | //It returns an error if the key is invalid. 71 | func (kv *AsymKeyVerifier) Verify(digest string) (key *Key, err error) { 72 | key = &Key{} 73 | rawDigest := base58.Decode(digest) 74 | buf := bytes.NewBuffer(rawDigest) 75 | 76 | rLen, err := buf.ReadByte() 77 | if err != nil { 78 | return 79 | } 80 | 81 | r := make([]byte, rLen) 82 | buf.Read(r) 83 | 84 | sLen, err := buf.ReadByte() 85 | if err != nil { 86 | return 87 | } 88 | s := make([]byte, sLen) 89 | buf.Read(s) 90 | 91 | if buf.Len() != 20 { 92 | return nil, ErrAsymMessageSize 93 | } 94 | 95 | key = unpack(buf.Bytes()) 96 | 97 | pubKey := kv.GetPublicKey(key) 98 | 99 | if pubKey == nil { 100 | return nil, ErrNoPubKey 101 | } 102 | 103 | //fmt.Printf("r %x s %x buf %x pkXY %s %s\n", r, s, buf.Bytes(), pubKey.X, pubKey.Y) 104 | 105 | checksum := sha256.Sum256(buf.Bytes()) 106 | signhash := checksum[:16] 107 | 108 | if !ecdsa.Verify(pubKey, signhash, big.NewInt(0).SetBytes(r), big.NewInt(0).SetBytes(s)) { 109 | return key, ErrBadSignature 110 | } 111 | 112 | if kv.Invalidate(key) { 113 | return key, ErrInvalid 114 | } 115 | 116 | return key, nil 117 | } 118 | -------------------------------------------------------------------------------- /coverage.out: -------------------------------------------------------------------------------- 1 | mode: set 2 | github.com/ammario/isokey/asym_key_signer.go:28.65,30.51 1 1 3 | github.com/ammario/isokey/asym_key_signer.go:30.51,32.4 1 1 4 | github.com/ammario/isokey/asym_key_signer.go:37.77,39.16 2 1 5 | github.com/ammario/isokey/asym_key_signer.go:42.2,42.36 1 1 6 | github.com/ammario/isokey/asym_key_signer.go:39.16,41.3 1 0 7 | github.com/ammario/isokey/asym_key_signer.go:48.68,51.25 2 1 8 | github.com/ammario/isokey/asym_key_signer.go:55.2,63.20 7 1 9 | github.com/ammario/isokey/asym_key_signer.go:67.2,72.16 4 1 10 | github.com/ammario/isokey/asym_key_signer.go:76.2,87.46 7 1 11 | github.com/ammario/isokey/asym_key_signer.go:51.25,53.3 1 1 12 | github.com/ammario/isokey/asym_key_signer.go:63.20,65.3 1 0 13 | github.com/ammario/isokey/asym_key_signer.go:72.16,74.3 1 0 14 | github.com/ammario/isokey/asym_key_verifier.go:32.67,34.49 1 1 15 | github.com/ammario/isokey/asym_key_verifier.go:34.49,36.4 1 1 16 | github.com/ammario/isokey/asym_key_verifier.go:41.77,43.16 2 1 17 | github.com/ammario/isokey/asym_key_verifier.go:47.2,48.16 2 1 18 | github.com/ammario/isokey/asym_key_verifier.go:52.2,54.9 2 1 19 | github.com/ammario/isokey/asym_key_verifier.go:58.2,58.20 1 1 20 | github.com/ammario/isokey/asym_key_verifier.go:43.16,45.3 1 0 21 | github.com/ammario/isokey/asym_key_verifier.go:48.16,50.3 1 0 22 | github.com/ammario/isokey/asym_key_verifier.go:54.9,56.3 1 0 23 | github.com/ammario/isokey/asym_key_verifier.go:62.54,63.27 1 1 24 | github.com/ammario/isokey/asym_key_verifier.go:66.2,66.64 1 0 25 | github.com/ammario/isokey/asym_key_verifier.go:63.27,65.3 1 1 26 | github.com/ammario/isokey/asym_key_verifier.go:71.72,77.16 5 1 27 | github.com/ammario/isokey/asym_key_verifier.go:81.2,85.16 4 1 28 | github.com/ammario/isokey/asym_key_verifier.go:88.2,91.21 3 1 29 | github.com/ammario/isokey/asym_key_verifier.go:95.2,99.19 3 1 30 | github.com/ammario/isokey/asym_key_verifier.go:105.2,108.91 3 1 31 | github.com/ammario/isokey/asym_key_verifier.go:112.2,112.24 1 1 32 | github.com/ammario/isokey/asym_key_verifier.go:116.2,116.17 1 1 33 | github.com/ammario/isokey/asym_key_verifier.go:77.16,79.3 1 0 34 | github.com/ammario/isokey/asym_key_verifier.go:85.16,87.3 1 0 35 | github.com/ammario/isokey/asym_key_verifier.go:91.21,93.3 1 1 36 | github.com/ammario/isokey/asym_key_verifier.go:99.19,101.3 1 0 37 | github.com/ammario/isokey/asym_key_verifier.go:108.91,110.3 1 0 38 | github.com/ammario/isokey/asym_key_verifier.go:112.24,114.3 1 1 39 | github.com/ammario/isokey/check_mac.go:8.53,13.2 4 1 40 | github.com/ammario/isokey/key.go:17.39,18.39 1 1 41 | github.com/ammario/isokey/key.go:22.2,23.38 2 1 42 | github.com/ammario/isokey/key.go:18.39,20.3 1 1 43 | github.com/ammario/isokey/sym_key_service.go:32.53,34.36 1 1 44 | github.com/ammario/isokey/sym_key_service.go:34.36,36.4 1 1 45 | github.com/ammario/isokey/sym_key_service.go:41.49,42.27 1 1 46 | github.com/ammario/isokey/sym_key_service.go:45.2,45.64 1 0 47 | github.com/ammario/isokey/sym_key_service.go:42.27,44.3 1 1 48 | github.com/ammario/isokey/sym_key_service.go:49.62,53.40 3 1 49 | github.com/ammario/isokey/sym_key_service.go:56.2,61.19 4 1 50 | github.com/ammario/isokey/sym_key_service.go:65.2,65.50 1 1 51 | github.com/ammario/isokey/sym_key_service.go:69.2,69.21 1 1 52 | github.com/ammario/isokey/sym_key_service.go:73.2,73.17 1 1 53 | github.com/ammario/isokey/sym_key_service.go:53.40,55.3 1 0 54 | github.com/ammario/isokey/sym_key_service.go:61.19,63.3 1 0 55 | github.com/ammario/isokey/sym_key_service.go:65.50,67.3 1 1 56 | github.com/ammario/isokey/sym_key_service.go:69.21,71.3 1 1 57 | github.com/ammario/isokey/sym_key_service.go:79.68,82.25 2 1 58 | github.com/ammario/isokey/sym_key_service.go:86.2,94.19 7 1 59 | github.com/ammario/isokey/sym_key_service.go:97.2,102.66 4 1 60 | github.com/ammario/isokey/sym_key_service.go:82.25,84.3 1 1 61 | github.com/ammario/isokey/sym_key_service.go:94.19,96.3 1 0 62 | github.com/ammario/isokey/unpack.go:8.30,16.2 7 1 63 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Isokey 2 | 3 | Isokey allows you to make and verify self-contained API keys without a database via HMAC/ECDSA signatures. 4 | 5 | ## Features 6 | - Important information such as userID, key expire time, and flags are authenticated and stored within 7 | the key. 8 | - Use mutliple secrets 9 | - Invalidate secrets and compromised keys 10 | 11 | 12 | 13 | ## Table Of Contents 14 | 15 | - [Symmetric Keys](#symmetric-keys) 16 | - [Make a key service](#make-a-key-service) 17 | - [Sign a new key](#sign-a-new-key) 18 | - [Verify key](#verify-key) 19 | - [Using multiple secrets](#using-multiple-secrets) 20 | - [Digest Structure](#digest-structure) 21 | - [Asymmetric Keys](#asymmetric-keys) 22 | - [Make a key pair](#make-a-key-pair) 23 | - [Make key digest](#make-key-digest) 24 | - [Verify key](#verify-key-1) 25 | - [Digest Structure](#digest-structure-1) 26 | - [Invalidating keys](#invalidating-keys) 27 | 28 | 29 | 30 | # Symmetric Keys 31 | 32 | ## Make a key service 33 | ```go 34 | ks := NewSymKeyService([]byte("super_secure111")) 35 | ``` 36 | 37 | ## Sign a new key 38 | ```go 39 | key := &Key{ 40 | UserID: 1, 41 | Expires: time.Now().AddDate(0, 1, 0), 42 | } 43 | 44 | digest, err := ks.Sign(key) 45 | if err != nil { 46 | log.Fatalf("Failed to sign key: %v", err) 47 | } 48 | fmt.Printf("Digest is %v\n", digest) 49 | ``` 50 | 51 | ## Verify key 52 | 53 | ```go 54 | key, err = ks.Verify(digest) 55 | 56 | if err != nil { 57 | log.Fatalf("Failed to verify digest: %v", err) 58 | } 59 | 60 | // Key authenticated 61 | fmt.Printf("Key: %+v\n", key) 62 | ``` 63 | 64 | ## Using multiple secrets 65 | The SecretVersion field is in included in the key object to enable 66 | implementors to easily use multiple secrets. 67 | 68 | A secret can be decided based on any feature of a key. 69 | 70 | ```go 71 | ks.GetSecret = function(key *Key) (secret []byte){ 72 | if key.SecretVersion == 1 { 73 | return []byte("sec1") 74 | } 75 | return nil 76 | } 77 | ``` 78 | 79 | 80 | 81 | ## Digest Structure 82 | All binary values are big endian. 83 | 84 | | Field | Type | 85 | |--------|------| 86 | | Signature | [16]byte | 87 | | Made Time (Unix epoch timestamp) | uint32 | 88 | | Expire Time (Unix epoch timestamp) | uint32 | 89 | | Secret Version | uint32 | 90 | | User ID | uint32 | 91 | | Flags | uint32 | 92 | 93 | Digests are encoded with Bitcoin's base58 alphabet. 94 | 95 | It may seem intuitive to put the signature at the end of the digest. It's located 96 | at the beginning as it makes eyeballing different keys easy. 97 | 98 | # Asymmetric Keys 99 | 100 | ## Make a key pair 101 | 102 | Make your private key 103 | `openssl ecparam -genkey -name prime256v1 -outform DER -noout -out privatekey.der` 104 | 105 | Make your public key 106 | `openssl ec -in privatekey.der -inform DER -outform DER -pubout -out publickey.der` 107 | 108 | 109 | ## Make key digest 110 | ```go 111 | privKey, _ = isokey.LoadPrivateKey("priv.key") 112 | 113 | ks := NewAsymKeySigner(privKey) 114 | 115 | key := &Key{ 116 | User: 1, 117 | Expires: time.Now().Add(time.Hour) 118 | } 119 | 120 | digest, _ := ks.Sign(key) 121 | 122 | fmt.Printf("Digest: %v", digest) 123 | ``` 124 | 125 | ## Verify key 126 | ```go 127 | pubKey, err := isokey.LoadPublicKey("pub.key") 128 | if err != nil { 129 | log.Fatalf("Failed to load pubkey: %v", err) 130 | } 131 | 132 | kv := NewAsymKeyVerifier(pubKey) 133 | 134 | key, err := kv.Verify(digest) 135 | if err != nil { 136 | log.Fatalf("Failed to verify key: %v", err) 137 | } 138 | 139 | fmt.Printf("Key verified %+v\n", key) 140 | 141 | ``` 142 | 143 | 144 | ## Digest Structure 145 | All binary values are big endian. 146 | 147 | | Field | Type | 148 | |--------|------| 149 | | R len | uint8 150 | | R | []byte 151 | | S Len | uint8 152 | | S | []byte 153 | | Made Time (Unix epoch timestamp) | uint32 | 154 | | Expire Time (Unix epoch timestamp) | uint32 | 155 | | Secret Version | uint32 | 156 | | User ID | uint32 | 157 | | Flags | uint32 | 158 | 159 | Digests are encoded with Bitcoin's base58 alphabet. 160 | 161 | 162 | # Invalidating keys 163 | 164 | Expired keys always fail to validate. 165 | 166 | You can add custom invalidation logic via the `Invalidator` field of verifiers. 167 | 168 | ```go 169 | verifier.Invalidator = function(key *isokey.Key) bool { 170 | // reject keys made before some time 171 | if key.UserID == 10 && key.Made.Before(time.Date(2015, time.November, 10, 23, 0, 0, 0, time.UTC)) { 172 | return true 173 | } 174 | return false 175 | } 176 | ``` 177 | --------------------------------------------------------------------------------