readium / webpub-manifest

📜 A JSON based Web Publication Manifest format used at the core of the Readium project
BSD 3-Clause "New" or "Revised" License
90 stars 23 forks source link

EPUB equivalent of "letterer" #26

Closed chocolatkey closed 5 years ago

chocolatkey commented 5 years ago

The webpub schema has a "letterer", "inker", and "penciler" field in the metadata, seen here: https://github.com/readium/webpub-manifest/blob/master/schema/metadata.schema.json#L96 . What are the equivalent contributor role MARC codes? I do not see them implemented in the two most up-to-date streamers: https://github.com/readium/r2-streamer-swift/blob/develop/Sources/parser/EPUB/MetadataParser.swift#L260 https://github.com/readium/r2-streamer-kotlin/blob/develop/r2-streamer/src/main/java/org/readium/r2/streamer/parser/epub/MetadataParser.kt#L132

I ask because I am writing an application that is generating an EPUB3 based on WebPub metadata, and have a "letterer"

danielweck commented 5 years ago

This is the current parser documentation for contributors: https://github.com/readium/architecture/blob/master/streamer/parser/metadata.md#contributors

danielweck commented 5 years ago

In the Go implementation, support for Letterer is missing too: https://github.com/readium/r2-streamer-go/blob/e4bf2ff6f829c2e6a87d62d83e4edf7bb876859c/parser/epub.go#L243-L244

In the TypeScript implementation ltr is used: https://github.com/readium/r2-shared-js/blob/096a56aa96a030e4f11a4809d9577ab06cfd19ce/src/parser/epub.ts#L1047-L1053 ...but this looks totally made up (I do not remember where this comes from, maybe a copy/paste from somewhere). Same problem with Penciller and Inker - I am filing an issue in r2-shared-js to address this.

MARC references:

http://id.loc.gov/vocabulary/relators.html https://www.loc.gov/marc/relators/relaterm.html https://www.loc.gov/marc/relators/relacode.html

danielweck commented 5 years ago

Also note that there used to be an explicit mention of Schema.org http://schema.org/inker http://schema.org/penciler and http://schema.org/letterer in this other piece of documentation ( see: https://github.com/readium/webpub-manifest/commit/d928f0daeb1ea9540682dbc4f1a0643b61de7028 ), but the informative contents didn't make the cut into the new URLs ( https://readium.org/architecture/streamer/parser/metadata / https://github.com/readium/architecture/blob/master/streamer/parser/metadata.md )

EDIT: the JSON-LD includes the definitions of Inker Penciller and Letterer (thus why the code models include these natively, despite the absence of EPUB / MARC:relator sources): https://github.com/readium/webpub-manifest/blob/6930a12439d7b36f2302d1ef233a6ad41b4854d6/context.jsonld#L22-L24

HadrienGardeur commented 5 years ago

It made more sense to strictly maintain the mapping through the JSON LD context document, where letterer is defined: https://github.com/readium/webpub-manifest/blob/master/context.jsonld#L24

But to answer the initial question: there's no equivalent in EPUB because there's no equivalent in MARC relators. You can still include the letterer in your output, but simply as a "contributor". Given the lack of good support for EPUB metadata out there, you're not losing much.

@chocolatkey could you explain what you're working on a bit more and why you're generating an EPUB3 from a RWPM?

chocolatkey commented 5 years ago

@HadrienGardeur Over at J-Novel Club, I implemented the new manga reader (https://j-novel.club/mc/animeta-volume-1-chapter-1) using the WebPub spec, and was happy with the resulting API. Now that the first volumes are finished and are set to be released soon, I decided to reuse some of the backend (golang) logic for generation of the fixed-layout EPUBs. Due to the database-less nature of the manga system, the metadata is stored on disk as webpub metadata during creation and editing. As the WebPub is JSON, and the typescript/javascript of r2 already has classes to deal with it, it is easy to have a web interface parse it, turn it into editable fields, and then post back to the server, which validates and stores it. When all the metadata is all ready and images have been uploaded, the EPUB is generated on the fly as it is downloading, which includes image conversion and of course, OPF generation.

The reason I was asking about the "letterer" field, is because J-Novel Club deals with translations, and I thought the "letterer" would be the most appropriate field for the typesetters, but then realized there's no appropriate MARC code when implementing it:

type SchemaThing struct {
    SchemaType string `json:"@type"`
    Name       string `json:"name"`
    Alt        string `json:"alternateName,omitempty"`
    Identifier string `json:"identifier"`
    Url        string `json:"url,omitempty,string"`
}

type WebPubMetadata struct {
    SchemaType  string  `json:"@type"` // Probably ComicIssue
    Identifier  string  `json:"identifier"`
    Title       string  `json:"title"`
    Subtitle    string  `json:"subtitle,omitempty"`
    Description string  `json:"description,omitempty"`
    IssueNumber float64 `json:"issueNumber,omitempty"`

    // Contributors
    Author      []*SchemaThing `json:"author,omitempty"`
    Artist      []*SchemaThing `json:"artist,omitempty"`
    Editor      []*SchemaThing `json:"editor,omitempty"`
    Translator  []*SchemaThing `json:"translator,omitempty"`
    Illustrator []*SchemaThing `json:"illustrator,omitempty"`
    Letterer    []*SchemaThing `json:"letterer,omitempty"`
    Penciler    []*SchemaThing `json:"penciler,omitempty"`
    Colorist    []*SchemaThing `json:"colorist,omitempty"`
    Inker       []*SchemaThing `json:"inker,omitempty"`
    Narrator    []*SchemaThing `json:"narrator,omitempty"`

    Publisher            []*SchemaThing   `json:"publisher,omitempty"`
    Imprint              []*SchemaThing   `json:"imprint,omitempty"`
    AccessMode           string           `json:"accessMode,omitempty"`
    AccessibilityControl []string         `json:"accessibilityControl,omitempty"`
    AccessibilitySummary string           `json:"accessibilitySummary,omitempty"`
    Free                 bool             `json:"isAccessibleForFree"`
    Provider             string           `json:"provider,omitempty"`
    Published            time.Time        `json:"published,omitempty"`
    Modified             time.Time        `json:"modified,omitempty"`
    Expires              time.Time        `json:"expires,omitempty"`
    Language             string           `json:"language,omitempty"`
    Genre                *[]WebPubSubject `json:"subject,omitempty"`
    BelongsTo            *WebPubOwnership `json:"belongsTo,omitempty"`
    Direction            string           `json:"readingProgression,omitempty"`
    PageCount            uint64           `json:"numberOfPages,omitempty"`
    Image                string           `json:"image,omitempty"`
    Thumbnail            string           `json:"thumbnailUrl,omitempty"`
    Rendition            *WebPubRendition `json:"rendition,omitempty"`

    Width  uint16 `json:"width,omitempty"`
    Height uint16 `json:"height,omitempty"`
}

type DaisyCreator struct { // This is very basic at the moment
    Identifier string
    Name       string
    Role       string
}

func convertSchemaThingsToCreators(container []*DaisyCreator, things []*SchemaThing, role string, offset *int) {
    for _, thing := range things {
        container[*offset] = &DaisyCreator{
            Identifier: fmt.Sprintf("creator%02d", *offset+1), // If there are more than 99 creators... well...
            Name:       escapeXML(thing.Name),
            Role:       role,
        }
        *offset += 1
    }
}

///// In the code
convertSchemaThingsToCreators(creators, metadata.Author, "aut", &at)
convertSchemaThingsToCreators(creators, metadata.Artist, "art", &at)
convertSchemaThingsToCreators(creators, metadata.Translator, "trl", &at)
convertSchemaThingsToCreators(creators, metadata.Editor, "edt", &at)

// convertSchemaThingsToCreators(creators, metadata.Letterer, "???", &at) TODO - what is letterer MARC code equivalent??
convertSchemaThingsToCreators(creators, metadata.Illustrator, "ill", &at)
//convertSchemaThingsToCreators(creators, metadata.Penciler, "???", &at) Same as above
convertSchemaThingsToCreators(creators, metadata.Colorist, "clr", &at)
//convertSchemaThingsToCreators(creators, metadata.Inker, "???", &at) Same as above
convertSchemaThingsToCreators(creators, metadata.Narrator, "aut", &at)

-->

{{ range $i, $creator := .Creators }}
<dc:creator id="{{$creator.Identifier}}">{{$creator.Name}}</dc:creator>
<meta refines="#{{$creator.Identifier}}" property="role" scheme="marc:relators">{{$creator.Role}}</meta>
<meta refines="#{{$creator.Identifier}}" property="display-seq">{{add $i 1}}</meta>{{ end }}

-->

<dc:creator id="creator01">Yaso Hanamura</dc:creator>
<meta refines="#creator01" property="role" scheme="marc:relators">aut</meta>
<meta refines="#creator01" property="display-seq">1</meta>
<dc:creator id="creator02">Yaso Hanamura</dc:creator>
<meta refines="#creator02" property="role" scheme="marc:relators">art</meta>
<meta refines="#creator02" property="display-seq">2</meta>
<dc:creator id="creator03">T. Emerson</dc:creator>
<meta refines="#creator03" property="role" scheme="marc:relators">trl</meta>
<meta refines="#creator03" property="display-seq">3</meta>
<dc:creator id="creator04">Maneesh Maganti</dc:creator>
<meta refines="#creator04" property="role" scheme="marc:relators">edt</meta>
<meta refines="#creator04" property="display-seq">4</meta>

For now, I've just changed their role to be that of an editor, since that also applies

HadrienGardeur commented 5 years ago

@chocolatkey thanks for this additional context, it's a great illustration of how all these building blocks that we've been working on can be used together.

Over at J-Novel Club, I implemented the new manga reader (https://j-novel.club/mc/animeta-volume-1-chapter-1) using the WebPub spec, and was happy with the resulting API

Could you provide some additional details about this manga reader?

I decided to reuse some of the backend (golang) logic for generation of the fixed-layout EPUBs

The Golang streamer is quite a bit behind the other implementations right now, but we plan on working on it again this year and fully aligning with the JSON Schema for RWPM and the extensibility work that was recently done in Swift.

One use case that we've identified for the Golang codebase in addition to being a publication server & streamer, is the ability to generate samples/previews as well:

It seems that you've done something similar but not quite the same. If I understand things correctly:

When all the metadata is all ready and images have been uploaded, the EPUB is generated on the fly as it is downloading, which includes image conversion and of course, OPF generation.

When you say on the fly, do you mean that this is an asynchronous task that is triggered every time that the manifest or one of its components are updated?

Due to the database-less nature of the manga system, the metadata is stored on disk as webpub metadata during creation and editing. As the WebPub is JSON, and the typescript/javascript of r2 already has classes to deal with it, it is easy to have a web interface parse it, turn it into editable fields, and then post back to the server, which validates and stores it.

That's very interesting as well. Is the Go backend responsible for receiving the updated JSON and storing it on disk as well? Is it also responsible for the protection scheme implemented on top of the manifest for each image in the readingOrder?

For now, I've just changed their role to be that of an editor, since that also applies

If you can find another role that fits your use case, that's fine.

I would recommend using dc:contributor as the default for most roles and only use dc:creator for the main contributors as specified in the EPUB spec:

Secondary contributors SHOULD be represented using the contributor element.

chocolatkey commented 5 years ago

I'm sorry I accidentally closed the issue, I'm still writing my reply

chocolatkey commented 5 years ago

@HadrienGardeur

Could you provide some additional details about this manga reader? Is this a project that you plan on releasing separately under an open source license? Is it written in TypeScript? Are you using the r2 shared models for the reader as well?

Here: https://github.com/chocolatkey/xbreader is the reader. Open source. Under heavy development, and in my opinion lots that needs to be done to make it a comic reader I am happy with. The J-Novel Club version is actually behind the latest version, where I have now rewritten it in TypeScript (It's my first time using TS for a large project, so probably not the best TS). Yes I am using the r2 packages, you can see how here: https://github.com/chocolatkey/xbreader/blob/master/src/app/models/Publication.ts . Some of them I had to pull into my repo and make my own version of because of how I am using them, but many I am able to use directly. Being able to reuse the shared JS was definitely a motivation for converting to TS.

Do you implement strictly RWPM or its visual narrative profile as well?

I started the work on xbreader last july, and the webpub spec has definitely evolved since then. It first caught my interest in your repo here: https://github.com/HadrienGardeur/comics-manifest. I've kept up with things such as spine -> readingOrder, but I actually hadn't noticed the addition of the visual narrative profile. I will definitely implement that, because I also am supporting LTR, RTL, and vertical (webtoon) formats in xbreader as well as my comic publishing software (also under heavy development, and also uses webpubs and xbreader): https://github.com/chocolatkey/comicake/blob/master/reader/jsonld.py#L39 . It also seems to support multiple image heights, which I am also doing in xbreader/j-novel club/the backend, how convenient.

Is this a based approach to handling spreads?

Currently, xbreader has two modes, one for DRM-inclusive publications that uses canvases, and for DRM-free ones it uses img elements. I've done a ton of experimentation with what results in the smoothest experience, especially on mobile devices where high-res image slideshows in browsers can really lag. A lot of (for example) Japanese publishers get around this by lowering the resolution of images to x1024 or x1200 and compress to q=80 or so, which I find an annoying decision for users that want to enjoy comics in high quality. I can understand it a little though why they do it, because img starts to behave differently (worse) once your image approaches x2000 on weak devices. Canvas elements result in much less choppy animations, which is much preferred, but you reach the max memory usage quickly and the page will crash, or worse on safari/ios, apple decided to hardcode a calculated limit after which canvas drawing will start to fail (was a "fun" source of issues). So currently, the canvases that are more than 5 pages away from the current page are wiped and reset to fix this. In the future, however, I think I might do like I have seen multiple japanese readers do and have 5 or so canvas elements that I cycle through and load with new images. This will be useful especially with magazine-length publications. It's difficult, and I want to perfect this.

The Golang streamer is quite a bit behind the other implementations right now, but we plan on working on it again this year and fully aligning with the JSON Schema for RWPM and the extensibility work that was recently done in Swift.

To clarify, I am using none of the existing go source code, in part because the golang source, as you said, was/is lagging behind the latest spec vs. the swift and java/kotlin. I'm sure you'll get around to it though, it's probably not easy maintaining the spec in so many languages.

It seems that you've done something similar but not quite the same All resources are marked as encrypted in the manifest, is this purely access control (a bearer token) or do you implement additional security measures on top of it (for example, breaking down a page into multiple tiles)? etc. etc.

This is a somewhat complex and J-Novel-specific thing: They already had an existing API, and the manga component is completely separate at the moment (hopefully in the future this could all be integrated). Remember that the manga server has no database. And it's also important to understand there are two different pieces I am talking about. The first one is the chapter reader. Here's how it works: the series/chapter is loaded from the JNC API, as well as a client-side subscription check if the chapter is for subscribers only. No subscription -> sign in/register, else continue with a request (server-side subscription check for account). The client receives two tokens as a response, one for the dynamic javascript DRM module (see below answer) and one for the manga API. The token for the manga API is a signed JWT, and extends the JWT spec with additional fields:

type NGClaims struct {
    Scope string          `json:"scp,omitempty"` // Applicable URI scope of token
    Box   string          `json:"box,omitempty"` // NaCl Box
    Pub   *WebPubMetadata `json:"pub,omitempty"` // WebPub Metadata
}

type Claims struct {
    jwt.StandardClaims
    NGClaims
}

so (after base64 decoding) the payload looks like this:

{
  "exp": 1554401556,
  "iss": "https://api.j-novel.club/",
  "sub": "user id goes here",
  "aud": "127.0.0.1",
  "box": "JYJgE-d7QrTNjfD9OVsr-GqjJNZgDNQTt1Jcv4HtnhzOY-R-XazXJx3ZmMK9-vWntiIDpVc",
  "pub": {
    "title": "Chapter 1",
    "subtitle": "How a Realist Hero Rebuilt the Kingdom Manga Volume 1 Chapter 1",
    "identifier": "urn:uuid:2c369981-8085-4709-8fb3-8ab9eb7fd096",
    "issueNumber": 1,
    "readingProgression": "rtl",
    // etc....
}

The box field contains the the (encrypted for transmission) key used to seed the DRM algorithms. The pub field contains the webpub metadata object. This JWT is posted to the manga API server, validated (both through the golang structs and signing of the JWT), and used to generate the complete webpub. The identifier is used to find the appropriate storage location for the images, and the readingOrder is populated with the images. Image properties are inferred from the image properties, and a signed (JWT) URL is generated to allow access to this image. These URLs are public, to save on bandwidth costs by allowing caching through cloudflare, but also short-lived, the expiry is calculated like this:

// Provide a reasonable future deadline for expiry
func nextReasonableHour(from time.Time) time.Time {
    return from.Truncate(time.Hour).Add(time.Hour * 2)
}

The URLs looks like this: https://m11.j-novel.club/nebel/img/< token >/< image height >.jpg . The height is restricted to 1280, 1600 and 2048 currently to prevent abuse. The tokens here are also using the extended JWT spec:

{
    "exp": 1554408000,
    "sub": "2c369981-8085-4709-8fb3-8ab9eb7fd096",
    "scp": "Realist Hero - 01_fixed_c1_01.png",
    "box": "JYJgE-d7QrTNjfD9OVsr-GqjJNZgDNQTt1Jcv4HtnhzOY-R-XazXJx3ZmMK9-vWntiIDpVc"
}

where subject is the chapter/webpub uuid, scope is raw file, and box is the DRM seeder (encrypted). The first time an image is requested from the server, the source file is loaded, it is resized, and mixed up based on the (decrypted) seed: image A unique hash is derived based on various values, and the image is stored to disk and also served to the client. When the image is then requested again, the hash will match a file on the disk and serve it directly from there instead. Meanwhile, on the client, a small bit of dynamically obfuscated JS has been loaded using the other token. xbreader has an onDRM hook that serves to pass back a loaded image to a custom function for processing. The script exposes an event listener that a message is posted to containing the image metadata and the destination canvas, and the obfuscated script unmixes the image onto the canvas. This first version of the DRM is not too difficult to crack (in my opinion), and will probably be improved upon. DRM is a necessary evil enforced by some publishers (and needed to prevent posting of chapters to manga aggregator sites), but I like to see to it that it interfere minimally with the user's experience. In the future, if the manga API were able to fetch the data directly instead of being passed metadata, less API calls would need to be made and things would load faster, but for now, this is the best way to operate with their current API.

The second system, which I am currently creating, is the one I originally opened this issue for. This is the "publisher" system, which at the moment serves to allow staff to easily generate manga EPUBs from uploaded files and provided metadata. These EPUBs are DRM-free, as they are intended for upload to various stores, and potential DRM-free purchases. The way this works is that I have an interface that basically acts as a basic webpub metadata editor. It loads (or creates) the webpub from the server through the backend, and allows editing title, identifier, author, etc. A separate TOC is stored as well, since it doesn't seem like webpubs have good nav spec yet (I think?). In addition to this, a file uploader is used to upload images, which, just like with the reader, are stored simply in a folder. To keep things simple at the moment, the webpub metadata is simply POSTed to the server, which thanks to the convenience of golang structs, can just load it (to prevent non-spec-conforming data) and then save to the system as a json file. In a more complex application, maybe a document database like mongodb would be suited for storage of this. Once it is time to download, a request is simply made to an endpoint passing in the height: /epub?height=3840, which starts a download. This download is streamed in the sense that the Content-Type and Content-Disposition headers are sent, and then a zip.Writer proceeds to write to the http response as the EPUB is assembled. First the mimetype and META-INF contents, then the OPF, which is populated by the webpub metadata, then the nav/ncx, and finally, the original PNG images are loaded and converted using vips like this:

// Write page image
fw, err = epubFile.Create(imagePath)
if err != nil {
    return fmt.Errorf("failed creating file in ZIP: %q", imagePath)
}

ix, _ := vips.NewImageFromFile(filepath.Join(basepath, info.Name))
defer ix.Close()
// println(path, ix.Bands(), ix.Interpretation())

var quality int

if ix.Bands() > 2 { // Is a color image
    quality = 93 // Causes vips to respect a magick decision to disable subsampling because q>90. Not to mention it's nicer
} else { // Grayscale image
    quality = 90
}

_, _, err = vips.NewTransform().
    Image(ix).
    BackgroundColor(vips.Color{255, 255, 255}). // White background on transparent parts of the image
    ResizeHeight(int(destHeight)).              // Uses Lanczos3 kernel by default
    Format(vips.ImageTypeJPEG).                 // Convert to JPEG
    Quality(quality).                           // JPEG Quality
    Interpretation(ix.Interpretation()).        // Set the interpretation to the original!
    StripProfile().StripMetadata().             // Strip all potential meta
    Output(fw).
    Apply()

if err != nil {
    return fmt.Errorf("failed writing file to ZIP: %q\n", info.Name)
}

Once all files are written, the zip is closed and it's on the user machine. Of course if this were later exposed to users, the files could be cached so this regeneration doesn't have to happen each time, but for staff, this is far better as new versions can be instantly generated without waiting for any backgroud work to happen (or fail!).

I would recommend using dc:contributor as the default for most roles and only use dc:creator for the main contributors as specified in the EPUB spec

Thanks!


This was probably more info than you wanted, but might as well answer with detail. If you need/want it, any of the backend is fine with open source, except the DRM algorithms (kind of like the LCP idea).

HadrienGardeur commented 5 years ago

@chocolatkey

Here: https://github.com/chocolatkey/xbreader is the reader. Open source. Under heavy development, and in my opinion lots that needs to be done to make it a comic reader I am happy with. The J-Novel Club version is actually behind the latest version, where I have now rewritten it in TypeScript

That's great news. We're considering building an "awesome-readium" repo and we would definitely highlight your project in there.

Yes I am using the r2 packages, you can see how here: https://github.com/chocolatkey/xbreader/blob/master/src/app/models/Publication.ts . Some of them I had to pull into my repo and make my own version of because of how I am using them, but many I am able to use directly. Being able to reuse the shared JS was definitely a motivation for converting to TS.

Do you mind listing some of the cases that required making your own version of them?

Our goal is to have full extensibility in our models moving forward to avoid this requirement. I've looked at your manifest output and I think all of it would be covered by the extensibility as implemented in Swift.

Some feedback on your use of the manifest by the way:

I started the work on xbreader last july, and the webpub spec has definitely evolved since then. It first caught my interest in your repo here: https://github.com/HadrienGardeur/comics-manifest.

I need to deprecate that repo and point back to the RWPM profile, I'll do something about that today.

I actually hadn't noticed the addition of the visual narrative profile

Quick summary of the additions:

If you feel that key metadata for describing comics/manga are missing, I'm all ears. We can easily map to existing schema.org metadata for example.

So currently, the canvases that are more than 5 pages away from the current page are wiped and reset to fix this. In the future, however, I think I might do like I have seen multiple japanese readers do and have 5 or so canvas elements that I cycle through and load with new images. This will be useful especially with magazine-length publications. It's difficult, and I want to perfect this.

This sounds very similar to the approach that we're using elsewhere. For example on iOS we have three webviews in a scrollview to also preload the adjacent resources (next and previous) and we recycle them as we move forward/backward in the publication.

Evident Point is working on something similar with multiple iframes for a Web App as well and they adopted a fully flexible option where the implementer can decide how many iframes should be used by the navigator.

We're also working on navigator APIs right now and any feedback is welcome.

A separate TOC is stored as well, since it doesn't seem like webpubs have good nav spec yet (I think?)

Initially core RWPM had a way to indicate that a resource is a TOC, while the EPUB extension allowed the definition of a TOC in the manifest directly.

This was moved into the core spec, which means that you can do it all in the manifest now. Here's a link to the relevant section in the specification and here's an example for an audiobook.

This was probably more info than you wanted, but might as well answer with detail. If you need/want it, any of the backend is fine with open source, except the DRM algorithms (kind of like the LCP idea).

On the contrary, I find this all very useful and interesting. I'll reply to the rest of it in a separate comment.

chocolatkey commented 5 years ago

@HadrienGardeur I will reply to the rest in a separate comment once I have applied the fixes you have suggested and converting things. I am looking through the visual narrative profile, and it reminds me of what I have seen quite a few Japanese companies are doing for comic reading, turning their EPUBs into the images + a JSON file in an archive, or adding the JSON in the EPUB to speed up parsing.

I do have a couple of questions as I read through https://readium.org/webpub-manifest/extensions/visual-narrative#5-package : "As an alternative, the manifest can also be added to a CBZ file at the same well-known location." - Does this mean a manifest.json in the root of the zip (another "well known" location for metadata I think of when talking about CBZ is in the ZIP comment section)? And why was it decided that "encrypted using a DRM must use a different media type and file extension"? The reason I ask is that the examples for a normal webpub here https://github.com/readium/webpub-manifest/blob/master/extensions/epub.md#encrypted show that the filename and mimetype remains the same, and I have been doing so as well. - Obviously so existing CBZ readers don't try and read the encrypted files

HadrienGardeur commented 5 years ago

@chocolatkey

Does this mean a manifest.json in the root of the zip (another "well known" location for metadata I think of when talking about CBZ is in the ZIP comment section)?

Yes, manifest.json directly at the root of the CBZ.

Obviously so existing CBZ readers don't try and read the encrypted files

Exactly, this is a major drawback in EPUB where every single publication is identified with the normal EPUB extension and media type but only some of them can be opened (plus there's no way to provide a hint about the DRM being used). Apple, Google and Kobo have their own proprietary DRM that isn't supported anywhere else than their apps.

HadrienGardeur commented 5 years ago

@chocolatkey

Some overall context about protection first:

The token for the manga API is a signed JWT, and extends the JWT spec

JWT is a very good fit for that and re-using portions of RWPM in your JWT feels like a smart move as well.

The box field contains the the (encrypted for transmission) key used to seed the DRM algorithms. The pub field contains the webpub metadata object.

Have you thought about encrypting the JWT rather than signing it? This way you wouldn't necessarily have to encrypt the key itself.

The URLs looks like this: https://m11.j-novel.club/nebel/img/< token >/< image height >.jpg . The height is restricted to 1280, 1600 and 2048 currently to prevent abuse.

I haven't seen these variations on height in your manifest. You could expose this information directly using alternate.

I'm trying to summarize your approach (tell me if anything is wrong):

Is it fair to say that access control is the primary method used for protecting content, with the exception of images who have a different set of requirements:

I'm interested in building such a list of requirements and eventually creating an equivalent of the LCP certification, which is why I'd like to get a better understanding.

chocolatkey commented 5 years ago

@HadrienGardeur

I'm interested in building such a list of requirements and eventually creating an equivalent of the LCP certification, which is why I'd like to get a better understanding.

Some (long) advice:

A lot of my decisions/opinions regarding content protection, or so-called DRM, is based on my greyhat cracking of various EBook protection schemes (I have tried reporting these, though it is oftentimes impossible to get in touch with large companies, especially when you don't speak the language).


The first and foremost priority is making it impossible for users to access content they are not authorized to access, no matter whether that content is encrypted, obfuscated, or otherwise thought to be "secure". This is by far the most prevalent weakness in ebook systems. Storing assets (in any form) in such a way that they are publicly accessible by default (no signed URLs) is OK, especially if you are using some sort of CDN. I personally discourage it for additional security, but a permanent home at cdn.com/xxxxxxxxxxxxxxx/mypage.jpg allows for cost savings, because signed URLs cannot be cached unless you are using cloudfront/akamai/cloudflare business (which can cost quite a bit for media-rich applications such as ebooks). What is unacceptable though, is storing assets at a public URL that can be guessed or otherwise brute-forced. Often times, I have seen systems where publications are stored in folders that are simply named as incrementing IDs, referring to the ID in their database, so URLs look like this: cdn.com/123/mypage.jpg. This makes it very easy to crawl and rip publications, as well as accessing unreleased publications, which is even worse, and there are still quite a few ebook services that have this kind of scheme. The first line of "defense" should be an long, unguessable path. This can be in the form of a random UUID (e.g. c2ac564a-f7d6-42be-9b61-3062d1e47fb6), or a hash derived from random or unguessable components (e.g. 5eb63bbbe01eeed093cb22bb8f5acdc3). A path such as cdn.com/3nf2b/mypage.jpg is not secure, it can be brute forced with a simple script. I'd recommend 16+ chars, to ensure it can never, ever be crawled/iterated through over the internet.

P.S.: Some stores do this properly, with unguesssable URLs, but then put the public covers in that same directory, thus revealing the path...


All of the above should apply to any ebook service, even if they are DRM-free. There are still stores out there that do not do this, and expose their free and unpublished content to the world. Even if they do actually encrypt the exposed content, oftentimes thumbnails are available, or they fail to properly encrypt/obfuscate their files anyway, which leads me to a second point: If you choose to encrypt/obfuscate the content, use at least one well-known, vetted, secure encryption algorithm on top of your proprietary obfuscation. Believe it or not, there are stores out there that still use RC4, and while RC4 still may hold up against brute-forcing, it opens the door to a lot of interesting attacks to get the content I have performed, which I will not dive in to. Whether it be xchacha20, AES-128/256 in a proper mode, or some other good algorithm, it's 2019, and there are many secure encryptions libraries available. Even some of the weaker ones are better than what I have seen out there. A lot (really a lot) of programmers seem to think that if they hide a master decryption key (for all content, yes this exists) well enough in their code, no one will find it. They put it somewhere, with a fancy XOR function, or "hide" it in a native library in their app, and think that's secure. It's not. Motivated individuals with time on their hands, such as I have had, will find it. Speaking of ebook app security, there's a lot that could be improved there, but I won't go in to that yet.

P.S.: Stores that have a trial reading feature sometimes encrypt their trial content with the same key as the full content... Do not do this, and do not place trial content in the same folder as the full content or make it accessible with the same authorization.


From what I understand of LCP, they do get the encryption part right. I know that past a certain point it's futile, and they're trying to keep their encryption scheme "clean" vs. being too obfuscated and confusing, but I don't think that their way of (from what I understand) adding a small obfuscation layer on top of the AES is going to hold out for long. The native libraries of the apps can be extracted and the obfuscation technique reversed. Correct me if I'm wrong, but as I understand it each member of the LCP will get supplied with that additional code they pop in to the r2 systems and fill in the blank space in the code I have seen. Their app is then reviewed and approved to be "secure". I don't think this is sufficient. I won't get too deep into this, because this is sort of off-topic, but another weakness I have seen in the development of ebook reading apps/programs is this: As time passes, the app code is increasingly obfuscated, and more anti-tampering detection is added. All the while, previous versions of the app (e.g. android APKs) can be acquired, and they can be reverse-engineered instead. These old versions still work because 1. The developers increased the obfuscation of the app but did not actually change the core content obfuscation algorithms, 2. They old versions of the app still work so users aren't pestered to immediately update, since many won't immediately update or move (sometimes their device prevents that) or 3. The developers just swap a few values around in the core algorithms, and it's not difficult to figure out by combining info from the old and new versions. These old versions use an old API, so you don't have to crack the new one. I'm actually working on a possible solution to that which involves the app acquiring and updating the obfuscation-specific code separately from the app download, but that's a whole other tangent.


The final point regarding "securing" of content: If obfuscation is to be used, it should be irritating/demotivating for crackers to crack. This means making it a tedious, long, boring process, and reducing the parts that can be automated/scripted, or parts where freely available code can be reused to make the DRM obsolete. An example of how I do this is to make the javascript responsible for unmixing the images dynamically generated and ephemeral. You can't save the script and reuse it for all publications (thus making it an invisible layer of obfuscation) or (painstakingly) reverse-engineer it and use that knowledge to unmix every publication (since the keys are embedded in the script as well). At the same time though, parts of the APIs that do not have to do with DRM I think should not be obfuscated. While it could help short-term to obfuscate the webpub and make it unreadable, it's not good in the long run for protection, and makes development of your client, as well as third-party and open source integrations painful as well, and could incur losses in loading speed. DRM should not cause any significant slow-down, we all know how that went for Denuvo. The final, and best way (if even possible/allowed by the publisher) to demotivate cracking is to provide eventual DRM-free downloads. On JNC for example, manga are localized chapter-by-chapter for online reading, but when eventually sold as EPUBs, they are DRM free. Once the EPUBs are released to stores such as Amazon, Rakuten, or others, it is impossible to prevent the removal of DRM anyway, considering there are open-source DRM removal libraries for the popular bookstores.

I hope those answers provided you with sufficient reading material for now ;)


Have you thought about encrypting the JWT rather than signing it? This way you wouldn't necessarily have to encrypt the key itself.

Why though? Yet another reason I chose to use JWTs is the simplicity when it comes to debugging/troubleshooting. If I am running in to problems with the JWTs, or another developer is, it is easy to base64 decode them, or use a site such as https://jwt.io/ without having to keep the encryption keys for the JWTs (I assume you mean using the JOSE standards) handy, which would encourage keeping keys easily accessible, not a good idea. I am of the opinion that only the parts of the JWT that need to be encrypted should be. No sense in hiding the expiration time, subject, issuer or other public data like that, I could even decode it client-side if needed to check for expiration. The obfuscation seeding key is encrypted with the NaCl secretbox functions. I do not pretend to be a cryptographer, so using secretbox appeals to me as it is "designed to meet the standard notions of privacy and authenticity for a secret-key authenticated-encryption scheme using nonces". I may not understand fully how those algorithms work, but I can rest assured that my implementation is not flawed (If this were personal user data I might go to further lengths to verify the security of those algorithms, but this is an obfuscation seed for e-publications).

HadrienGardeur commented 5 years ago

A lot of my decisions/opinions regarding content protection, or so-called DRM, is based on my greyhat cracking of various EBook protection schemes (I have tried reporting these, though it is oftentimes impossible to get in touch with large companies, especially when you don't speak the language).

That's always a useful perspective to have IMO.

The first and foremost priority is making it impossible for users to access content they are not authorized to access, no matter whether that content is encrypted, obfuscated, or otherwise thought to be "secure".

Yes, that's usually handled through a mix of access control (to the manifest for example) and short-lived URLs (for the resources).

Often times, I have seen systems where publications are stored in folders that are simply named as incrementing IDs

That's a second principle: it shouldn't be possible to guess the URLs of the resources in the readingOrder, especially if you don't have access to the manifest (which is behind access control). Once a manifest has been exchanged though, the only way to keep the URLs of the resources "protected" is to require access control all over again (for example through a session or a Bearer Token).

[...] signed URLs cannot be cached unless you are using cloudfront/akamai/cloudflare business (which can cost quite a bit for media-rich applications such as ebooks)

Most CDN services have the ability to secure content using secure URLs which are essentially the equivalent of a JWT with an expiration time. But you're right that a CDN to serve high resolution images can be costly, but this seems to be a requirement for any good commercial service anyway.

The native libraries of the apps can be extracted and the obfuscation technique reversed.

IMO that's true for any DRM scheme or obfuscation technique, but for "traditional DRMs" that need to work outside a fully integrated online environment, that's the only solution. All DRM schemes rely on some secret being hidden in the code somewhere, most of the time to initialize an exchange that will result in obtaining the real key necessary to decrypt the content.

As time passes, the app code is increasingly obfuscated, and more anti-tampering detection is added. All the while, previous versions of the app (e.g. android APKs) can be acquired, and they can be reverse-engineered instead. These old versions still work because 1. The developers increased the obfuscation of the app but did not actually change the core content obfuscation algorithms, 2. They old versions of the app still work so users aren't pestered to immediately update, since many won't immediately update or move (sometimes their device prevents that) or 3. The developers just swap a few values around in the core algorithms, and it's not difficult to figure out by combining info from the old and new versions

That's true, it's impractical to update a DRM to a new scheme and even for the largest companies it's almost impossible to do it at all.

I'm actually working on a possible solution to that which involves the app acquiring and updating the obfuscation-specific code separately from the app download, but that's a whole other tangent.

It's been done before and two important points to note about that:

The final point regarding "securing" of content: If obfuscation is to be used, it should be irritating/demotivating for crackers to crack. This means making it a tedious, long, boring process, and reducing the parts that can be automated/scripted, or parts where freely available code can be reused to make the DRM obsolete.

I think that's a good principle but it's hard to apply it.

An example of how I do this is to make the javascript responsible for unmixing the images dynamically generated and ephemeral.

This could work in a fully integrated environment, but the end goal with the Readium Web Publication Manifest is to make it possible to any Web App to read any Readium Web Publication Manifest. We could define an API to exchange this obfuscated JS but the API itself would then become the weak point in the system and that's where things would eventually break.

At the same time though, parts of the APIs that do not have to do with DRM I think should not be obfuscated. While it could help short-term to obfuscate the webpub and make it unreadable, it's not good in the long run for protection, and makes development of your client, as well as third-party and open source integrations painful as well, and could incur losses in loading speed. DRM should not cause any significant slow-down, we all know how that went for Denuvo.

As explained above, I think it goes even beyond that. I'd like to see an ecosystem of certified Web Apps similar to the ecosystem of certified native apps/devices for LCP.

I am of the opinion that only the parts of the JWT that need to be encrypted should be. No sense in hiding the expiration time, subject, issuer or other public data like that, I could even decode it client-side if needed to check for expiration

That's a fair point and I was indeed referencing JOSE.

chocolatkey commented 5 years ago

@HadrienGardeur I wanted to double-check about the addition of schema: to the non-webpub attributes: Is that all I have to do? No telling it what the namespace is or something, just change it do be like this:

{
"schema:issueNumber": 123
}

?

Also regarding the original issue in question, I found MARC code tyg works well for letterers.

HadrienGardeur commented 5 years ago

@chocolatkey

There's no "namespace" really, this is all handled in the JSON-LD context document. As long as you reference the default context, everything will work fine.

I would recommend using the JSON-LD Playground to test your output, this will show you the RDF triples that will eventually be generated.

HadrienGardeur commented 5 years ago

While we've had many interesting discussions in this issue, I think that the main issue has been adressed.

I'm closing it now.