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 }