github.com/unidoc/unidoc@v2.2.0+incompatible/common/license/crypto.go (about)

     1  /*
     2   * This file is subject to the terms and conditions defined in
     3   * file 'LICENSE.md', which is part of this source code package.
     4   */
     5  
     6  package license
     7  
     8  import (
     9  	"crypto"
    10  	"crypto/rand"
    11  	"crypto/rsa"
    12  	"crypto/sha512"
    13  	"crypto/x509"
    14  	"encoding/base64"
    15  	"encoding/json"
    16  	"encoding/pem"
    17  	"fmt"
    18  	"strings"
    19  	"time"
    20  
    21  	_ "github.com/unidoc/unidoc/common"
    22  )
    23  
    24  const (
    25  	licenseKeyHeader = "-----BEGIN UNIDOC LICENSE KEY-----"
    26  	licenseKeyFooter = "-----END UNIDOC LICENSE KEY-----"
    27  )
    28  
    29  // Returns signed content in a base64 format which is in format:
    30  //
    31  // Base64OriginalContent
    32  // +
    33  // Base64Signature
    34  func signContent(privKey string, content []byte) (string, error) {
    35  	privBlock, _ := pem.Decode([]byte(privKey))
    36  	if privBlock == nil {
    37  		return "", fmt.Errorf("PrivKey failed")
    38  	}
    39  
    40  	priv, err := x509.ParsePKCS1PrivateKey(privBlock.Bytes)
    41  	if err != nil {
    42  		return "", err
    43  	}
    44  
    45  	hash := sha512.New()
    46  	hash.Write(content)
    47  	digest := hash.Sum(nil)
    48  
    49  	signature, err := rsa.SignPKCS1v15(rand.Reader, priv, crypto.SHA512, digest)
    50  	if err != nil {
    51  		return "", err
    52  	}
    53  
    54  	ret := base64.StdEncoding.EncodeToString(content)
    55  	ret += "\n+\n"
    56  	ret += base64.StdEncoding.EncodeToString(signature)
    57  
    58  	return ret, nil
    59  }
    60  
    61  // Verifies and reconstructs the original content
    62  func verifyContent(pubKey string, content string) ([]byte, error) {
    63  	// Empty + line is the delimiter between content and signature.
    64  	// We need to cope with both unix and windows newline, default to unix
    65  	// one and try Windows one as fallback.
    66  	separator := "\n+\n"
    67  	separatorFallback := "\r\n+\r\n"
    68  
    69  	sepIdx := strings.Index(content, separator)
    70  	if sepIdx == -1 {
    71  		sepIdx = strings.Index(content, separatorFallback)
    72  		if sepIdx == -1 {
    73  			return nil, fmt.Errorf("Invalid input, signature separator")
    74  		}
    75  	}
    76  
    77  	// Original is from start until the separator - 1
    78  	original := content[:sepIdx]
    79  
    80  	// Signature is from after the separator until the end of file.
    81  	signatureStarts := sepIdx + len(separator)
    82  	signature := content[signatureStarts:]
    83  
    84  	if original == "" || signature == "" {
    85  		return nil, fmt.Errorf("Invalid input, missing original or signature")
    86  	}
    87  
    88  	originalBytes, err := base64.StdEncoding.DecodeString(original)
    89  	if err != nil {
    90  		return nil, fmt.Errorf("Invalid input original")
    91  	}
    92  
    93  	signatureBytes, err := base64.StdEncoding.DecodeString(signature)
    94  	if err != nil {
    95  		return nil, fmt.Errorf("Invalid input signature")
    96  	}
    97  
    98  	pubBlock, _ := pem.Decode([]byte(pubKey))
    99  	if pubBlock == nil {
   100  		return nil, fmt.Errorf("PubKey failed")
   101  	}
   102  
   103  	tempPub, err := x509.ParsePKIXPublicKey(pubBlock.Bytes)
   104  	if err != nil {
   105  		return nil, err
   106  	}
   107  
   108  	pub := tempPub.(*rsa.PublicKey)
   109  	if pub == nil {
   110  		return nil, fmt.Errorf("PubKey conversion failed")
   111  	}
   112  
   113  	hash := sha512.New()
   114  	hash.Write(originalBytes)
   115  	digest := hash.Sum(nil)
   116  
   117  	err = rsa.VerifyPKCS1v15(pub, crypto.SHA512, digest, signatureBytes)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	return originalBytes, nil
   123  }
   124  
   125  // Returns the content wrap around the headers
   126  func getWrappedContent(header string, footer string, content string) (string, error) {
   127  	// Find all content between header and footer.
   128  	headerIdx := strings.Index(content, header)
   129  	if headerIdx == -1 {
   130  		return "", fmt.Errorf("Header not found")
   131  	}
   132  
   133  	footerIdx := strings.Index(content, footer)
   134  	if footerIdx == -1 {
   135  		return "", fmt.Errorf("Footer not found")
   136  	}
   137  
   138  	start := headerIdx + len(header) + 1
   139  	return content[start : footerIdx-1], nil
   140  }
   141  
   142  func licenseKeyDecode(content string) (LicenseKey, error) {
   143  	var ret LicenseKey
   144  
   145  	data, err := getWrappedContent(licenseKeyHeader, licenseKeyFooter, content)
   146  	if err != nil {
   147  		return ret, err
   148  	}
   149  
   150  	verifiedRet, err := verifyContent(pubKey, data)
   151  	if err != nil {
   152  		return ret, err
   153  	}
   154  
   155  	err = json.Unmarshal(verifiedRet, &ret)
   156  	if err != nil {
   157  		return ret, err
   158  	}
   159  
   160  	ret.CreatedAt = time.Unix(ret.CreatedAtInt, 0)
   161  
   162  	if ret.ExpiresAtInt > 0 {
   163  		expiresAt := time.Unix(ret.ExpiresAtInt, 0)
   164  		ret.ExpiresAt = &expiresAt
   165  	}
   166  
   167  	return ret, nil
   168  }