License Key Registration with ECDSA JWT Tokens
* New command-line tool: doodle-admin for signing license keys for users. Includes functions to initialize a keypair, sign license keys and validate existing keys. * The Main Menu screen shows a blue "Register Game" button in the bottom right corner of the screen, for unregistered users only. * In Edit Mode, there is a "Help -> Register" menu item that opens the License Window. * The License UI Window lets the user select the license.key file to register the game with. If registered, a copy of the key is placed in Doodle's profile directory and the licensee name/email is shown in the License UI window. * Unregistered games will show the word "(shareware)" next to the title screen version number and Edit Mode status bar. * No restrictions are yet placed on free versions of the game.
This commit is contained in:
parent
d6f86487f5
commit
0449737607
|
@ -9,8 +9,8 @@ import (
|
|||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/cmd/doodad/commands"
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||
"git.kirsle.net/apps/doodle/pkg/license"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
|
@ -32,7 +32,7 @@ func main() {
|
|||
app.Usage = "command line interface for Doodle"
|
||||
|
||||
var freeLabel string
|
||||
if balance.FreeVersion {
|
||||
if !license.IsRegistered() {
|
||||
freeLabel = " (shareware)"
|
||||
}
|
||||
|
||||
|
|
44
cmd/doodle-admin/command/key.go
Normal file
44
cmd/doodle-admin/command/key.go
Normal file
|
@ -0,0 +1,44 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/doodle/pkg/license"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Key a license key for Sketchy Maze.
|
||||
var Key *cli.Command
|
||||
|
||||
func init() {
|
||||
Key = &cli.Command{
|
||||
Name: "key",
|
||||
Usage: "generate an admin ECDSA signing key",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "public",
|
||||
Usage: "Filename to write the public key to (.pem)",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "private",
|
||||
Usage: "Filename to write the private key to (.pem)",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
key, err := license.AdminGenerateKeys()
|
||||
if err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
err = license.AdminWriteKeys(key, c.String("private"), c.String("public"))
|
||||
if err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
log.Info("Written private key: %s", c.String("private"))
|
||||
log.Info("Written public key: %s", c.String("public"))
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
73
cmd/doodle-admin/command/sign.go
Normal file
73
cmd/doodle-admin/command/sign.go
Normal file
|
@ -0,0 +1,73 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/license"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Sign a license key for Sketchy Maze.
|
||||
var Sign *cli.Command
|
||||
|
||||
func init() {
|
||||
Sign = &cli.Command{
|
||||
Name: "sign",
|
||||
Usage: "sign a license key for the paid version of Sketchy Maze.",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "key",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "Private key .pem file for signing",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "name",
|
||||
Aliases: []string{"n"},
|
||||
Usage: "User name for certificate",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "email",
|
||||
Aliases: []string{"e"},
|
||||
Usage: "User email address",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "output",
|
||||
Aliases: []string{"o"},
|
||||
Usage: "Output file, default outputs to console",
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
key, err := license.AdminLoadPrivateKey(c.String("key"))
|
||||
if err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
reg := license.Registration{
|
||||
Name: c.String("name"),
|
||||
Email: c.String("email"),
|
||||
}
|
||||
|
||||
result, err := license.AdminSignRegistration(key, reg)
|
||||
if err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
// Writing to an output file?
|
||||
if output := c.String("output"); output != "" {
|
||||
log.Info("Write to: %s", output)
|
||||
if err := ioutil.WriteFile(output, []byte(result), 0644); err != nil {
|
||||
return cli.Exit(err, 1)
|
||||
}
|
||||
} else {
|
||||
fmt.Println(result)
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
59
cmd/doodle-admin/command/verify.go
Normal file
59
cmd/doodle-admin/command/verify.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/license"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Verify a license key for Sketchy Maze.
|
||||
var Verify *cli.Command
|
||||
|
||||
func init() {
|
||||
Verify = &cli.Command{
|
||||
Name: "verify",
|
||||
Usage: "check the signature on a license key",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{
|
||||
Name: "key",
|
||||
Aliases: []string{"k"},
|
||||
Usage: "Public key .pem file that signed the JWT",
|
||||
Required: true,
|
||||
},
|
||||
&cli.StringFlag{
|
||||
Name: "filename",
|
||||
Aliases: []string{"f"},
|
||||
Usage: "File name of the license file to validate",
|
||||
Required: true,
|
||||
},
|
||||
},
|
||||
Action: func(c *cli.Context) error {
|
||||
key, err := license.AdminLoadPublicKey(c.String("key"))
|
||||
if err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
jwt, err := ioutil.ReadFile(c.String("filename"))
|
||||
if err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
reg, err := license.Validate(key, string(jwt))
|
||||
if err != nil {
|
||||
return cli.Exit(err.Error(), 1)
|
||||
}
|
||||
|
||||
log.Info("Registration valid")
|
||||
log.Info(" Name: %s", reg.Name)
|
||||
log.Info(" Email: %s", reg.Email)
|
||||
log.Info(" Issued: %s", time.Unix(reg.IssuedAt, 0))
|
||||
log.Info(" NBF: %s", time.Unix(reg.NotBefore, 0))
|
||||
log.Info("Raw:\n%+v", reg)
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
}
|
59
cmd/doodle-admin/main.go
Normal file
59
cmd/doodle-admin/main.go
Normal file
|
@ -0,0 +1,59 @@
|
|||
// doodle-admin performs secret admin tasks like generating license keys.
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/cmd/doodle-admin/command"
|
||||
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||
"github.com/urfave/cli/v2"
|
||||
)
|
||||
|
||||
// Build variables.
|
||||
var (
|
||||
Build = "N/A"
|
||||
BuildDate string
|
||||
)
|
||||
|
||||
func init() {
|
||||
if BuildDate == "" {
|
||||
BuildDate = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "doodle-admin"
|
||||
app.Usage = "Admin tasks for Sketchy Maze."
|
||||
|
||||
app.Version = fmt.Sprintf("%s build %s. Built on %s",
|
||||
branding.Version,
|
||||
Build,
|
||||
BuildDate,
|
||||
)
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
&cli.BoolFlag{
|
||||
Name: "debug, d",
|
||||
Usage: "enable debug level logging",
|
||||
},
|
||||
}
|
||||
|
||||
app.Commands = []*cli.Command{
|
||||
command.Key,
|
||||
command.Sign,
|
||||
command.Verify,
|
||||
}
|
||||
|
||||
sort.Sort(cli.FlagsByName(app.Flags))
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
|
@ -15,6 +15,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/bindata"
|
||||
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||
"git.kirsle.net/apps/doodle/pkg/license"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||
"git.kirsle.net/apps/doodle/pkg/sound"
|
||||
|
@ -51,7 +52,7 @@ func main() {
|
|||
app.Usage = fmt.Sprintf("%s - %s", branding.AppName, branding.Summary)
|
||||
|
||||
var freeLabel string
|
||||
if balance.FreeVersion {
|
||||
if !license.IsRegistered() {
|
||||
freeLabel = " (shareware)"
|
||||
}
|
||||
|
||||
|
|
|
@ -127,7 +127,9 @@ func init() {
|
|||
ButtonDanger.HoverForeground = ButtonPrimary.Foreground
|
||||
|
||||
ButtonBabyBlue.Background = render.RGBA(0, 153, 255, 255)
|
||||
ButtonBabyBlue.Foreground = render.White
|
||||
ButtonBabyBlue.HoverBackground = render.RGBA(0, 220, 255, 255)
|
||||
ButtonBabyBlue.HoverForeground = render.White
|
||||
|
||||
ButtonPink.Background = render.RGBA(255, 153, 255, 255)
|
||||
ButtonPink.HoverBackground = render.RGBA(255, 220, 255, 255)
|
||||
|
|
|
@ -4,7 +4,7 @@ package branding
|
|||
const (
|
||||
AppName = "Sketchy Maze"
|
||||
Summary = "A drawing-based maze game"
|
||||
Version = "0.6.1-alpha"
|
||||
Version = "0.6.1"
|
||||
Website = "https://www.sketchymaze.com"
|
||||
Copyright = "2021 Noah Petherbridge"
|
||||
|
||||
|
|
|
@ -233,11 +233,6 @@ func (d *Doodle) NewMap() {
|
|||
|
||||
// NewDoodad loads a new Doodad in Edit Mode.
|
||||
func (d *Doodle) NewDoodad(size int) {
|
||||
if balance.FreeVersion {
|
||||
d.Flash("Doodad editor is not available in your version of the game.")
|
||||
return
|
||||
}
|
||||
|
||||
log.Info("Starting a new doodad")
|
||||
scene := &EditorScene{
|
||||
DrawingType: enum.DoodadDrawing,
|
||||
|
@ -262,9 +257,6 @@ func (d *Doodle) EditDrawing(filename string) error {
|
|||
log.Info("is a Level type")
|
||||
scene.DrawingType = enum.LevelDrawing
|
||||
case ".doodad":
|
||||
if balance.FreeVersion {
|
||||
return fmt.Errorf("Doodad editor not supported in your version of the game")
|
||||
}
|
||||
scene.DrawingType = enum.DoodadDrawing
|
||||
default:
|
||||
return fmt.Errorf("file extension '%s' doesn't indicate its drawing type", ext)
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
||||
"git.kirsle.net/apps/doodle/pkg/enum"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/license"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/native"
|
||||
"git.kirsle.net/apps/doodle/pkg/uix"
|
||||
|
@ -51,6 +52,7 @@ type EditorUI struct {
|
|||
layersWindow *ui.Window
|
||||
publishWindow *ui.Window
|
||||
filesystemWindow *ui.Window
|
||||
licenseWindow *ui.Window
|
||||
|
||||
// Palette window.
|
||||
Palette *ui.Window
|
||||
|
@ -508,24 +510,22 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
|
|||
d.GotoNewMenu()
|
||||
})
|
||||
})
|
||||
if !balance.FreeVersion {
|
||||
fileMenu.AddItem("New doodad", func() {
|
||||
u.Scene.ConfirmUnload(func() {
|
||||
d.Prompt("Doodad size [100]>", func(answer string) {
|
||||
size := balance.DoodadSize
|
||||
if answer != "" {
|
||||
i, err := strconv.Atoi(answer)
|
||||
if err != nil {
|
||||
d.Flash("Error: Doodad size must be a number.")
|
||||
return
|
||||
}
|
||||
size = i
|
||||
fileMenu.AddItem("New doodad", func() {
|
||||
u.Scene.ConfirmUnload(func() {
|
||||
d.Prompt("Doodad size [100]>", func(answer string) {
|
||||
size := balance.DoodadSize
|
||||
if answer != "" {
|
||||
i, err := strconv.Atoi(answer)
|
||||
if err != nil {
|
||||
d.Flash("Error: Doodad size must be a number.")
|
||||
return
|
||||
}
|
||||
d.NewDoodad(size)
|
||||
})
|
||||
size = i
|
||||
}
|
||||
d.NewDoodad(size)
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
fileMenu.AddItemAccel("Save", "Ctrl-S*", func() {
|
||||
if u.Scene.filename != "" {
|
||||
saveFunc(u.Scene.filename)
|
||||
|
@ -688,6 +688,27 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
|
|||
helpMenu.AddItemAccel("User Manual", "F1", func() {
|
||||
native.OpenLocalURL(balance.GuidebookPath)
|
||||
})
|
||||
helpMenu.AddItem("Register", func() {
|
||||
if u.licenseWindow == nil {
|
||||
cfg := windows.License{
|
||||
Supervisor: u.Supervisor,
|
||||
Engine: d.Engine,
|
||||
OnCancel: func() {
|
||||
u.licenseWindow.Hide()
|
||||
},
|
||||
}
|
||||
cfg.OnLicensed = func() {
|
||||
// License status has changed, reload the window!
|
||||
if u.licenseWindow != nil {
|
||||
u.licenseWindow.Hide()
|
||||
}
|
||||
u.licenseWindow = windows.MakeLicenseWindow(d.width, d.height, cfg)
|
||||
}
|
||||
|
||||
cfg.OnLicensed()
|
||||
}
|
||||
u.licenseWindow.Show()
|
||||
})
|
||||
helpMenu.AddItem("About", func() {
|
||||
if u.aboutWindow == nil {
|
||||
u.aboutWindow = windows.NewAboutWindow(windows.About{
|
||||
|
@ -747,7 +768,7 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
|
|||
}
|
||||
|
||||
var shareware string
|
||||
if balance.FreeVersion {
|
||||
if !license.IsRegistered() {
|
||||
shareware = " (shareware)"
|
||||
}
|
||||
extraLabel := ui.NewLabel(ui.Label{
|
||||
|
|
100
pkg/license/admin.go
Normal file
100
pkg/license/admin.go
Normal file
|
@ -0,0 +1,100 @@
|
|||
package license
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"io/ioutil"
|
||||
"time"
|
||||
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
// AdminGenerateKeys generates the ECDSA public and private key pair for the admin
|
||||
// side of creating signed license files.
|
||||
func AdminGenerateKeys() (*ecdsa.PrivateKey, error) {
|
||||
privateKey, err := ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
|
||||
return privateKey, err
|
||||
}
|
||||
|
||||
// AdminWriteKeys writes the admin signing key to .pem files on disk.
|
||||
func AdminWriteKeys(key *ecdsa.PrivateKey, privateFile, publicFile string) error {
|
||||
// Encode the private key to PEM format.
|
||||
x509Encoded, err := x509.MarshalECPrivateKey(key)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pemEncoded := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PRIVATE KEY",
|
||||
Bytes: x509Encoded,
|
||||
})
|
||||
|
||||
// Encode the public key to PEM format.
|
||||
x509EncodedPub, err := x509.MarshalPKIXPublicKey(key.Public())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pemEncodedPub := pem.EncodeToMemory(&pem.Block{
|
||||
Type: "PUBLIC KEY",
|
||||
Bytes: x509EncodedPub,
|
||||
})
|
||||
|
||||
// Write the files.
|
||||
if err := ioutil.WriteFile(privateFile, pemEncoded, 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := ioutil.WriteFile(publicFile, pemEncodedPub, 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// AdminLoadPrivateKey loads the private key from disk.
|
||||
func AdminLoadPrivateKey(privateFile string) (*ecdsa.PrivateKey, error) {
|
||||
// Read the private key file.
|
||||
pemEncoded, err := ioutil.ReadFile(privateFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the private key.
|
||||
block, _ := pem.Decode([]byte(pemEncoded))
|
||||
x509Encoded := block.Bytes
|
||||
privateKey, _ := x509.ParseECPrivateKey(x509Encoded)
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
// AdminLoadPublicKey loads the private key from disk.
|
||||
func AdminLoadPublicKey(publicFile string) (*ecdsa.PublicKey, error) {
|
||||
pemEncodedPub, err := ioutil.ReadFile(publicFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Decode the public key.
|
||||
blockPub, _ := pem.Decode([]byte(pemEncodedPub))
|
||||
x509EncodedPub := blockPub.Bytes
|
||||
genericPublicKey, _ := x509.ParsePKIXPublicKey(x509EncodedPub)
|
||||
publicKey := genericPublicKey.(*ecdsa.PublicKey)
|
||||
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
// AdminSignRegistration signs the registration object.
|
||||
func AdminSignRegistration(key *ecdsa.PrivateKey, reg Registration) (string, error) {
|
||||
reg.StandardClaims = jwt.StandardClaims{
|
||||
Issuer: "Maze Admin",
|
||||
IssuedAt: time.Now().Unix(),
|
||||
NotBefore: time.Now().Unix(),
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodES384, reg)
|
||||
signed, err := token.SignedString(key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return signed, nil
|
||||
}
|
25
pkg/license/config.go
Normal file
25
pkg/license/config.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package license
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Run-time configuration variables provided by the application.
|
||||
const pemSigningKey = `-----BEGIN PUBLIC KEY-----
|
||||
MHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEMrqAMHjZ1dPlKwDOsiCSr5N3OSvnYKLM
|
||||
efe2xD+5hJYrpvparRFnaMbMuqde4M6d6sCCKO8BHtfAzmyiQ/CD38zs9MiDsamy
|
||||
FDYEEJu+Fqx482I7fIa5ZEE770+wWJ3k
|
||||
-----END PUBLIC KEY-----`
|
||||
|
||||
var Signer *ecdsa.PublicKey
|
||||
|
||||
func init() {
|
||||
key, err := ParsePublicKeyPEM(pemSigningKey)
|
||||
if err != nil {
|
||||
fmt.Printf("license: failed to parse app keys: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
Signer = key
|
||||
}
|
103
pkg/license/license.go
Normal file
103
pkg/license/license.go
Normal file
|
@ -0,0 +1,103 @@
|
|||
// Package license holds functions related to paid product activation.
|
||||
package license
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
)
|
||||
|
||||
// Registration object encoded into a license key file.
|
||||
type Registration struct {
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email"`
|
||||
jwt.StandardClaims
|
||||
}
|
||||
|
||||
// IsRegistered returns a boolean answer: is the product registered?
|
||||
func IsRegistered() bool {
|
||||
if _, err := GetRegistration(); err == nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetRegistration returns the currently registered user, by checking
|
||||
// for the license.key file in the profile folder.
|
||||
func GetRegistration() (Registration, error) {
|
||||
if Signer == nil {
|
||||
return Registration{}, errors.New("signer not ready")
|
||||
}
|
||||
|
||||
filename := filepath.Join(userdir.ProfileDirectory, "license.key")
|
||||
jwt, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return Registration{}, err
|
||||
}
|
||||
|
||||
// Check if the JWT is valid.
|
||||
reg, err := Validate(Signer, string(jwt))
|
||||
if err != nil {
|
||||
return Registration{}, err
|
||||
}
|
||||
|
||||
return reg, err
|
||||
}
|
||||
|
||||
// UploadLicenseFile handles the user selecting the license key file, and it is
|
||||
// validated and ingested.
|
||||
func UploadLicenseFile(filename string) (Registration, error) {
|
||||
if Signer == nil {
|
||||
return Registration{}, errors.New("signer not ready")
|
||||
}
|
||||
|
||||
jwt, err := ioutil.ReadFile(filename)
|
||||
if err != nil {
|
||||
return Registration{}, err
|
||||
}
|
||||
|
||||
// Check if the JWT is valid.
|
||||
reg, err := Validate(Signer, string(jwt))
|
||||
if err != nil {
|
||||
return Registration{}, err
|
||||
}
|
||||
|
||||
// Upload the license to Doodle's profile directory.
|
||||
outfile := filepath.Join(userdir.ProfileDirectory, "license.key")
|
||||
if err := ioutil.WriteFile(outfile, jwt, 0644); err != nil {
|
||||
return Registration{}, err
|
||||
}
|
||||
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
// Validate the registration is signed by the appropriate public key.
|
||||
func Validate(publicKey *ecdsa.PublicKey, tokenString string) (Registration, error) {
|
||||
var reg Registration
|
||||
token, err := jwt.ParseWithClaims(tokenString, ®, func(token *jwt.Token) (interface{}, error) {
|
||||
return publicKey, nil
|
||||
})
|
||||
if err != nil {
|
||||
return reg, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return reg, errors.New("token not valid")
|
||||
}
|
||||
return reg, nil
|
||||
}
|
||||
|
||||
// ParsePublicKeyPEM loads a public key from PEM format.
|
||||
func ParsePublicKeyPEM(keytext string) (*ecdsa.PublicKey, error) {
|
||||
blockPub, _ := pem.Decode([]byte(keytext))
|
||||
x509EncodedPub := blockPub.Bytes
|
||||
genericPublicKey, _ := x509.ParsePKIXPublicKey(x509EncodedPub)
|
||||
publicKey := genericPublicKey.(*ecdsa.PublicKey)
|
||||
return publicKey, nil
|
||||
}
|
|
@ -6,12 +6,14 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/license"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/native"
|
||||
"git.kirsle.net/apps/doodle/pkg/scripting"
|
||||
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||
"git.kirsle.net/apps/doodle/pkg/uix"
|
||||
"git.kirsle.net/apps/doodle/pkg/updater"
|
||||
"git.kirsle.net/apps/doodle/pkg/windows"
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/render/event"
|
||||
"git.kirsle.net/go/ui"
|
||||
|
@ -30,6 +32,8 @@ type MainScene struct {
|
|||
labelVersion *ui.Label
|
||||
labelHint *ui.Label
|
||||
frame *ui.Frame // Main button frame
|
||||
btnRegister *ui.Button
|
||||
winRegister *ui.Window
|
||||
|
||||
// Update check variables.
|
||||
updateButton *ui.Button
|
||||
|
@ -63,7 +67,7 @@ func (s *MainScene) Setup(d *Doodle) error {
|
|||
|
||||
// Version label.
|
||||
var shareware string
|
||||
if balance.FreeVersion {
|
||||
if !license.IsRegistered() {
|
||||
shareware = " (shareware)"
|
||||
}
|
||||
ver := ui.NewLabel(ui.Label{
|
||||
|
@ -106,6 +110,41 @@ func (s *MainScene) Setup(d *Doodle) error {
|
|||
s.updateButton.Hide()
|
||||
s.Supervisor.Add(s.updateButton)
|
||||
|
||||
// Register button.
|
||||
s.btnRegister = ui.NewButton("Register", ui.NewLabel(ui.Label{
|
||||
Text: "Register Game",
|
||||
Font: balance.LabelFont,
|
||||
}))
|
||||
s.btnRegister.SetStyle(&balance.ButtonPrimary)
|
||||
s.btnRegister.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
if s.winRegister == nil {
|
||||
cfg := windows.License{
|
||||
Supervisor: s.Supervisor,
|
||||
Engine: d.Engine,
|
||||
OnCancel: func() {
|
||||
s.winRegister.Hide()
|
||||
},
|
||||
}
|
||||
cfg.OnLicensed = func() {
|
||||
// License status has changed, reload the window!
|
||||
if s.winRegister != nil {
|
||||
s.winRegister.Hide()
|
||||
}
|
||||
s.winRegister = windows.MakeLicenseWindow(d.width, d.height, cfg)
|
||||
}
|
||||
|
||||
cfg.OnLicensed()
|
||||
}
|
||||
s.winRegister.Show()
|
||||
return nil
|
||||
})
|
||||
s.btnRegister.Compute(d.Engine)
|
||||
s.Supervisor.Add(s.btnRegister)
|
||||
|
||||
if license.IsRegistered() {
|
||||
s.btnRegister.Hide()
|
||||
}
|
||||
|
||||
// Main UI button frame.
|
||||
frame := ui.NewFrame("frame")
|
||||
s.frame = frame
|
||||
|
@ -329,7 +368,7 @@ func (s *MainScene) Draw(d *Doodle) error {
|
|||
|
||||
// Hint label.
|
||||
s.labelHint.MoveTo(render.Point{
|
||||
X: d.width - s.labelHint.Size().W - 32,
|
||||
X: (d.width / 2) - (s.labelHint.Size().W / 2),
|
||||
Y: d.height - s.labelHint.Size().H - 32,
|
||||
})
|
||||
s.labelHint.Present(d.Engine, s.labelHint.Point())
|
||||
|
@ -348,6 +387,16 @@ func (s *MainScene) Draw(d *Doodle) error {
|
|||
})
|
||||
s.frame.Present(d.Engine, s.frame.Point())
|
||||
|
||||
// Register button.
|
||||
s.btnRegister.MoveTo(render.Point{
|
||||
X: d.width - s.btnRegister.Size().W - 24,
|
||||
Y: d.height - s.btnRegister.Size().H - 24,
|
||||
})
|
||||
s.btnRegister.Present(d.Engine, s.btnRegister.Point())
|
||||
|
||||
// Present supervised windows.
|
||||
s.Supervisor.Present(d.Engine)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
275
pkg/windows/license_key.go
Normal file
275
pkg/windows/license_key.go
Normal file
|
@ -0,0 +1,275 @@
|
|||
package windows
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/branding"
|
||||
"git.kirsle.net/apps/doodle/pkg/license"
|
||||
"git.kirsle.net/apps/doodle/pkg/modal"
|
||||
"git.kirsle.net/apps/doodle/pkg/native"
|
||||
"git.kirsle.net/go/render"
|
||||
"git.kirsle.net/go/ui"
|
||||
"git.kirsle.net/go/ui/style"
|
||||
)
|
||||
|
||||
// License window.
|
||||
type License struct {
|
||||
// Settings passed in by doodle
|
||||
Supervisor *ui.Supervisor
|
||||
Engine render.Engine
|
||||
|
||||
OnLicensed func()
|
||||
OnCancel func()
|
||||
}
|
||||
|
||||
// MakeLicenseWindow initializes a license window for any scene.
|
||||
// The window width/height are the actual SDL2 window dimensions.
|
||||
func MakeLicenseWindow(windowWidth, windowHeight int, cfg License) *ui.Window {
|
||||
win := NewLicenseWindow(cfg)
|
||||
win.Compute(cfg.Engine)
|
||||
win.Supervise(cfg.Supervisor)
|
||||
|
||||
// Center the window.
|
||||
size := win.Size()
|
||||
win.MoveTo(render.Point{
|
||||
X: (windowWidth / 2) - (size.W / 2),
|
||||
Y: (windowHeight / 2) - (size.H / 2),
|
||||
})
|
||||
|
||||
return win
|
||||
}
|
||||
|
||||
// NewLicenseWindow initializes the window.
|
||||
func NewLicenseWindow(cfg License) *ui.Window {
|
||||
var (
|
||||
windowWidth = 340
|
||||
windowHeight = 320
|
||||
labelSize = render.NewRect(100, 16)
|
||||
valueSize = render.NewRect(windowWidth-labelSize.W-4, labelSize.H)
|
||||
isRegistered bool
|
||||
registration license.Registration
|
||||
summary = "Unregistered (shareware)"
|
||||
)
|
||||
|
||||
// Get our current registration status.
|
||||
if reg, err := license.GetRegistration(); err == nil {
|
||||
isRegistered = true
|
||||
registration = reg
|
||||
windowHeight = 200
|
||||
summary = "Registered"
|
||||
}
|
||||
|
||||
window := ui.NewWindow("Registration")
|
||||
window.SetButtons(ui.CloseButton)
|
||||
window.Configure(ui.Config{
|
||||
Width: windowWidth,
|
||||
Height: windowHeight,
|
||||
Background: render.RGBA(200, 200, 255, 255),
|
||||
})
|
||||
|
||||
var rows = []struct {
|
||||
IfRegistered bool
|
||||
IfUnregistered bool
|
||||
|
||||
Label string
|
||||
Text string
|
||||
Button *ui.Button
|
||||
ButtonStyle *style.Button
|
||||
Func func()
|
||||
PadY int
|
||||
PadX int
|
||||
}{
|
||||
{
|
||||
Label: "Version:",
|
||||
Text: fmt.Sprintf("%s v%s", branding.AppName, branding.Version),
|
||||
},
|
||||
{
|
||||
Label: "Status:",
|
||||
Text: summary,
|
||||
},
|
||||
{
|
||||
IfRegistered: true,
|
||||
Label: "Name:",
|
||||
Text: registration.Name,
|
||||
},
|
||||
{
|
||||
IfRegistered: true,
|
||||
Label: "Email:",
|
||||
Text: registration.Email,
|
||||
},
|
||||
{
|
||||
IfRegistered: true,
|
||||
Label: "Issued:",
|
||||
Text: time.Unix(registration.IssuedAt, 0).Format("Jan 2, 2006 15:04:05 MST"),
|
||||
},
|
||||
{
|
||||
IfUnregistered: true,
|
||||
Text: "Register your game today! By purchasing the full\n" +
|
||||
"version of Sketchy Maze, you will unlock additional\n" +
|
||||
"features including improved support for custom\n" +
|
||||
"doodads that attach with your level files for easy\n" +
|
||||
"sharing between multiple computers.\n\n" +
|
||||
"When you purchase the game you will receive a\n" +
|
||||
"license key file; click the button below to browse\n" +
|
||||
"and select the key file to register this copy of\n" +
|
||||
branding.AppName + ".",
|
||||
PadY: 8,
|
||||
PadX: 2,
|
||||
},
|
||||
{
|
||||
IfRegistered: true,
|
||||
Text: "Thank you for your support!",
|
||||
PadY: 8,
|
||||
PadX: 2,
|
||||
},
|
||||
{
|
||||
IfUnregistered: true,
|
||||
Button: ui.NewButton("Key Browse", ui.NewLabel(ui.Label{
|
||||
Text: "Upload License Key File",
|
||||
Font: balance.UIFont,
|
||||
})),
|
||||
ButtonStyle: &balance.ButtonPrimary,
|
||||
Func: func() {
|
||||
filename, err := native.OpenFile("Select License File", "*.key *.txt")
|
||||
if err != nil {
|
||||
modal.Alert(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Upload and validate the license key.
|
||||
reg, err := license.UploadLicenseFile(filename)
|
||||
if err != nil {
|
||||
modal.Alert(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
modal.Alert("Thank you, %s!", reg.Name).WithTitle("Registration OK!")
|
||||
if cfg.OnLicensed != nil {
|
||||
cfg.OnLicensed()
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, row := range rows {
|
||||
row := row
|
||||
|
||||
// It has a conditional?
|
||||
if (row.IfRegistered && !isRegistered) ||
|
||||
(row.IfUnregistered && isRegistered) {
|
||||
continue
|
||||
}
|
||||
|
||||
frame := ui.NewFrame("Frame")
|
||||
if row.Label != "" {
|
||||
lf := ui.NewFrame("LabelFrame")
|
||||
lf.Resize(labelSize)
|
||||
label := ui.NewLabel(ui.Label{
|
||||
Text: row.Label,
|
||||
Font: balance.LabelFont,
|
||||
})
|
||||
lf.Pack(label, ui.Pack{
|
||||
Side: ui.E,
|
||||
})
|
||||
frame.Pack(lf, ui.Pack{
|
||||
Side: ui.W,
|
||||
})
|
||||
}
|
||||
|
||||
if row.Text != "" {
|
||||
tf := ui.NewFrame("TextFrame")
|
||||
if row.Label != "" {
|
||||
tf.Resize(valueSize)
|
||||
}
|
||||
label := ui.NewLabel(ui.Label{
|
||||
Text: row.Text,
|
||||
Font: balance.UIFont,
|
||||
})
|
||||
tf.Pack(label, ui.Pack{
|
||||
Side: ui.W,
|
||||
})
|
||||
frame.Pack(tf, ui.Pack{
|
||||
Side: ui.W,
|
||||
})
|
||||
}
|
||||
|
||||
if row.Button != nil {
|
||||
btn := row.Button
|
||||
if row.ButtonStyle != nil {
|
||||
btn.SetStyle(row.ButtonStyle)
|
||||
}
|
||||
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
if row.Func != nil {
|
||||
row.Func()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
btn.Compute(cfg.Engine)
|
||||
cfg.Supervisor.Add(btn)
|
||||
frame.Pack(btn, ui.Pack{
|
||||
Side: ui.N,
|
||||
})
|
||||
}
|
||||
window.Pack(frame, ui.Pack{
|
||||
Side: ui.N,
|
||||
FillX: true,
|
||||
PadY: row.PadY,
|
||||
PadX: row.PadX,
|
||||
})
|
||||
}
|
||||
|
||||
/////////////
|
||||
// Buttons at bottom of window
|
||||
|
||||
bottomFrame := ui.NewFrame("Button Frame")
|
||||
window.Pack(bottomFrame, ui.Pack{
|
||||
Side: ui.N,
|
||||
FillX: true,
|
||||
})
|
||||
|
||||
frame := ui.NewFrame("Button frame")
|
||||
buttons := []struct {
|
||||
label string
|
||||
f func()
|
||||
}{
|
||||
{"Website", func() {
|
||||
native.OpenURL(branding.Website)
|
||||
}},
|
||||
{"Close", func() {
|
||||
if cfg.OnCancel != nil {
|
||||
cfg.OnCancel()
|
||||
}
|
||||
}},
|
||||
}
|
||||
for _, button := range buttons {
|
||||
button := button
|
||||
|
||||
btn := ui.NewButton(button.label, ui.NewLabel(ui.Label{
|
||||
Text: button.label,
|
||||
Font: balance.MenuFont,
|
||||
}))
|
||||
|
||||
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
||||
button.f()
|
||||
return nil
|
||||
})
|
||||
|
||||
btn.Compute(cfg.Engine)
|
||||
cfg.Supervisor.Add(btn)
|
||||
|
||||
frame.Pack(btn, ui.Pack{
|
||||
Side: ui.W,
|
||||
PadX: 4,
|
||||
Expand: true,
|
||||
Fill: true,
|
||||
})
|
||||
}
|
||||
bottomFrame.Pack(frame, ui.Pack{
|
||||
Side: ui.N,
|
||||
PadX: 8,
|
||||
PadY: 12,
|
||||
})
|
||||
|
||||
return window
|
||||
}
|
Loading…
Reference in New Issue
Block a user