├── go.mod ├── .gitignore ├── .github └── workflows │ └── tests.yml ├── suite_test.go ├── LICENSE ├── helpers.go ├── lkgen ├── README.md └── main.go ├── go.sum ├── license.go ├── license_test.go ├── keys_test.go ├── keys.go ├── examples_test.go └── README.md /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hyperboloide/lk 2 | 3 | go 1.17 4 | 5 | require gopkg.in/alecthomas/kingpin.v2 v2.2.6 6 | 7 | require ( 8 | github.com/davecgh/go-spew v1.1.1 // indirect 9 | github.com/pmezard/go-difflib v1.0.0 // indirect 10 | ) 11 | 12 | require ( 13 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 // indirect 14 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect 15 | github.com/stretchr/testify v1.11.1 16 | gopkg.in/yaml.v3 v3.0.1 // indirect 17 | ) 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | /lkgen/lkgen 26 | /examples/gen_keys/gen_keys 27 | /lkgen/license.pub.key 28 | /lkgen/license.priv.key 29 | /lkgen/license.key 30 | /lk.coverprofile 31 | 32 | # IDE files 33 | *.iml 34 | /.idea 35 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | on: [push] 3 | 4 | jobs: 5 | 6 | tests: 7 | name: Test suite 8 | runs-on: ubuntu-latest 9 | strategy: 10 | matrix: 11 | go-version: ["1.25"] 12 | 13 | steps: 14 | - uses: actions/checkout@v5 15 | 16 | - uses: actions/setup-go@v6 17 | with: 18 | go-version: stable 19 | - name: golangci-lint 20 | uses: golangci/golangci-lint-action@v9 21 | with: 22 | version: v2.6 23 | 24 | - name: Run Gosec Security Scanner 25 | uses: securego/gosec@master 26 | with: 27 | args: ./... 28 | 29 | - name: Tests 30 | run: go test -count=1 -timeout 240s -race -cover ./... 31 | 32 | - name: Build lkgen 33 | run: cd lkgen && go build -------------------------------------------------------------------------------- /suite_test.go: -------------------------------------------------------------------------------- 1 | package lk_test 2 | 3 | import ( 4 | "crypto/rand" 5 | "encoding/base32" 6 | "encoding/base64" 7 | "encoding/hex" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/suite" 11 | ) 12 | 13 | type ( 14 | Suite struct { 15 | suite.Suite 16 | } 17 | ) 18 | 19 | func TestSuite(t *testing.T) { 20 | suite.Run(t, &Suite{}) 21 | } 22 | 23 | func (s *Suite) RandomBytes(n int) []byte { 24 | b := make([]byte, n) 25 | _, err := rand.Read(b) 26 | s.Require().NoError(err) 27 | return b 28 | } 29 | 30 | func (s *Suite) RandomB64String(n int) string { 31 | b := s.RandomBytes(n) 32 | return base64.RawStdEncoding.EncodeToString(b) 33 | } 34 | 35 | func (s *Suite) RandomB32String(n int) string { 36 | b := s.RandomBytes(n) 37 | return base32.StdEncoding.EncodeToString(b) 38 | } 39 | 40 | func (s *Suite) RandomHexString(n int) string { 41 | b := s.RandomBytes(n) 42 | return hex.EncodeToString(b) 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2025 Frederic Delbos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /helpers.go: -------------------------------------------------------------------------------- 1 | package lk 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base32" 6 | "encoding/base64" 7 | "encoding/gob" 8 | "encoding/hex" 9 | ) 10 | 11 | func toBytes(obj interface{}) ([]byte, error) { 12 | var buffBin bytes.Buffer 13 | 14 | encoderBin := gob.NewEncoder(&buffBin) 15 | if err := encoderBin.Encode(obj); err != nil { 16 | return nil, err 17 | } 18 | 19 | return buffBin.Bytes(), nil 20 | } 21 | 22 | func toB64String(obj interface{}) (string, error) { 23 | b, err := toBytes(obj) 24 | if err != nil { 25 | return "", err 26 | } 27 | 28 | return base64.StdEncoding.EncodeToString(b), nil 29 | } 30 | 31 | func toB32String(obj interface{}) (string, error) { 32 | b, err := toBytes(obj) 33 | if err != nil { 34 | return "", err 35 | } 36 | 37 | return base32.StdEncoding.EncodeToString(b), nil 38 | } 39 | 40 | func toHexString(obj interface{}) (string, error) { 41 | b, err := toBytes(obj) 42 | if err != nil { 43 | return "", err 44 | } 45 | 46 | return hex.EncodeToString(b), nil 47 | } 48 | 49 | func fromBytes(obj interface{}, b []byte) error { 50 | buffBin := bytes.NewBuffer(b) 51 | decoder := gob.NewDecoder(buffBin) 52 | 53 | return decoder.Decode(obj) 54 | } 55 | 56 | func fromB64String(obj interface{}, s string) error { 57 | b, err := base64.StdEncoding.DecodeString(s) 58 | if err != nil { 59 | return err 60 | } 61 | 62 | return fromBytes(obj, b) 63 | } 64 | 65 | func fromB32String(obj interface{}, s string) error { 66 | b, err := base32.StdEncoding.DecodeString(s) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return fromBytes(obj, b) 72 | } 73 | 74 | func fromHexString(obj interface{}, s string) error { 75 | b, err := hex.DecodeString(s) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return fromBytes(obj, b) 81 | } 82 | -------------------------------------------------------------------------------- /lkgen/README.md: -------------------------------------------------------------------------------- 1 | # lkgen 2 | `lkgen` is a license generation and validation utility using the [lk](https://github.com/hyperboloide/lk) library as a backend. 3 | 4 | It can be used standalone to generate cypto keys, sign and validate license keys. 5 | 6 | Install it with the following command : 7 | 8 | ```sh 9 | go install github.com/hyperboloide/lk/lkgen@latest 10 | ``` 11 | 12 | ## Complete flow example: 13 | 14 | 1. Generate a private key file: 15 | ```sh 16 | lkgen gen --output=./private.key 17 | ``` 18 | 2. Generate a public key to distribute with your app: 19 | ```sh 20 | lkgen pub ./private.key --output=./pub.key 21 | ``` 22 | 3. Create the licence (here we use json but can be anything...): 23 | ```sh 24 | echo '{"email":"user@example.com", "until":"2023-10-04"}' > license.tmp 25 | ``` 26 | 4. sign the license: 27 | ```sh 28 | lkgen sign --input=./license.tmp --output=./license.signed private.key 29 | ``` 30 | The file `license.signed` is your redistributable license. 31 | 32 | 5. validate the license: 33 | ```sh 34 | lkgen verify --input=./license.signed ./pub.key 35 | {"email":"user@example.com", "until":"2023-10-04"} 36 | echo $? 37 | 0 38 | ``` 39 | 40 | ## Reference documentation 41 | 42 | ``` 43 | lkgen --help-long 44 | usage: lkgen [] [ ...] 45 | 46 | A command-line utility to generate private keys and licenses. 47 | 48 | Flags: 49 | --help Show context-sensitive help (also try --help-long and --help-man). 50 | 51 | Commands: 52 | help [...] 53 | Show help. 54 | 55 | 56 | gen [] 57 | Generates a base32 encoded private key. 58 | 59 | -o, --output=OUTPUT Output file (if not defined then stdout). 60 | 61 | pub [] 62 | Get the public key. 63 | 64 | -o, --output=OUTPUT Output file (if not defined then stdout). 65 | 66 | sign [] 67 | Creates a license. 68 | 69 | -i, --input=INPUT Input data file (if not defined then stdin). 70 | -o, --output=OUTPUT Output file (if not defined then stdout). 71 | 72 | verify [] 73 | Verifies a license. 74 | 75 | -i, --input=INPUT Input license file (if not defined then stdin). 76 | 77 | ``` 78 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751 h1:JYp7IbQjafoB+tBA3gMyHYHrpOtNuDiK/uB5uXxq5wM= 2 | github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 3 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= 4 | github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 9 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 10 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 11 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 12 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 13 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 14 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 15 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 16 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 17 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 18 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 19 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 20 | gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc= 21 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 23 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 25 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 26 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 27 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 28 | -------------------------------------------------------------------------------- /license.go: -------------------------------------------------------------------------------- 1 | package lk 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/rand" 6 | "crypto/sha256" 7 | "math/big" 8 | ) 9 | 10 | // License represents a license with some data and a hash. 11 | type License struct { 12 | Data []byte 13 | R *big.Int 14 | S *big.Int 15 | } 16 | 17 | // NewLicense create a new license and sign it. 18 | func NewLicense(k *PrivateKey, data []byte) (*License, error) { 19 | l := &License{ 20 | Data: data, 21 | } 22 | 23 | if h, err := l.hash(); err != nil { 24 | return nil, err 25 | } else if r, s, err := ecdsa.Sign(rand.Reader, k.toEcdsa(), h); err != nil { 26 | return nil, err 27 | } else { 28 | l.R = r 29 | l.S = s 30 | } 31 | return l, nil 32 | } 33 | 34 | func (l *License) hash() ([]byte, error) { 35 | h256 := sha256.New() 36 | 37 | if _, err := h256.Write(l.Data); err != nil { 38 | return nil, err 39 | } 40 | return h256.Sum(nil), nil 41 | } 42 | 43 | // Verify the License with the public key 44 | func (l *License) Verify(k *PublicKey) (bool, error) { 45 | h, err := l.hash() 46 | if err != nil { 47 | return false, err 48 | } 49 | 50 | return ecdsa.Verify(k.toEcdsa(), h, l.R, l.S), nil 51 | } 52 | 53 | // ToBytes transforms the licence to a base64 []byte. 54 | func (l *License) ToBytes() ([]byte, error) { 55 | return toBytes(l) 56 | } 57 | 58 | // ToB64String transforms the licence to a base64 []byte. 59 | func (l *License) ToB64String() (string, error) { 60 | return toB64String(l) 61 | } 62 | 63 | // ToB32String transforms the license to a base32 []byte. 64 | func (l *License) ToB32String() (string, error) { 65 | return toB32String(l) 66 | } 67 | 68 | // ToHexString transforms the license to a hexadecimal []byte. 69 | func (l *License) ToHexString() (string, error) { 70 | return toHexString(l) 71 | } 72 | 73 | // LicenseFromBytes returns a License from a []byte. 74 | func LicenseFromBytes(b []byte) (*License, error) { 75 | l := &License{} 76 | return l, fromBytes(l, b) 77 | } 78 | 79 | // LicenseFromB64String returns a License from a base64 encoded 80 | // string. 81 | func LicenseFromB64String(str string) (*License, error) { 82 | l := &License{} 83 | return l, fromB64String(l, str) 84 | } 85 | 86 | // LicenseFromB32String returns a License from a base64 encoded 87 | // string. 88 | func LicenseFromB32String(str string) (*License, error) { 89 | l := &License{} 90 | return l, fromB32String(l, str) 91 | } 92 | 93 | // LicenseFromHexString returns a License from a hexadecimal encoded 94 | // string. 95 | func LicenseFromHexString(str string) (*License, error) { 96 | l := &License{} 97 | return l, fromHexString(l, str) 98 | } 99 | -------------------------------------------------------------------------------- /license_test.go: -------------------------------------------------------------------------------- 1 | package lk_test 2 | 3 | import ( 4 | "bytes" 5 | 6 | "github.com/hyperboloide/lk" 7 | ) 8 | 9 | func (s *Suite) TestExamples() { 10 | s.Run("Example complete", Example_complete) 11 | s.Run("Example license generation", Example_licenseGeneration) 12 | s.Run("Example license verification", Example_licenseVerification) 13 | } 14 | 15 | func (s *Suite) TestLicense() { 16 | 17 | var privateKey *lk.PrivateKey // private key for license generation 18 | var wrongKey *lk.PrivateKey // wrong key for verification 19 | var license *lk.License // license to be tested 20 | var theData []byte // data to be signed 21 | 22 | s.Run("Generate test data", func() { 23 | var err error 24 | 25 | privateKey, err = lk.NewPrivateKey() 26 | s.Require().NoError(err) 27 | s.Require().NotNil(privateKey) 28 | 29 | wrongKey, err = lk.NewPrivateKey() 30 | s.Require().NoError(err) 31 | s.Require().NotNil(wrongKey) 32 | 33 | theData = s.RandomBytes(100) 34 | 35 | license, err = lk.NewLicense(privateKey, theData) 36 | s.Require().NoError(err) 37 | s.Require().NotNil(license) 38 | 39 | ok, err := license.Verify(privateKey.GetPublicKey()) 40 | s.Require().NoError(err) 41 | s.Require().True(ok) 42 | }) 43 | 44 | s.Run("Should not validate with wrong key", func() { 45 | ok, err := license.Verify(wrongKey.GetPublicKey()) 46 | s.Require().NoError(err) 47 | s.Require().False(ok) 48 | }) 49 | 50 | s.Run("Test license with bytes", func() { 51 | b2, err := license.ToBytes() 52 | s.Require().NoError(err) 53 | 54 | l2, err := lk.LicenseFromBytes(b2) 55 | s.Require().NoError(err) 56 | 57 | ok, err := l2.Verify(privateKey.GetPublicKey()) 58 | s.Require().NoError(err) 59 | s.Require().True(ok) 60 | 61 | s.Require().True(bytes.Equal(license.Data, l2.Data)) 62 | }) 63 | 64 | s.Run("Test license with b64", func() { 65 | b2, err := license.ToB64String() 66 | s.Require().NoError(err) 67 | 68 | l2, err := lk.LicenseFromB64String(b2) 69 | s.Require().NoError(err) 70 | 71 | ok, err := l2.Verify(privateKey.GetPublicKey()) 72 | s.Require().NoError(err) 73 | s.Require().True(ok) 74 | 75 | s.Require().True(bytes.Equal(license.Data, l2.Data)) 76 | }) 77 | 78 | s.Run("should test a license with b32", func() { 79 | b2, err := license.ToB32String() 80 | s.Require().NoError(err) 81 | 82 | l2, err := lk.LicenseFromB32String(b2) 83 | s.Require().NoError(err) 84 | 85 | ok, err := l2.Verify(privateKey.GetPublicKey()) 86 | s.Require().NoError(err) 87 | s.Require().True(ok) 88 | 89 | s.Require().True(bytes.Equal(license.Data, l2.Data)) 90 | }) 91 | 92 | s.Run("should test a license with hex", func() { 93 | b2, err := license.ToHexString() 94 | s.Require().NoError(err) 95 | 96 | l2, err := lk.LicenseFromHexString(b2) 97 | s.Require().NoError(err) 98 | 99 | ok, err := l2.Verify(privateKey.GetPublicKey()) 100 | s.Require().NoError(err) 101 | s.Require().True(ok) 102 | 103 | s.Require().True(bytes.Equal(license.Data, l2.Data)) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /keys_test.go: -------------------------------------------------------------------------------- 1 | package lk_test 2 | 3 | import ( 4 | "github.com/hyperboloide/lk" 5 | ) 6 | 7 | func (s *Suite) TestKeys() { 8 | k, err := lk.NewPrivateKey() 9 | s.Require().NoError(err) 10 | 11 | s.Run("should test private key bytes", func() { 12 | b, err := k.ToBytes() 13 | s.Require().NoError(err) 14 | k1, err := lk.PrivateKeyFromBytes(b) 15 | s.Require().NoError(err) 16 | s.Require().Equal(k1, k) 17 | 18 | invalidBytes := s.RandomBytes(42) 19 | k2, err := lk.PrivateKeyFromBytes(invalidBytes) 20 | s.Require().Error(err) 21 | s.Require().Nil(k2) 22 | }) 23 | 24 | s.Run("should test private key bytes", func() { 25 | b, err := k.ToBytes() 26 | s.Require().NoError(err) 27 | k1, err := lk.PrivateKeyFromBytes(b) 28 | s.Require().NoError(err) 29 | s.Require().Equal(k1, k) 30 | 31 | invalidBytes := s.RandomBytes(42) 32 | k2, err := lk.PrivateKeyFromBytes(invalidBytes) 33 | s.Require().Error(err) 34 | s.Require().Nil(k2) 35 | }) 36 | 37 | tc := []struct { 38 | name string 39 | privateKeyToString func(k *lk.PrivateKey) (string, error) 40 | publicKeyToString func(k *lk.PublicKey) string 41 | fromString func(string) (*lk.PrivateKey, error) 42 | fromPubStr func(string) (*lk.PublicKey, error) 43 | randomStr func(int) string 44 | }{ 45 | { 46 | name: "b64", 47 | privateKeyToString: func(k *lk.PrivateKey) (string, error) { 48 | return k.ToB64String() 49 | }, 50 | publicKeyToString: func(k *lk.PublicKey) string { 51 | return k.ToB64String() 52 | }, 53 | fromString: lk.PrivateKeyFromB64String, 54 | fromPubStr: lk.PublicKeyFromB64String, 55 | randomStr: s.RandomB64String, 56 | }, 57 | { 58 | name: "b32", 59 | privateKeyToString: func(k *lk.PrivateKey) (string, error) { 60 | return k.ToB32String() 61 | }, 62 | publicKeyToString: func(k *lk.PublicKey) string { 63 | return k.ToB32String() 64 | }, 65 | fromString: lk.PrivateKeyFromB32String, 66 | fromPubStr: lk.PublicKeyFromB32String, 67 | randomStr: s.RandomB32String, 68 | }, 69 | { 70 | name: "hex", 71 | privateKeyToString: func(k *lk.PrivateKey) (string, error) { 72 | return k.ToHexString() 73 | }, 74 | publicKeyToString: func(k *lk.PublicKey) string { 75 | return k.ToHexString() 76 | }, 77 | fromString: lk.PrivateKeyFromHexString, 78 | fromPubStr: lk.PublicKeyFromHexString, 79 | randomStr: s.RandomHexString, 80 | }, 81 | } 82 | 83 | for _, tc := range tc { 84 | s.Run("should test private key "+tc.name, func() { 85 | b, err := tc.privateKeyToString(k) 86 | s.Require().NoError(err) 87 | k1, err := tc.fromString(b) 88 | s.Require().NoError(err) 89 | s.Require().Equal(k1, k) 90 | 91 | invalidStr := tc.randomStr(42) 92 | k2, err := tc.fromString(invalidStr) 93 | s.Require().Error(err) 94 | s.Require().Nil(k2) 95 | }) 96 | 97 | s.Run("should test pubic key "+tc.name, func() { 98 | b := tc.publicKeyToString(k.GetPublicKey()) 99 | k1, err := tc.fromPubStr(b) 100 | s.Require().NoError(err) 101 | s.Require().Equal(k1, k.GetPublicKey()) 102 | 103 | invalidStr := s.RandomHexString(42) 104 | k2, err := tc.fromPubStr(invalidStr) 105 | s.Require().Error(err) 106 | s.Require().Nil(k2) 107 | }) 108 | } 109 | 110 | } 111 | -------------------------------------------------------------------------------- /lkgen/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "os" 8 | 9 | "github.com/hyperboloide/lk" 10 | "gopkg.in/alecthomas/kingpin.v2" 11 | ) 12 | 13 | var ( 14 | app = kingpin.New("lkgen", "A command-line utility to generate private keys and licenses.") 15 | 16 | // Gen a private key. 17 | gen = app.Command("gen", "Generates a base32 encoded private key.") 18 | genOut = gen.Flag("output", "Output file (if not defined then stdout).").Short('o').String() 19 | 20 | // Pub returns the public key. 21 | pub = app.Command("pub", "Get the public key.") 22 | pubKey = pub.Arg("key", "Path to private key to use.").Required().String() 23 | pubOut = pub.Flag("output", "Output file (if not defined then stdout).").Short('o').String() 24 | 25 | // Sign a new license 26 | sign = app.Command("sign", "Creates a license.") 27 | signKey = sign.Arg("key", "Path to private key to use.").Required().String() 28 | signIn = sign.Flag("input", "Input data file (if not defined then stdin).").Short('i').String() 29 | signOut = sign.Flag("output", "Output file (if not defined then stdout).").Short('o').String() 30 | 31 | // Verfify a license 32 | verify = app.Command("verify", "Verifies a license.") 33 | verifyPubKey = verify.Arg("key", "Path to the public key to use.").Required().String() 34 | verifyIn = verify.Flag("input", "Input license file (if not defined then stdin).").Short('i').String() 35 | ) 36 | 37 | func main() { 38 | switch kingpin.MustParse(app.Parse(os.Args[1:])) { 39 | 40 | //Generate a private key 41 | case gen.FullCommand(): 42 | genKey() 43 | 44 | case pub.FullCommand(): 45 | publicKey() 46 | 47 | // Sign a license 48 | case sign.FullCommand(): 49 | signLicense() 50 | 51 | case verify.FullCommand(): 52 | verifyLicense() 53 | } 54 | } 55 | 56 | func publicKey() { 57 | b, err := os.ReadFile(*pubKey) 58 | if err != nil { 59 | log.Fatal(err) 60 | } 61 | 62 | pk, err := lk.PrivateKeyFromB32String(string(b[:])) 63 | if err != nil { 64 | log.Fatal(err) 65 | } 66 | 67 | key := pk.GetPublicKey() 68 | str := key.ToB32String() 69 | 70 | if *pubOut != "" { 71 | if err := os.WriteFile(*pubOut, []byte(str), 0600); err != nil { 72 | log.Fatal(err) 73 | } 74 | } else { 75 | if _, err := os.Stdout.WriteString(str); err != nil { 76 | log.Fatal(err) 77 | } 78 | } 79 | } 80 | 81 | func signLicense() { 82 | b, err := os.ReadFile(*signKey) 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | pk, err := lk.PrivateKeyFromB32String(string(b[:])) 87 | if err != nil { 88 | log.Fatal(err) 89 | } 90 | 91 | var data []byte 92 | if *signIn != "" { 93 | data, err = os.ReadFile(*signIn) 94 | } else { 95 | data, err = io.ReadAll(os.Stdin) 96 | } 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | 101 | l, err := lk.NewLicense(pk, data) 102 | if err != nil { 103 | log.Fatal(err) 104 | } 105 | 106 | str, err := l.ToB32String() 107 | if err != nil { 108 | log.Fatal(err) 109 | } 110 | 111 | if *signOut != "" { 112 | if err := os.WriteFile(*signOut, []byte(str), 0600); err != nil { 113 | log.Fatal(err) 114 | } 115 | } else { 116 | if _, err := os.Stdout.WriteString(str); err != nil { 117 | log.Fatal(err) 118 | } 119 | } 120 | } 121 | 122 | func genKey() { 123 | key, err := lk.NewPrivateKey() 124 | if err != nil { 125 | log.Fatal(err) 126 | } 127 | str, err := key.ToB32String() 128 | if err != nil { 129 | log.Fatal(err) 130 | } 131 | 132 | if *genOut != "" { 133 | if err := os.WriteFile(*genOut, []byte(str), 0600); err != nil { 134 | log.Fatal(err) 135 | } 136 | } else { 137 | if _, err := os.Stdout.WriteString(str); err != nil { 138 | log.Fatal(err) 139 | } 140 | } 141 | } 142 | 143 | func verifyLicense() { 144 | b, err := os.ReadFile(*verifyPubKey) 145 | if err != nil { 146 | log.Print(*verifyPubKey) 147 | log.Fatal(err) 148 | } 149 | 150 | publicKey, err := lk.PublicKeyFromB32String(string(b)) 151 | if err != nil { 152 | log.Fatal(err) 153 | } 154 | 155 | if *verifyIn != "" { 156 | b, err = os.ReadFile(*verifyIn) 157 | } else { 158 | b, err = io.ReadAll(os.Stdin) 159 | } 160 | if err != nil { 161 | log.Fatal(err) 162 | } 163 | 164 | license, err := lk.LicenseFromB32String(string(b)) 165 | if err != nil { 166 | log.Fatal(err) 167 | } 168 | 169 | if ok, err := license.Verify(publicKey); err != nil { 170 | log.Fatal(err) 171 | } else if !ok { 172 | log.Fatal("Invalid license signature") 173 | } 174 | fmt.Print(string(license.Data)) 175 | } 176 | -------------------------------------------------------------------------------- /keys.go: -------------------------------------------------------------------------------- 1 | package lk 2 | 3 | import ( 4 | "crypto/ecdsa" 5 | "crypto/elliptic" 6 | "crypto/rand" 7 | "encoding/base32" 8 | "encoding/base64" 9 | "encoding/hex" 10 | "math/big" 11 | ) 12 | 13 | // PrivateKey is the master key to create the licenses. Keep it in a secure 14 | // location. 15 | type PrivateKey ecdsa.PrivateKey 16 | 17 | type pkContainer struct { 18 | Pub []byte 19 | D *big.Int 20 | } 21 | 22 | // Curve is the elliptic.Curve to use. Default is elliptic.P384. 23 | var Curve = elliptic.P384 24 | 25 | // NewPrivateKey generates a new private key. The default elliptic.Curve used 26 | // is elliptic.P384(). 27 | func NewPrivateKey() (*PrivateKey, error) { 28 | tmp, err := ecdsa.GenerateKey(Curve(), rand.Reader) 29 | if err != nil { 30 | return nil, err 31 | } 32 | key := PrivateKey(*tmp) 33 | return &key, nil 34 | } 35 | 36 | func (k *PrivateKey) toEcdsa() *ecdsa.PrivateKey { 37 | r := ecdsa.PrivateKey(*k) 38 | return &r 39 | } 40 | 41 | // ToBytes transforms the private key to a []byte. 42 | func (k PrivateKey) ToBytes() ([]byte, error) { 43 | ek := k.toEcdsa() 44 | pubBytes, err := ek.PublicKey.Bytes() 45 | if err != nil { 46 | return nil, err 47 | } 48 | c := &pkContainer{ 49 | Pub: pubBytes, 50 | D: ek.D, 51 | } 52 | 53 | return toBytes(c) 54 | } 55 | 56 | // ToB64String transforms the private key to a base64 string. 57 | func (k PrivateKey) ToB64String() (string, error) { 58 | b, err := k.ToBytes() 59 | if err != nil { 60 | return "", err 61 | } 62 | return base64.StdEncoding.EncodeToString(b), nil 63 | } 64 | 65 | // ToB32String transforms the private key to a base32 string. 66 | func (k PrivateKey) ToB32String() (string, error) { 67 | b, err := k.ToBytes() 68 | if err != nil { 69 | return "", err 70 | } 71 | return base32.StdEncoding.EncodeToString(b), nil 72 | } 73 | 74 | // ToHexString transforms the private key to a hexadecimal string 75 | func (k PrivateKey) ToHexString() (string, error) { 76 | b, err := k.ToBytes() 77 | if err != nil { 78 | return "", err 79 | } 80 | return hex.EncodeToString(b), nil 81 | } 82 | 83 | // PrivateKeyFromBytes returns a private key from a []byte. 84 | func PrivateKeyFromBytes(b []byte) (*PrivateKey, error) { 85 | c := &pkContainer{} 86 | if err := fromBytes(c, b); err != nil { 87 | return nil, err 88 | } 89 | pk, err := PublicKeyFromBytes(c.Pub) 90 | if err != nil { 91 | return nil, err 92 | } 93 | k := ecdsa.PrivateKey{ 94 | PublicKey: ecdsa.PublicKey(*pk), 95 | D: c.D, 96 | } 97 | res := PrivateKey(k) 98 | return &res, nil 99 | } 100 | 101 | // PrivateKeyFromB64String returns a private key from a base64 encoded 102 | // string. 103 | func PrivateKeyFromB64String(str string) (*PrivateKey, error) { 104 | b, err := base64.StdEncoding.DecodeString(str) 105 | if err != nil { 106 | return nil, err 107 | } 108 | return PrivateKeyFromBytes(b) 109 | } 110 | 111 | // PrivateKeyFromB32String returns a private key from a base32 encoded 112 | // string. 113 | func PrivateKeyFromB32String(str string) (*PrivateKey, error) { 114 | b, err := base32.StdEncoding.DecodeString(str) 115 | if err != nil { 116 | return nil, err 117 | } 118 | return PrivateKeyFromBytes(b) 119 | } 120 | 121 | // PrivateKeyFromHexString returns a private key from a hexadecimal encoded 122 | // string. 123 | func PrivateKeyFromHexString(str string) (*PrivateKey, error) { 124 | b, err := hex.DecodeString(str) 125 | if err != nil { 126 | return nil, err 127 | } 128 | return PrivateKeyFromBytes(b) 129 | } 130 | 131 | // GetPublicKey returns the PublicKey associated with the private key. 132 | func (k PrivateKey) GetPublicKey() *PublicKey { 133 | pk := PublicKey(k.PublicKey) 134 | return &pk 135 | } 136 | 137 | // PublicKey is used to check the validity of the licenses. You can share it 138 | // freely. 139 | type PublicKey ecdsa.PublicKey 140 | 141 | func (k *PublicKey) toEcdsa() *ecdsa.PublicKey { 142 | r := ecdsa.PublicKey(*k) 143 | return &r 144 | } 145 | 146 | // ToBytes transforms the public key to a []byte. 147 | func (k PublicKey) ToBytes() []byte { 148 | pkBytes, err := k.toEcdsa().Bytes() 149 | if err != nil { 150 | panic("lk: could not marshal public key: " + err.Error()) 151 | } 152 | 153 | return pkBytes 154 | } 155 | 156 | // ToB64String transforms the public key to a base64 string. 157 | func (k PublicKey) ToB64String() string { 158 | return base64.StdEncoding.EncodeToString( 159 | k.ToBytes(), 160 | ) 161 | } 162 | 163 | // ToB32String transforms the public key to a base32 string. 164 | func (k PublicKey) ToB32String() string { 165 | return base32.StdEncoding.EncodeToString( 166 | k.ToBytes(), 167 | ) 168 | } 169 | 170 | // ToHexString transforms the public key to a hexadecimal string. 171 | func (k PublicKey) ToHexString() string { 172 | return hex.EncodeToString( 173 | k.ToBytes(), 174 | ) 175 | } 176 | 177 | // PublicKeyFromBytes returns a public key from a []byte. 178 | func PublicKeyFromBytes(b []byte) (*PublicKey, error) { 179 | k, err := ecdsa.ParseUncompressedPublicKey(Curve(), b) 180 | if err != nil { 181 | return nil, err 182 | } 183 | r := PublicKey(*k) 184 | return &r, nil 185 | } 186 | 187 | // PublicKeyFromB64String returns a public key from a base64 encoded 188 | // string. 189 | func PublicKeyFromB64String(str string) (*PublicKey, error) { 190 | b, err := base64.StdEncoding.DecodeString(str) 191 | if err != nil { 192 | return nil, err 193 | } 194 | 195 | return PublicKeyFromBytes(b) 196 | } 197 | 198 | // PublicKeyFromB32String returns a public key from a base32 encoded 199 | // string. 200 | func PublicKeyFromB32String(str string) (*PublicKey, error) { 201 | b, err := base32.StdEncoding.DecodeString(str) 202 | if err != nil { 203 | return nil, err 204 | } 205 | 206 | return PublicKeyFromBytes(b) 207 | } 208 | 209 | // PublicKeyFromHexString returns a public key from a hexadecimal encoded 210 | // string. 211 | func PublicKeyFromHexString(str string) (*PublicKey, error) { 212 | b, err := hex.DecodeString(str) 213 | if err != nil { 214 | return nil, err 215 | } 216 | 217 | return PublicKeyFromBytes(b) 218 | } 219 | -------------------------------------------------------------------------------- /examples_test.go: -------------------------------------------------------------------------------- 1 | package lk_test 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "time" 8 | 9 | "github.com/hyperboloide/lk" 10 | ) 11 | 12 | type MyLicence struct { 13 | Email string `json:"email"` 14 | End time.Time `json:"end"` 15 | } 16 | 17 | // Example_complete creates a new license and validate it. 18 | func Example_complete() { 19 | // create a new Private key: 20 | privateKey, err := lk.NewPrivateKey() 21 | if err != nil { 22 | log.Fatal("private key generation failed: " + err.Error()) 23 | 24 | } 25 | 26 | // create a license document: 27 | doc := MyLicence{ 28 | "test@example.com", 29 | time.Now().Add(time.Hour * 24 * 365), // 1 year 30 | } 31 | 32 | // marshall the document to json bytes: 33 | docBytes, err := json.Marshal(doc) 34 | if err != nil { 35 | log.Fatal(err) 36 | 37 | } 38 | 39 | // generate your license with the private key and the document: 40 | license, err := lk.NewLicense(privateKey, docBytes) 41 | if err != nil { 42 | log.Fatal("license generation failed: " + err.Error()) 43 | 44 | } 45 | 46 | // encode the new license to b64, this is what you give to your customer. 47 | str64, err := license.ToB64String() 48 | if err != nil { 49 | log.Fatal(err) 50 | } 51 | fmt.Println(str64) 52 | 53 | // get the public key. The public key should be hardcoded in your app 54 | // to check licences. Do not distribute the private key! 55 | publicKey := privateKey.GetPublicKey() 56 | 57 | // validate the license: 58 | if ok, err := license.Verify(publicKey); err != nil { 59 | log.Fatal("license verification failed: " + err.Error()) 60 | } else if !ok { 61 | log.Fatal("Invalid license signature") 62 | } 63 | 64 | // unmarshal the document and check the end date: 65 | res := MyLicence{} 66 | if err := json.Unmarshal(license.Data, &res); err != nil { 67 | log.Fatal(err) 68 | } else if res.End.Before(time.Now()) { 69 | log.Fatalf("License expired on: %s", res.End.String()) 70 | } else { 71 | fmt.Printf(`Licensed to %s until %s \n`, res.Email, res.End.Format("2006-01-02")) 72 | } 73 | } 74 | 75 | // Example_licenseGeneration shows how to create a license file from a private 76 | // key. 77 | func Example_licenseGeneration() { 78 | 79 | // a base32 encoded private key generated by `lkgen gen` 80 | // note that you might prefer reading it from a file... 81 | const privateKeyBase32 = "FD7YCAYBAEFXA22DN5XHIYLJNZSXEAP7QIAACAQBANIHKYQBBIAACAKEAH7YIAAAAAFP7AYFAEBP7BQAAAAP7GP7QIAWCBCRKQVWKPT7UJDNP4LB5TXEQMO7EYEGDCE42KVBDNEGRIYIIJFBIWIVB6T6ZTKLSYSGK54DZ5VX6M5SJHBYZU2JXUFXJI25L2JJKJW4RL7UL2XBDT4GKYZ5IS6IWBCN7CWTMVBCBHJMH3RHZ5BVGVAY66MQAEYQEPSS2ANTYZIWXWSGIUJW3MDOO335JK3D4N3IV4L5UTAQMLS5YC7QASCAAUOHTZ5ZCCCYIBNCWBELBMAA====" 82 | 83 | // Here we use a struct that is marshalled to json, 84 | // but ultimatly all you need is a []byte. 85 | doc := struct { 86 | Email string `json:"email"` 87 | End time.Time `json:"end"` 88 | }{ 89 | "test@example.com", 90 | time.Now().Add(time.Hour * 24 * 365), // 1 year 91 | } 92 | 93 | // marshall the document to []bytes (this is the data that our license 94 | // will contain): 95 | docBytes, err := json.Marshal(doc) 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | 100 | // Unmarshal the private key: 101 | privateKey, err := lk.PrivateKeyFromB32String(privateKeyBase32) 102 | if err != nil { 103 | log.Fatal("private key unmarshal failed: " + err.Error()) 104 | } 105 | 106 | // generate your license with the private key and the document: 107 | license, err := lk.NewLicense(privateKey, docBytes) 108 | if err != nil { 109 | log.Fatal("license generation failed: " + err.Error()) 110 | } 111 | 112 | // the b32 representation of our license, this is what you give to 113 | // your customer. 114 | licenseB32, err := license.ToB32String() 115 | if err != nil { 116 | log.Fatal("license encoding failed: " + err.Error()) 117 | 118 | } 119 | 120 | // print the license that you should give to your customer 121 | fmt.Println(licenseB32) 122 | } 123 | 124 | // Example_licenseVerification validates a previously generated license with 125 | // a public key. 126 | func Example_licenseVerification() { 127 | 128 | // A previously generated licence b32 encoded. In real life you should read 129 | // it from a file (or prompt the user) at the beginning of your program and check it 130 | // before doing anything else... 131 | const licenseB32 = "FT7YOAYBAEDUY2LDMVXHGZIB76EAAAIDAECEIYLUMEAQUAABAFJAD74EAAAQCUYB76CAAAAABL7YGBIBAL7YMAAAAD73H74IAFEHWITFNVQWS3BCHIRHIZLTORAGK6DBNVYGYZJOMNXW2IRMEJSW4ZBCHIRDEMBRHAWTCMBNGI3FIMJSHIYTSORTGMXDOMBZG43TIMJYHAVTAMR2GAYCE7IBGEBAPXB37ROJCUOYBVG4LAL3MSNKJKPGIKNT564PYK5X542NH62V7TAUEYHGLEOPZHRBAPH7M4SC55OHAEYQEXMKGG3JPO6BSHTDF3T5H6T42VUD7YAJ3TY5AP5MDE5QW4ZYWMSAPEK24HZOUXQ3LJ5YY34XYPVXBUAA====" 132 | 133 | // the public key b32 encoded from the private key using: 134 | // `lkgen pub my_private_key_file`. It should be 135 | // hardcoded somewhere in your app. 136 | const publicKeyBase32 = "ARIVIK3FHZ72ERWX6FQ6Z3SIGHPSMCDBRCONFKQRWSDIUMEEESQULEKQ7J7MZVFZMJDFO6B46237GOZETQ4M2NE32C3UUNOV5EUVE3OIV72F5LQRZ6DFMM6UJPELARG7RLJWKQRATUWD5YT46Q2TKQMPPGIA====" 137 | 138 | // Unmarshal the public key 139 | publicKey, err := lk.PublicKeyFromB32String(publicKeyBase32) 140 | if err != nil { 141 | log.Fatal("public key unmarshal failed: " + err.Error()) 142 | } 143 | 144 | // Unmarshal the customer license: 145 | license, err := lk.LicenseFromB32String(licenseB32) 146 | if err != nil { 147 | log.Fatal("license unmarshal failed: " + err.Error()) 148 | } 149 | 150 | // validate the license signature: 151 | if ok, err := license.Verify(publicKey); err != nil { 152 | log.Fatal("license verification failed: " + err.Error()) 153 | } else if !ok { 154 | log.Fatal("Invalid license signature") 155 | } 156 | 157 | result := struct { 158 | Email string `json:"email"` 159 | End time.Time `json:"end"` 160 | }{} 161 | 162 | // unmarshal the document: 163 | if err := json.Unmarshal(license.Data, &result); err != nil { 164 | log.Fatal(err) 165 | } 166 | 167 | // Now you just have to check the end date and if it before time.Now(), 168 | // then you can continue! 169 | // if result.End.Before(time.Now()) { 170 | // log.Fatal("License expired on: %s", result.End.Format("2006-01-02")) 171 | // } else { 172 | // fmt.Printf(`Licensed to %s until %s`, result.Email, result.End.Format("2006-01-02")) 173 | // } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # license-key 2 | 3 | [![Tests](https://github.com/hyperboloide/lk/actions/workflows/tests.yml/badge.svg)](https://github.com/hyperboloide/lk/actions/workflows/tests.yml) 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/hyperboloide/lk.svg)](https://pkg.go.dev/github.com/hyperboloide/lk) 5 | [![Go Report Card](https://goreportcard.com/badge/github.com/hyperboloide/lk)](https://goreportcard.com/report/github.com/hyperboloide/lk) 6 | 7 | A simple licensing library in Golang, that generates license files containing 8 | arbitrary data (ex: user email, end date...) that you can further validate if you want. 9 | 10 | The license files are signed with the: Elliptic Curve Digital Signature Algorithm 11 | (using [elliptic.P384 ](https://pkg.go.dev/crypto/elliptic#P384)) 12 | 13 | The license file can be marshalled in an easy to distribute format (ex: base32 encoded strings) 14 | 15 | Note that this implementation is quite basic and that in no way it could 16 | prevent someone to hack your software. The goal of this project is only 17 | to provide a convenient way for software publishers to generate license keys 18 | and distribute them without too much hassle for the user. 19 | 20 | ### How does it works? 21 | 22 | 1. Generate a private key (and keep it secure). 23 | 2. Transform the data you want to provide (end date, user email...) to a byte array (using json or gob for example). 24 | 3. The library takes the data and create a cryptographically signed hash that is appended to the data. 25 | 4. Convert the result to a Base64/Base32/Hex string and send it to the end user: this is the license. 26 | 5. when the user starts your program load the license and verify the signature using a public key. 27 | 6. validate the data in your license key (ex: the end date) 28 | 29 | 30 | ### lkgen 31 | 32 | A command line helper [lkgen](lkgen) is also provided to generate private keys and create licenses. 33 | 34 | This is also a good example of how to use the library. 35 | 36 | ### Examples 37 | 38 | #### Generating a new license: 39 | 40 | Below is an example of code that generates a license from a private key and a struct containing the end date and a user email that is marshalled to json. 41 | 42 | ```go 43 | // first, you need a base32 encoded private key generated by `lkgen gen` note that you might 44 | // prefer reading it from a file, and that it should stay secret (ie: dont distribute it with your app)! 45 | const privateKeyBase32 = "FD7YCAYBAEFXA22DN5XHIYLJNZSXEAP7QIAACAQBANIHKYQBBIAACAKEAH7YIAAAAAFP7AYFAEBP7BQAAAAP7GP7QIAWCBCRKQVWKPT7UJDNP4LB5TXEQMO7EYEGDCE42KVBDNEGRIYIIJFBIWIVB6T6ZTKLSYSGK54DZ5VX6M5SJHBYZU2JXUFXJI25L2JJKJW4RL7UL2XBDT4GKYZ5IS6IWBCN7CWTMVBCBHJMH3RHZ5BVGVAY66MQAEYQEPSS2ANTYZIWXWSGIUJW3MDOO335JK3D4N3IV4L5UTAQMLS5YC7QASCAAUOHTZ5ZCCCYIBNCWBELBMAA====" 46 | 47 | // Unmarshal the private key 48 | privateKey, err := lk.PrivateKeyFromB32String(privateKeyBase32) 49 | if err != nil { 50 | log.Fatal(err) 51 | } 52 | 53 | // Define the data you need in your license, 54 | // here we use a struct that is marshalled to json, but ultimately all you need is a []byte. 55 | doc := struct { 56 | Email string `json:"email"` 57 | End time.Time `json:"end"` 58 | }{ 59 | "user@example.com", 60 | time.Now().Add(time.Hour * 24 * 365), // 1 year 61 | } 62 | 63 | // marshall the document to []bytes (this is the data that our license will contain). 64 | docBytes, err := json.Marshal(doc) 65 | if err != nil { 66 | log.Fatal(err) 67 | } 68 | 69 | // generate your license with the private key and the document 70 | license, err := lk.NewLicense(privateKey, docBytes) 71 | if err != nil { 72 | log.Fatal(err) 73 | 74 | } 75 | // the b32 representation of our license, this is what you give to your customer. 76 | licenseB32, err := license.ToB32String() 77 | if err != nil { 78 | log.Fatal(err) 79 | } 80 | fmt.Println(licenseB32) 81 | ``` 82 | 83 | #### Validating a license: 84 | 85 | Before your execute your program you want to check the user license, here is how with key generated from the previous example: 86 | 87 | ```go 88 | // A previously generated license b32 encoded. In real life you should read it from a file... 89 | const licenseB32 = "FT7YOAYBAEDUY2LDMVXHGZIB76EAAAIDAECEIYLUMEAQUAABAFJAD74EAAAQCUYB76CAAAAABL7YGBIBAL7YMAAAAD73H74IAFEHWITFNVQWS3BCHIRHIZLTORAGK6DBNVYGYZJOMNXW2IRMEJSW4ZBCHIRDEMBRHAWTCMBNGI3FIMJSHIYTSORTGMXDOMBZG43TIMJYHAVTAMR2GAYCE7IBGEBAPXB37ROJCUOYBVG4LAL3MSNKJKPGIKNT564PYK5X542NH62V7TAUEYHGLEOPZHRBAPH7M4SC55OHAEYQEXMKGG3JPO6BSHTDF3T5H6T42VUD7YAJ3TY5AP5MDE5QW4ZYWMSAPEK24HZOUXQ3LJ5YY34XYPVXBUAA====" 90 | 91 | // the public key b32 encoded from the private key using: lkgen pub my_private_key_file`. 92 | // It should be hardcoded somewhere in your app. 93 | const publicKeyBase32 = "ARIVIK3FHZ72ERWX6FQ6Z3SIGHPSMCDBRCONFKQRWSDIUMEEESQULEKQ7J7MZVFZMJDFO6B46237GOZETQ4M2NE32C3UUNOV5EUVE3OIV72F5LQRZ6DFMM6UJPELARG7RLJWKQRATUWD5YT46Q2TKQMPPGIA====" 94 | 95 | // Unmarshal the public key. 96 | publicKey, err := lk.PublicKeyFromB32String(publicKeyBase32) 97 | if err != nil { 98 | log.Fatal(err) 99 | } 100 | 101 | // Unmarshal the customer license. 102 | license, err := lk.LicenseFromB32String(licenseB32) 103 | if err != nil { 104 | log.Fatal(err) 105 | } 106 | 107 | // validate the license signature. 108 | if ok, err := license.Verify(publicKey); err != nil { 109 | log.Fatal(err) 110 | } else if !ok { 111 | log.Fatal("Invalid license signature") 112 | } 113 | 114 | result := struct { 115 | Email string `json:"email"` 116 | End time.Time `json:"end"` 117 | }{} 118 | 119 | // unmarshal the document. 120 | if err := json.Unmarshal(license.Data, &result); err != nil { 121 | log.Fatal(err) 122 | } 123 | 124 | // Now you just have to check that the end date is after time.Now() then you can continue! 125 | if result.End.Before(time.Now()) { 126 | log.Fatalf("License expired on: %s", result.End.Format("2006-01-02")) 127 | } else { 128 | fmt.Printf(`Licensed to %s until %s`, result.Email, result.End.Format("2006-01-02")) 129 | } 130 | ``` 131 | 132 | 133 | #### A Complete example 134 | 135 | Bellow is a sample function that generate a key pair, signs a license and verify it. 136 | 137 | ```go 138 | // create a new Private key: 139 | privateKey, err := lk.NewPrivateKey() 140 | if err != nil { 141 | log.Fatal(err) 142 | 143 | } 144 | 145 | // create a license document: 146 | doc := MyLicence{ 147 | "test@example.com", 148 | time.Now().Add(time.Hour * 24 * 365), // 1 year 149 | } 150 | 151 | // marshall the document to json bytes: 152 | docBytes, err := json.Marshal(doc) 153 | if err != nil { 154 | log.Fatal(err) 155 | 156 | } 157 | 158 | // generate your license with the private key and the document: 159 | license, err := lk.NewLicense(privateKey, docBytes) 160 | if err != nil { 161 | log.Fatal(err) 162 | 163 | } 164 | 165 | // encode the new license to b64, this is what you give to your customer. 166 | str64, err := license.ToB64String() 167 | if err != nil { 168 | log.Fatal(err) 169 | 170 | } 171 | fmt.Println(str64) 172 | 173 | // get the public key. The public key should be hardcoded in your app to check licences. 174 | // Do not distribute the private key! 175 | publicKey := privateKey.GetPublicKey() 176 | 177 | // validate the license: 178 | if ok, err := license.Verify(publicKey); err != nil { 179 | log.Fatal(err) 180 | } else if !ok { 181 | log.Fatal("Invalid license signature") 182 | } 183 | 184 | // unmarshal the document and check the end date: 185 | res := MyLicence{} 186 | if err := json.Unmarshal(license.Data, &res); err != nil { 187 | log.Fatal(err) 188 | } else if res.End.Before(time.Now()) { 189 | log.Fatalf("License expired on: %s", res.End.String()) 190 | } else { 191 | fmt.Printf(`Licensed to %s until %s \n`, res.Email, res.End.Format("2006-01-02")) 192 | } 193 | ``` 194 | --------------------------------------------------------------------------------