From 1a5c918307d9f11f1b1d988a95c886fe6bee539c Mon Sep 17 00:00:00 2001 From: Gunnsteinn Hall Date: Sat, 8 Jul 2017 22:00:11 +0000 Subject: [PATCH] License package added to v2. --- license/crypto.go | 164 ++++++++++++++++++++++++++++++++++++++++++++ license/key.go | 137 ++++++++++++++++++++++++++++++++++++ license/pubkeys.go | 19 +++++ license/util.go | 33 +++++++++ pdf/model/writer.go | 15 +--- 5 files changed, 356 insertions(+), 12 deletions(-) create mode 100644 license/crypto.go create mode 100644 license/key.go create mode 100644 license/pubkeys.go create mode 100644 license/util.go diff --git a/license/crypto.go b/license/crypto.go new file mode 100644 index 00000000..763b5837 --- /dev/null +++ b/license/crypto.go @@ -0,0 +1,164 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package license + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha512" + "crypto/x509" + "encoding/base64" + "encoding/json" + "encoding/pem" + "fmt" + "strings" + "time" + + _ "github.com/unidoc/unidoc/common" +) + +const ( + licenseKeyHeader = "-----BEGIN UNIDOC LICENSE KEY-----" + licenseKeyFooter = "-----END UNIDOC LICENSE KEY-----" +) + +// Returns signed content in a base64 format which is in format: +// +// Base64OriginalContent +// + +// Base64Signature +func signContent(privKey string, content []byte) (string, error) { + privBlock, _ := pem.Decode([]byte(privKey)) + if privBlock == nil { + return "", fmt.Errorf("PrivKey failed") + } + + priv, err := x509.ParsePKCS1PrivateKey(privBlock.Bytes) + if err != nil { + return "", err + } + + hash := sha512.New() + hash.Write(content) + digest := hash.Sum(nil) + + signature, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA512, digest) + if err != nil { + return "", err + } + + ret := base64.StdEncoding.EncodeToString(content) + ret += "\n+\n" + ret += base64.StdEncoding.EncodeToString(signature) + + return ret, nil +} + +// Verifies and reconstructs the original content +func verifyContent(pubKey string, content string) ([]byte, error) { + // Empty + line is the delimiter between content and signature. + // We need to cope with both unix and windows newline, default to unix + // one and try Windows one as fallback. + separator := "\n+\n" + separatorFallback := "\r\n+\r\n" + + sepIdx := strings.Index(content, separator) + if sepIdx == -1 { + sepIdx = strings.Index(content, separatorFallback) + if sepIdx == -1 { + return nil, fmt.Errorf("Invalid input, signature separator") + } + } + + // Original is from start until the separator - 1 + original := content[:sepIdx] + + // Signature is from after the separator until the end of file. + signatureStarts := sepIdx + len(separator) + signature := content[signatureStarts:] + + if original == "" || signature == "" { + return nil, fmt.Errorf("Invalid input, missing original or signature") + } + + originalBytes, err := base64.StdEncoding.DecodeString(original) + if err != nil { + return nil, fmt.Errorf("Invalid input original") + } + + signatureBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return nil, fmt.Errorf("Invalid input signature") + } + + pubBlock, _ := pem.Decode([]byte(pubKey)) + if pubBlock == nil { + return nil, fmt.Errorf("PubKey failed") + } + + tempPub, err := x509.ParsePKIXPublicKey(pubBlock.Bytes) + if err != nil { + return nil, err + } + + pub := tempPub.(*rsa.PublicKey) + if pub == nil { + return nil, fmt.Errorf("PubKey conversion failed") + } + + hash := sha512.New() + hash.Write(originalBytes) + digest := hash.Sum(nil) + + err = rsa.VerifyPKCS1v15(pub, crypto.SHA512, digest, signatureBytes) + if err != nil { + return nil, err + } + + return originalBytes, nil +} + +// Returns the content wrap around the headers +func getWrappedContent(header string, footer string, content string) (string, error) { + // Find all content between header and footer. + headerIdx := strings.Index(content, header) + if headerIdx == -1 { + return "", fmt.Errorf("Header not found") + } + + footerIdx := strings.Index(content, footer) + if footerIdx == -1 { + return "", fmt.Errorf("Footer not found") + } + + start := headerIdx + len(header) + 1 + return content[start : footerIdx-1], nil +} + +func licenseKeyDecode(content string) (LicenseKey, error) { + var ret LicenseKey + + data, err := getWrappedContent(licenseKeyHeader, licenseKeyFooter, content) + if err != nil { + return ret, err + } + + verifiedRet, err := verifyContent(pubKey, data) + if err != nil { + return ret, err + } + + err = json.Unmarshal(verifiedRet, &ret) + if err != nil { + return ret, err + } + + ret.CreatedAt = time.Unix(ret.CreatedAtInt, 0) + ret.ExpiresAt = time.Unix(ret.ExpiresAtInt, 0) + + return ret, nil +} diff --git a/license/key.go b/license/key.go new file mode 100644 index 00000000..57957f82 --- /dev/null +++ b/license/key.go @@ -0,0 +1,137 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package license + +import ( + "fmt" + "strings" + "time" + + "github.com/unidoc/unidoc/common" +) + +const ( + LicenseTypeCommercial = "commercial" + LicenseTypeOpensource = "opensource" +) + +const opensourceLicenseId = "01aa523c-b4c6-4d57-bbdd-5a88d2bd5300" +const opensourceLicenseUuid = "01aa523c-b4c6-4d57-bbdd-5a88d2bd5301" + +func getSupportedFeatures() []string { + return []string{"unidoc", "unidoc-cli"} +} + +// Make sure all time is at least after this for sanity check. +var testTime = time.Date(2010, 1, 1, 0, 0, 0, 0, time.UTC) + +type LicenseKey struct { + LicenseId string `json:"license_id"` + CustomerId string `json:"customer_id"` + CustomerName string `json:"customer_name"` + Type string `json:"type"` + Features []string `json:"features"` + CreatedAt time.Time `json:"-"` + CreatedAtInt int64 `json:"created_at"` + ExpiresAt time.Time `json:"-"` + ExpiresAtInt int64 `json:"expires_at"` + CreatedBy string `json:"created_by"` + CreatorName string `json:"creator_name"` + CreatorEmail string `json:"creator_email"` +} + +func (this *LicenseKey) Validate() error { + if len(this.LicenseId) < 10 { + return fmt.Errorf("Invalid license: License Id") + } + + if len(this.CustomerId) < 10 { + return fmt.Errorf("Invalid license: Customer Id") + } + + if len(this.CustomerName) < 1 { + return fmt.Errorf("Invalid license: Customer Name") + } + + if this.Features == nil || len(this.Features) < 1 { + return fmt.Errorf("Invalid license: No features") + } + + for _, feature := range this.Features { + found := false + + for _, sf := range getSupportedFeatures() { + if sf == feature { + found = true + break + } + } + + if !found { + return fmt.Errorf("Invalid license: Unsupported feature %s", feature) + } + } + + if testTime.After(this.CreatedAt) { + return fmt.Errorf("Invalid license: Created At is invalid") + } + + if this.CreatedAt.After(this.ExpiresAt) { + return fmt.Errorf("Invalid license: Created At cannot be Greater than Expires At") + } + + if common.ReleasedAt.After(this.ExpiresAt) { + return fmt.Errorf("Expired license, expired at: %s", common.UtcTimeFormat(this.ExpiresAt)) + } + + if len(this.CreatorName) < 1 { + return fmt.Errorf("Invalid license: Creator name") + } + + if len(this.CreatorEmail) < 1 { + return fmt.Errorf("Invalid license: Creator email") + } + + return nil +} + +func (this *LicenseKey) TypeToString() string { + ret := "AGPLv3 Open Source License" + + if this.Type == LicenseTypeCommercial { + ret = "Commercial License" + } + + return ret +} + +func (this *LicenseKey) ToString() string { + str := fmt.Sprintf("License Id: %s\n", this.LicenseId) + str += fmt.Sprintf("Customer Id: %s\n", this.CustomerId) + str += fmt.Sprintf("Customer Name: %s\n", this.CustomerName) + str += fmt.Sprintf("Type: %s\n", this.Type) + str += fmt.Sprintf("Features: %s\n", strings.Join(this.Features, ", ")) + str += fmt.Sprintf("Created At: %s\n", common.UtcTimeFormat(this.CreatedAt)) + str += fmt.Sprintf("Expires At: %s\n", common.UtcTimeFormat(this.ExpiresAt)) + str += fmt.Sprintf("Creator: %s <%s>\n", this.CreatorName, this.CreatorEmail) + return str +} + +func MakeOpensourceLicenseKey() *LicenseKey { + lk := LicenseKey{} + lk.LicenseId = opensourceLicenseId + lk.CustomerId = opensourceLicenseUuid + lk.CustomerName = "Open Source Evangelist" + lk.Type = LicenseTypeOpensource + lk.Features = getSupportedFeatures() + lk.CreatedAt = time.Now().UTC() + lk.CreatedAtInt = lk.CreatedAt.Unix() + lk.ExpiresAt = lk.CreatedAt.AddDate(10, 0, 0) + lk.ExpiresAtInt = lk.ExpiresAt.Unix() + lk.CreatorName = "UniDoc Support" + lk.CreatorEmail = "support@unidoc.io" + return &lk +} diff --git a/license/pubkeys.go b/license/pubkeys.go new file mode 100644 index 00000000..701cb1f1 --- /dev/null +++ b/license/pubkeys.go @@ -0,0 +1,19 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +package license + +// Public key +const pubKey = ` +-----BEGIN PUBLIC KEY----- +MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmFUiyd7b5XjpkP5Rap4w +Dc1dyzIQ4LekxrvytnEMpNUbo6iA74V8ruZOvrScsf2QeN9/qrUG8qEbUWdoEYq+ +otFNAFNxlGbxbDHcdGVaM0OXdXgDyL5aIEagL0c5pwjIdPGIn46f78eMJ+JkdcpD +DJaqYXdrz5KeshjSiIaa7menBIAXS4UFxNfHhN0HCYZYqQG7bK+s5rRHonydNWEG +H8Myvr2pya2KrMumfmAxUB6fenC/4O0Wr8gfPOU8RitmbDvQPIRXOL4vTBrBdbaA +9nwNP+i//20MT2bxmeWB+gpcEhGpXZ733azQxrC3J4v3CZmENStDK/KDSPKUGfu6 +fwIDAQAB +-----END PUBLIC KEY----- +` diff --git a/license/util.go b/license/util.go new file mode 100644 index 00000000..9bbc80d5 --- /dev/null +++ b/license/util.go @@ -0,0 +1,33 @@ +/* + * This file is subject to the terms and conditions defined in + * file 'LICENSE.md', which is part of this source code package. + */ + +// Package license contains customer license handling with open source license as default. +// The main purpose of the license package is to serve commercial license users to help identify version eligibility +// based on purchase date etc. +package license + +// Defaults to the open source license. +var licenseKey *LicenseKey = MakeOpensourceLicenseKey() + +// Sets and validates the license key. +func SetLicenseKey(content string) error { + lk, err := licenseKeyDecode(content) + if err != nil { + return err + } + + err = lk.Validate() + if err != nil { + return err + } + + licenseKey = &lk + + return nil +} + +func GetLicenseKey() *LicenseKey { + return licenseKey +} diff --git a/pdf/model/writer.go b/pdf/model/writer.go index 46b40ba3..68fdd9e7 100644 --- a/pdf/model/writer.go +++ b/pdf/model/writer.go @@ -19,24 +19,15 @@ import ( "time" "github.com/unidoc/unidoc/common" + "github.com/unidoc/unidoc/license" . "github.com/unidoc/unidoc/pdf/core" ) -var pdfProducer = "" var pdfCreator = "" func getPdfProducer() string { - if len(pdfProducer) > 0 { - return pdfProducer - } - - // We kindly request that users of UniDoc and derived versions refer to UniDoc in the Producer line. - // Something like "(based on UniDoc)" would be great. - return fmt.Sprintf("UniDoc Library version %s - http://unidoc.io", getUniDocVersion()) -} - -func SetPdfProducer(producer string) { - pdfProducer = producer + licenseKey := license.GetLicenseKey() + return fmt.Sprintf("UniDoc v%s (%s) - http://unidoc.io", getUniDocVersion(), licenseKey.TypeToString()) } func getPdfCreator() string {