github.com/readium/readium-lcp-server@v0.0.0-20240101192032-6e95190e99f1/license/license.go (about)

     1  // Copyright 2020 Readium Foundation. All rights reserved.
     2  // Use of this source code is governed by a BSD-style license
     3  // that can be found in the LICENSE file exposed on Github (readium) in the project repository.
     4  
     5  package license
     6  
     7  import (
     8  	"bytes"
     9  	"crypto/rand"
    10  	"crypto/tls"
    11  	"encoding/base64"
    12  	"errors"
    13  	"fmt"
    14  	"io"
    15  	"log"
    16  	"net/url"
    17  	"reflect"
    18  	"regexp"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/jtacoma/uritemplates"
    23  	"github.com/readium/readium-lcp-server/api"
    24  	"github.com/readium/readium-lcp-server/config"
    25  	"github.com/readium/readium-lcp-server/crypto"
    26  	"github.com/readium/readium-lcp-server/index"
    27  	"github.com/readium/readium-lcp-server/sign"
    28  	"golang.org/x/text/cases"
    29  	"golang.org/x/text/language"
    30  )
    31  
    32  type Key struct {
    33  	Algorithm string `json:"algorithm,omitempty"`
    34  }
    35  
    36  type ContentKey struct {
    37  	Key
    38  	Value []byte `json:"encrypted_value,omitempty"`
    39  }
    40  
    41  type UserKey struct {
    42  	Key
    43  	Hint     string `json:"text_hint,omitempty"`
    44  	Check    []byte `json:"key_check,omitempty"`
    45  	Value    []byte `json:"value,omitempty"`     //Used for license generation
    46  	HexValue string `json:"hex_value,omitempty"` //Used for license generation
    47  }
    48  
    49  type Encryption struct {
    50  	Profile    string     `json:"profile,omitempty"`
    51  	ContentKey ContentKey `json:"content_key"`
    52  	UserKey    UserKey    `json:"user_key"`
    53  }
    54  
    55  type Link struct {
    56  	Rel       string `json:"rel"`
    57  	Href      string `json:"href"`
    58  	Type      string `json:"type,omitempty"`
    59  	Title     string `json:"title,omitempty"`
    60  	Profile   string `json:"profile,omitempty"`
    61  	Templated bool   `json:"templated,omitempty"`
    62  	Size      int64  `json:"length,omitempty"`
    63  	//Digest    []byte `json:"hash,omitempty"`
    64  	Checksum string `json:"hash,omitempty"`
    65  }
    66  
    67  type UserInfo struct {
    68  	ID        string   `json:"id"`
    69  	Email     string   `json:"email,omitempty"`
    70  	Name      string   `json:"name,omitempty"`
    71  	Encrypted []string `json:"encrypted,omitempty"`
    72  }
    73  
    74  type UserRights struct {
    75  	Print *int32     `json:"print,omitempty"`
    76  	Copy  *int32     `json:"copy,omitempty"`
    77  	Start *time.Time `json:"start,omitempty"`
    78  	End   *time.Time `json:"end,omitempty"`
    79  }
    80  
    81  var DefaultLinks map[string]string
    82  
    83  type License struct {
    84  	Provider   string          `json:"provider"`
    85  	ID         string          `json:"id"`
    86  	Issued     time.Time       `json:"issued"`
    87  	Updated    *time.Time      `json:"updated,omitempty"`
    88  	Encryption Encryption      `json:"encryption"`
    89  	Links      []Link          `json:"links,omitempty"`
    90  	User       UserInfo        `json:"user"`
    91  	Rights     *UserRights     `json:"rights,omitempty"`
    92  	Signature  *sign.Signature `json:"signature,omitempty"`
    93  	ContentID  string          `json:"-"`
    94  }
    95  
    96  type LicenseReport struct {
    97  	Provider  string      `json:"provider"`
    98  	ID        string      `json:"id"`
    99  	Issued    time.Time   `json:"issued"`
   100  	Updated   *time.Time  `json:"updated,omitempty"`
   101  	User      UserInfo    `json:"user,omitempty"`
   102  	Rights    *UserRights `json:"rights"`
   103  	ContentID string      `json:"-"`
   104  }
   105  
   106  // EncryptionProfile is an enum of possible encryption profiles
   107  type EncryptionProfile int
   108  
   109  // Declare typed constants for Encryption Profile
   110  const (
   111  	BasicProfile EncryptionProfile = iota
   112  	V1Profile
   113  )
   114  
   115  // isValidPositiveDecimal checks if a string represents a positive decimal numeral with one digit before and after the separator
   116  func isValidPositiveDecimal(s string) bool {
   117  	regex := regexp.MustCompile(`^[1-9]\.\d$`)
   118  	return regex.MatchString(s)
   119  }
   120  
   121  // licenseProfileURL converts the profile token in the config to a standard profile URL
   122  func licenseProfileURL() string {
   123  	// possible profiles are basic, 1.0 and other decimal values
   124  	// "2.x" is not processable in this version, because the api of user_key_prod would have to be modified,
   125  	// and providers must be able to recompile with the original version.
   126  	var profileURL string
   127  	if config.Config.Profile == "basic" {
   128  		profileURL = "http://readium.org/lcp/basic-profile"
   129  	} else if isValidPositiveDecimal(config.Config.Profile) {
   130  		profileURL = "http://readium.org/lcp/profile-" + config.Config.Profile
   131  	} else {
   132  		profileURL = "unknown-profile"
   133  	}
   134  	return profileURL
   135  }
   136  
   137  // SetLicenseProfile sets the license profile from config
   138  func SetLicenseProfile(l *License) error {
   139  	l.Encryption.Profile = licenseProfileURL()
   140  	if l.Encryption.Profile == "unknown-profile" {
   141  		return errors.New("failed to assign a license profile url")
   142  	}
   143  	return nil
   144  }
   145  
   146  // newUUID generates a random UUID according to RFC 4122
   147  // source: http://play.golang.org/p/4FkNSiUDMg
   148  func newUUID() (string, error) {
   149  
   150  	uuid := make([]byte, 16)
   151  	n, err := io.ReadFull(rand.Reader, uuid)
   152  	if n != len(uuid) || err != nil {
   153  		return "", err
   154  	}
   155  	// variant bits; see section 4.1.1
   156  	uuid[8] = uuid[8]&^0xc0 | 0x80
   157  	// version 4 (pseudo-random); see section 4.1.3
   158  	uuid[6] = uuid[6]&^0xf0 | 0x40
   159  	return fmt.Sprintf("%x-%x-%x-%x-%x", uuid[0:4], uuid[4:6], uuid[6:8], uuid[8:10], uuid[10:]), nil
   160  }
   161  
   162  // Initialize sets a license id and issued date, contentID,
   163  func Initialize(contentID string, l *License) {
   164  
   165  	// random license id
   166  	uuid, _ := newUUID()
   167  	l.ID = uuid
   168  	// issued datetime is now
   169  	l.Issued = time.Now().UTC().Truncate(time.Second)
   170  	// set the content id
   171  	l.ContentID = contentID
   172  }
   173  
   174  // CreateDefaultLinks inits the global var DefaultLinks from config data
   175  func CreateDefaultLinks() error {
   176  
   177  	configLinks := config.Config.License.Links
   178  	// the storage url should now be in the storage section.
   179  	storageURL := config.Config.Storage.FileSystem.URL
   180  
   181  	DefaultLinks = make(map[string]string)
   182  
   183  	for key := range configLinks {
   184  		DefaultLinks[key] = configLinks[key]
   185  	}
   186  	// this value supercedes a (deprecated) publication link placed in the license section;
   187  	// keep backward compatibility.
   188  	if storageURL != "" {
   189  		u, err := url.Parse(storageURL)
   190  		if err != nil {
   191  			return err
   192  		}
   193  		if !strings.HasSuffix(u.Path, "/") {
   194  			u.Path = u.Path + "/"
   195  		}
   196  		DefaultLinks["publication"] = u.String() + "{publication_id}"
   197  	}
   198  	return nil
   199  }
   200  
   201  // setDefaultLinks sets a Link array from config links
   202  func setDefaultLinks() []Link {
   203  
   204  	links := new([]Link)
   205  	for key := range DefaultLinks {
   206  		link := Link{Href: DefaultLinks[key], Rel: key}
   207  		*links = append(*links, link)
   208  	}
   209  	return *links
   210  }
   211  
   212  // appendDefaultLinks appends default links to custom links
   213  func appendDefaultLinks(inLinks *[]Link) []Link {
   214  
   215  	if *inLinks == nil {
   216  		// if there are no custom links in the partial license, set default links
   217  		return setDefaultLinks()
   218  	} else {
   219  		// otherwise append default links to custom links.
   220  		// If a default Link is already present, override the custom links with the default one
   221  		links := new([]Link)
   222  		for _, link := range *inLinks {
   223  			rel := link.Rel
   224  			if _, exist := DefaultLinks[rel]; !exist {
   225  				*links = append(*links, link)
   226  			}
   227  		}
   228  		return append(*links, setDefaultLinks()...)
   229  	}
   230  }
   231  
   232  // SetLicenseLinks sets publication and status links
   233  // l.ContentID must have been set before the call
   234  func SetLicenseLinks(l *License, c index.Content) error {
   235  
   236  	// append default links to custom links
   237  	l.Links = appendDefaultLinks(&l.Links)
   238  
   239  	// check if the publication link is in the content database
   240  	hasPubLink, err := isURL(c.Location)
   241  	if err != nil {
   242  		return err
   243  	}
   244  
   245  	for i := 0; i < len(l.Links); i++ {
   246  		// set the publication link
   247  		if l.Links[i].Rel == "publication" {
   248  			if hasPubLink {
   249  				// override a default link (from the config) by a publication url from the db if it exists
   250  				l.Links[i].Href = c.Location
   251  				l.Links[i].Title = l.ContentID
   252  				hasPubLink = false
   253  			} else {
   254  				l.Links[i].Href = expandUriTemplate(l.Links[i].Href, "publication_id", l.ContentID)
   255  				l.Links[i].Title = c.Location
   256  			}
   257  			l.Links[i].Type = c.Type
   258  			l.Links[i].Size = c.Length
   259  			l.Links[i].Checksum = c.Sha256
   260  		}
   261  		// set the status link
   262  		if l.Links[i].Rel == "status" {
   263  			l.Links[i].Href = expandUriTemplate(l.Links[i].Href, "license_id", l.ID)
   264  			l.Links[i].Type = api.ContentType_LSD_JSON
   265  		}
   266  
   267  		// set the hint page link, which may be associated with a specific license
   268  		if l.Links[i].Rel == "hint" {
   269  			l.Links[i].Href = expandUriTemplate(l.Links[i].Href, "license_id", l.ID)
   270  			l.Links[i].Type = api.ContentType_TEXT_HTML
   271  		}
   272  	}
   273  
   274  	// add the publication link present in the content index
   275  	if hasPubLink {
   276  		link := Link{
   277  			Rel:      "publication",
   278  			Href:     c.Location,
   279  			Title:    l.ContentID,
   280  			Type:     c.Type,
   281  			Size:     c.Length,
   282  			Checksum: c.Sha256,
   283  		}
   284  		l.Links = append(l.Links, link)
   285  	}
   286  
   287  	return nil
   288  }
   289  
   290  // expandUriTemplate resolves a url template from the configuration to a url the system can embed in a status document
   291  func expandUriTemplate(uriTemplate, variable, value string) string {
   292  	template, _ := uritemplates.Parse(uriTemplate)
   293  	values := make(map[string]interface{})
   294  	values[variable] = value
   295  	expanded, err := template.Expand(values)
   296  	if err != nil {
   297  		log.Printf("failed to expand an uri template: %s", uriTemplate)
   298  		return uriTemplate
   299  	}
   300  	return expanded
   301  }
   302  
   303  // EncryptLicenseFields sets the content key, encrypted user info and key check
   304  func EncryptLicenseFields(l *License, c index.Content) error {
   305  
   306  	// generate the user key
   307  	encryptionKey := GenerateUserKey(l.Encryption.UserKey)
   308  	if encryptionKey == nil {
   309  		return errors.New("incompatible LCP profile; error generating a user key")
   310  	}
   311  
   312  	// empty the passphrase hash to avoid sending it back to the user
   313  	l.Encryption.UserKey.Value = nil
   314  	l.Encryption.UserKey.HexValue = ""
   315  
   316  	// encrypt the content key with the user key
   317  	encrypterContentKey := crypto.NewAESEncrypter_CONTENT_KEY()
   318  	l.Encryption.ContentKey.Algorithm = encrypterContentKey.Signature()
   319  	l.Encryption.ContentKey.Value = encryptKey(encrypterContentKey, c.EncryptionKey, encryptionKey[:])
   320  
   321  	// encrypt the user info fields
   322  	encrypterFields := crypto.NewAESEncrypter_FIELDS()
   323  	err := encryptFields(encrypterFields, l, encryptionKey[:])
   324  	if err != nil {
   325  		return err
   326  	}
   327  
   328  	// build the key check
   329  	encrypterUserKeyCheck := crypto.NewAESEncrypter_USER_KEY_CHECK()
   330  	l.Encryption.UserKey.Check, err = buildKeyCheck(l.ID, encrypterUserKeyCheck, encryptionKey[:])
   331  	if err != nil {
   332  		return err
   333  	}
   334  	return nil
   335  }
   336  
   337  func encryptKey(encrypter crypto.Encrypter, key []byte, kek []byte) []byte {
   338  	var out bytes.Buffer
   339  	in := bytes.NewReader(key)
   340  	encrypter.Encrypt(kek[:], in, &out)
   341  	return out.Bytes()
   342  }
   343  
   344  func encryptFields(encrypter crypto.Encrypter, l *License, key []byte) error {
   345  	for _, toEncrypt := range l.User.Encrypted {
   346  		var out bytes.Buffer
   347  		field := getField(&l.User, toEncrypt)
   348  		err := encrypter.Encrypt(key[:], bytes.NewBufferString(field.String()), &out)
   349  		if err != nil {
   350  			return err
   351  		}
   352  		field.Set(reflect.ValueOf(base64.StdEncoding.EncodeToString(out.Bytes())))
   353  	}
   354  	return nil
   355  }
   356  
   357  func getField(u *UserInfo, field string) reflect.Value {
   358  	v := reflect.ValueOf(u).Elem()
   359  	c := cases.Title(language.Und, cases.NoLower)
   360  	return v.FieldByName(c.String(field))
   361  }
   362  
   363  // buildKeyCheck
   364  // encrypt the license id with the key used for encrypting content
   365  func buildKeyCheck(licenseID string, encrypter crypto.Encrypter, key []byte) ([]byte, error) {
   366  
   367  	var out bytes.Buffer
   368  	err := encrypter.Encrypt(key, bytes.NewBufferString(licenseID), &out)
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  	return out.Bytes(), nil
   373  }
   374  
   375  // SignLicense signs a license using the server certificate
   376  func SignLicense(l *License, cert *tls.Certificate) error {
   377  
   378  	sig, err := sign.NewSigner(cert)
   379  	if err != nil {
   380  		return err
   381  	}
   382  	res, err := sig.Sign(l)
   383  	if err != nil {
   384  		return err
   385  	}
   386  	l.Signature = &res
   387  
   388  	return nil
   389  }
   390  
   391  func isURL(filePathOrURL string) (bool, error) {
   392  	url, err := url.Parse(filePathOrURL)
   393  	if err != nil {
   394  		return false, errors.New("error parsing the input string")
   395  	}
   396  	return url.Scheme == "http" || url.Scheme == "https", nil
   397  }