Kindle Touch ACX

From MobileRead
Jump to: navigation, search

ACX files are HTML/JS/CSS-based plugins for Kindle Touch native reader with somewhat limited capabilities. ACX is standing for Active Content Extensions. They are provided by 5.x firmware.

ACX files are, in fact, W3C Widgets.

Widget specifications (another view of specs list) are developed by W3C Web Applications Working Group (see also Webapps Working Group Wiki)

Contents

[edit] Locations

(Checked on KT 5.3.2)

Stock ACX are stored in /opt/amazon/acw . They are copied to working location /mnt/us/system/acw by library /usr/lib/ccat/libmesquiteE.so .

To install homebrew ACX, just copy it to /mnt/us/system/acw. After a couple of seconds, it will be automatically registered in system (if it's valid signed widget).

To deinstall homebrew ACX, just delete it from /mnt/us/system/acw.

Public key for validating stock ACX is stored in /opt/amazon/ebook/security/keystore_mesquite.xml with name AmazonActiveContentExtensions.

[edit] Signing

Specification, tutorial, another tutorial, widgetsigner software.

Amazon uses 1024-bit DSA key for signing stock ACX. It's not possible to take theirs private key and sign homebrew ACX, so it's necessary to make our own keypair, implant public key into Kindle's keystore and then sign ACX with our key.

[edit] Prerequisites

For key generation: get OpenSSL and Python 3

For signing: clone/download widgetsigner repository and get Ant and Java for compiling widgetsigner.

[edit] Key generation

Following aforementioned tutorial and http://www.openssl.org/docs/HOWTO/keys.txt:

[edit] Generate DSA key

Generate 1024-bit DSA key without encryption. Without encryption there will be no requirement to enter password in operations with key. It's convenient.

$ openssl dsaparam -out dsaparam.pem 1024
$ openssl gendsa -out author.key.pem dsaparam.pem

dsa_param.pem could be safely deleted after key generation. Only author.key.pem is necessary for next steps.

[edit] Generate self-signed certificate

Make certificate valid for 111 years (40515 days). This number has no special meaning, it's just an arbitrary value.

$ openssl req -batch -new -x509 -days 40515 \
         -key author.key.pem \
         -out author.cert.pem \
         -subj "/CN=GeneralHomebrewAuthor/C=AQ/O=HomebrewAuthors/OU=HomebrewACXDevelopment"

[edit] Make PKCS#12 file out of certificate and key

Again, no password for made file. Just for convenience.

Value of name parameter could be selected at will.

$ openssl pkcs12 -export \
  -passout pass: \
  -in author.cert.pem -inkey author.key.pem \
  -name "HomebrewActiveContentExtensions" \
  -out author.p12

[edit] Create XML signature snippet for implanting in Kindle's keystore

Use following Python 3 script (was tested with Python 3.3) for printing snippet. Save printed snippet somewhere.

If different name was selected for PKCS#12 file in previous step, pass it to script with -k parameter.

#!/usr/bin/env python3

"""
Read (from stdin) text output of OpenSSL for DSA private key
and print corresponding XML Signature snippet.

Example usage:

    $ openssl dsa -text -noout < dsa.private.key.pem 2>/dev/null \
      | python3 print_xml_dsig_for_dsa_key_params.py

Script parses following output:

    $ openssl dsa -text -noout < dsa.private.key.pem
    read DSA key
    Private-Key: (1024 bit)
    priv:
        01:a1:<...>
        <...>
    pub:
        11:a2:<...>
        <...>
    P:  
        22:b1:<...>
        <...>
    Q:  
        a1:23:<...>
        <...>
    G:  
        bb:ad:<...>
        <...>
"""


import argparse
import base64
import itertools
import math
import operator
import re
import sys

PARAM_NAME_MAPPING = {
  'pub:' : 'y',
  'P:'   : 'p',
  'Q:'   : 'q',
  'G:'   : 'g',
}

PARAM_VALUE_PART_RE = re.compile('^\s{4}((?:[0-9a-f]{2}:?)+)$', re.IGNORECASE)

XML_DSIG_DSA_KEY_SNIPPET_TPL = """
<dsig:KeyInfo>
    <dsig:KeyName>{name}</dsig:KeyName>
    <dsig:KeyValue>
        <dsig:DSAKeyValue>
            <dsig:P>{p}</dsig:P>
            <dsig:Q>{q}</dsig:Q>
            <dsig:G>{g}</dsig:G>
            <dsig:Y>{y}</dsig:Y>
        </dsig:DSAKeyValue>
    </dsig:KeyValue>
</dsig:KeyInfo>
"""
.strip()

def collect_text_dsa_key_params(openssl_stdout):
    """Build a dictionary of DSA key parameters found in OpenSSL output."""
    params = dict(zip(PARAM_NAME_MAPPING.values(), itertools.repeat('')))
    param_name = None
    for line in openssl_stdout:
        param_value_part = PARAM_VALUE_PART_RE.match(line)
        if param_value_part and param_name is not None:
            params[param_name] += param_value_part.group(1).replace(':', '')
        else:
            param_name = PARAM_NAME_MAPPING.get(line.strip())
    return params

def convert_to_cryptobinary(bignum):
    """http://www.w3.org/TR/xmldsig-core/#sec-CryptoBinary

    The integer value is first converted to a "big endian" bitstring. The
    bitstring is then padded with leading zero bits so that the total number
    of bits == 0 mod 8 (so that there are an integral number of octets). If
    the bitstring contains entire leading octets that are zero, these are
    removed (so the high-order octet is always non-zero). This octet string is
    then base64 encoded.
    """

    byte_length = math.ceil(bignum.bit_length() / 8)
    bitstring = bignum.to_bytes(byte_length, byteorder='big')
    return base64.b64encode(bitstring).decode('ascii')

def xml_dsig_snippet(key_name, dsa_key_params):
    """Return XML signature snippet filled with provided values."""
    params = {k: convert_to_cryptobinary(v) for k, v in dsa_key_params.items()}
    return XML_DSIG_DSA_KEY_SNIPPET_TPL.format(name=key_name, **params)

def error(description, dsa_key_params):
    """Build error message."""
    message = ['ERROR: {}!'.format(description)]
    message.append('Collected parameters:')
    for name, value in dsa_key_params.items():
        message.append("  {}: {}".format(name, value or '<not found>'))
    return '\n'.join(message)

# Parse CLI arguments.
cli = argparse.ArgumentParser()
cli.add_argument('-k', '--keyname', type=str,
                 help='KeyName of XML Signature KeyInfo',
                 default='HomebrewActiveContentExtensions')
args = cli.parse_args()

# Collect DSA key parameters from stdin and print corresponding dsig:KeyInfo.
text_dsa_key_params = collect_text_dsa_key_params(sys.stdin)
if any(map(operator.not_, text_dsa_key_params.values())):
    print(error("some DSA key parameters weren't found", text_dsa_key_params))
else:
    dsa_key_params = {k: int(v, 16) for k, v in text_dsa_key_params.items()}
    print(xml_dsig_snippet(args.keyname, dsa_key_params))

[edit] Making widgetsigner

Widgetsigner needs to be patched. By default, it inserts X509 certificate information in KeyInfo section in signature file. However, Kindle's mesquite subsystem expects KeyName and KeyValue in KeyInfo section.

Here is the patch (for a commit d6322de):

diff --git a/src/org/meshpoint/widgetsigner/WidgetSigner.java b/src/org/meshpoint/widgetsigner/WidgetSigner.java
index dd450dd..3a6f4b0 100755
--- a/src/org/meshpoint/widgetsigner/WidgetSigner.java
+++ b/src/org/meshpoint/widgetsigner/WidgetSigner.java
@@ -12,6 +12,7 @@ import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.security.PrivateKey;
 import java.security.Provider;
+import java.security.PublicKey;
 import java.security.Security;
 import java.security.UnrecoverableKeyException;
 import java.security.cert.CRLException;
@@ -43,7 +44,10 @@ import org.apache.xml.security.c14n.CanonicalizationException;
 import org.apache.xml.security.c14n.Canonicalizer;
 import org.apache.xml.security.c14n.InvalidCanonicalizerException;
 import org.apache.xml.security.exceptions.XMLSecurityException;
+import org.apache.xml.security.keys.content.KeyName;
+import org.apache.xml.security.keys.content.KeyValue;
 import org.apache.xml.security.keys.content.X509Data;
+import org.apache.xml.security.keys.content.keyvalues.DSAKeyValue;
 import org.apache.xml.security.signature.ObjectContainer;
 import org.apache.xml.security.signature.SignatureProperties;
 import org.apache.xml.security.signature.SignatureProperty;
@@ -281,7 +285,7 @@ public final class WidgetSigner {
                        }
                }
 
-               int ret = createSignatureXML(widgetPath, signType, created, identifier, imeis, meids);
+               int ret = createSignatureXML(widgetPath, signType, created, identifier, imeis, meids, alias);
                return ret;
        }
 
@@ -383,7 +387,7 @@ public final class WidgetSigner {
                return pk;
        }
 
-       private int createSignatureXML(String widgetPath, int signType, boolean created, String identifier, String[] imeis, String[] meids) {
+       private int createSignatureXML(String widgetPath, int signType, boolean created, String identifier, String[] imeis, String[] meids, String alias) {
                String signFile = null;
                File file = new File(widgetPath);
                String signFileName = (signType == AUTHOR_SIGNATURE) ? AUTHOR_SIGNATURE_FILE :
@@ -474,24 +478,8 @@ public final class WidgetSigner {
                        return XMLSIGNATUREEXCEPTION_THROWN;
                }
 
-               try {
-                       X509Data x509Data = new X509Data(doc);
-                       X509Certificate[] certChain = getCertificateChain(cert);
-                       for(X509Certificate c : certChain){
-                               x509Data.addCertificate(c);
-                       }
-                       if(crl != null) {
-                               x509Data.addCRL(crl.getEncoded());
-                       }
-                       xmlsig.getKeyInfo().add(x509Data);
-               } catch (XMLSecurityException e) {
-                       e.printStackTrace();
-                       error = e.getMessage();
-                       return XMLSIGNATUREEXCEPTION_THROWN;
-               } catch (CRLException e) {
-                       error = e.getMessage();
-                       return XMLSIGNATUREEXCEPTION_THROWN;
-               }
+               xmlsig.getKeyInfo().add(new KeyName(doc, alias));
+               xmlsig.getKeyInfo().add(new KeyValue(doc, new DSAKeyValue(doc, cert.getPublicKey())));
 
                try {
                        xmlsig.sign(privatekey);

After patching, issue ant command from root of widgetsigner directory. Compiled tool will be available as out/widgetsigner.jar

[edit] Add key to Kindle

MAKE BACKUPS OF ALL CHANGED FILES!!

[edit] Implant XML Signature snippet into keystore

Just paste snippet into /opt/amazon/ebook/security/mesquite_keystore.xml

[edit] Allow access to all widget features for ACX signed with generated key

Edit /opt/amazon/ebook/security/defaultPolicies.json

Duplicate section with key AmazonActiveContentExtensions (copy/paste), then change key name in new section from AmazonActiveContentExtensions to name defined in generation of PKCS#12 file. Make sure changed JSON is still valid by using http://jsonlint.com/, https://jsonformatter-online.com or something similar.

[edit] How to sign

java -jar widgetsigner.jar -w homebrew.acx -s 0 -k author.p12 -a 'HomebrewActiveContentExtensions' -p ''
-w 
path to widget (should be a zip file)
-s 0
create author-signature.xml
-k
path to PKCS#12 file
-a
name of certificate to use from PKCS#12 file
-p
password for certificate (use empty string when password isn't required)

ACX file will be edited in-place (i.e. author-signatire.xml will be added to passed zip file).

[edit] Simple unsigned ACX to test signing

Download Media:Example-acx.zip and change its extension to acx. It's a simple ACX displayed as Homebrew ACX in ACX menu. It works and just shows window with text "It works!".

Try to sign it and install to check correctness of signing.

[edit] How to create a Google Translator ACX for Paperwhite 2

[edit] 1. Download Google Translate PHP proxy and find a hosting for it

Google Translate PHP proxy

Mark down the URI of the proxy.

Let's say it's http://myphphosting.site/GoogleTranslate.php

[edit] 2. Get Amazon Translator ACX from your Kindle

ACX location: [userstore]/system/acw/stock-translator-<datetime>.acx or /opt/amazon/acw/translator.acx

[edit] 3. Unpack it to a working dir

unzip translator.acx

[edit] 4. Use jsbeautifier to make some of the JavaScript files more readable

pip install jsbeautifier
mv -f script/translator.js script/translator.js.ugly
js-beautify script/translator.js.ugly > script/translator.js
rm -f script/translator.js.ugly
mv -f script/strings.js script/strings.js.ugly
js-beautify script/strings.js.ugly > script/strings.js
rm -f script/strings.js.ugly

If you happen to use en_GB locale (or any other, just replace en-gb with appropriate id):

mv -f locales/en-gb/strings.js locales/en-gb/strings.js.ugly
js-beautify locales/en-gb/strings.js.ugly > locales/en-gb/strings.js
rm -f locales/en-gb/strings.js.ugly

[edit] 5. Optionally change the ACX name in config.xml

You don't need to if you want to replace the Amazon Translator ACX.

diff -ur a/translator.acx/config.xml b/translator.acx/config.xml
--- a/translator.acx/config.xml 2014-02-13 12:02:38.000000000 +0100
+++ b/translator.acx/config.xml 2014-02-13 08:20:36.000000000 +0100
@@ -2,21 +2,21 @@

 <widget xmlns       = "http://www.w3.org/ns/widgets"
         version     = "1.8.12"
-        id          = "http://kindle.amazon.com/ns/widgets/translation"
+        id          = "http://kindle.amazon.com/ns/widgets/gtranslation"
         viewmodes   = "windowed"
         xmlns:kindle  = "http://kindle.amazon.com"
         xmlns:ui    = "http://kindle.amazon.com/ns/ui">

-    <name short="Translation">Translation</name>
-    <name short="翻译" xml:lang="zh-cn">&#32763;&#35793;</name>
-    <name short="Übersetzung" xml:lang="de">&#220;bersetzung</name>
-    <name short="Traduzione" xml:lang="it">Traduzione</name>
-    <name short="Traducción" xml:lang="es">Traducci&#243;n</name>
-    <name short="Translation" xml:lang="en-gb">Translation</name>
-    <name short="Traduction" xml:lang="fr">Traduction</name>
-    <name short="Перевод" xml:lang="ru">&#1055;&#1077;&#1088;&#1077;&#1074;&#1086;&#1076;</name>
-    <name short="翻訳" xml:lang="ja">&#32763;&#35379;</name>
-    <name short="Tradução" xml:lang="pt">Tradu&#231;&#227;o</name>
+    <name short="Google Translation">Google Translation</name>
+    <name short="Google 翻译" xml:lang="zh-cn">Google &#32763;&#35793;</name>
+    <name short="Google Übersetzung" xml:lang="de">Google &#220;bersetzung</name>
+    <name short="Google Traduzione" xml:lang="it">Google Traduzione</name>
+    <name short="Google Traducción" xml:lang="es">Google Traducci&#243;n</name>
+    <name short="Google Translation" xml:lang="en-gb">Google Translation</name>
+    <name short="Google Traduction" xml:lang="fr">Google Traduction</name>
+    <name short="Google Перевод" xml:lang="ru">Google &#1055;&#1077;&#1088;&#1077;&#1074;&#1086;&#1076;</name>
+    <name short="Google 翻訳" xml:lang="ja">Google &#32763;&#35379;</name>
+    <name short="Google Tradução" xml:lang="pt">Google Tradu&#231;&#227;o</name>

     <feature name="http://kindle.amazon.com/features/reader" required="true"></feature>
     <feature name="http://kindle.amazon.com/features/DeviceEventTracking" required="true">

[edit] 6. Allow ACX to access your PHP hosting by patching config.xml

diff -ur a/translator.acx/config.xml b/translator.acx/config.xml
--- a/translator.acx/config.xml 2014-02-12 09:33:29.690302228 +0100
+++ b/translator.acx/config.xml 2014-02-11 15:00:49.000000000 +0100
@@ -51,6 +51,7 @@

     <access origin="https://translate-acx.amazon.com" subdomains="false" kindle:authentication="token" />
     <access origin="https://translate-acx.amazon.co.uk" subdomains="false" kindle:authentication="token" />
+    <access origin="http://myphphosting.site" subdomains="false" kindle:authentication="token" />

     <ui:uicontext name="selection" view="content" priority="1000" />

[edit] 7. Replace Amazon Translator service URI with your Google Translate PHP proxy URI

diff -ur a/translator.acx/script/translator.js b/translator.acx/script/translator.js
--- a/translator.acx/script/translator.js       2014-02-12 09:45:56.636731547 +0100
+++ b/translator.acx/script/translator.js       2014-02-12 09:32:18.000000000 +0100
@@ -252,8 +252,8 @@
     this.previousQuery = null;
     var _this = this;
     var _stringLoader = initializer.stringLoader;
-    var _serviceUrlUK = "https://translate-acx.amazon.co.uk/V2/translate";
-    var _serviceUrl = "https://translate-acx.amazon.com/V2/translate";
+    var _serviceUrlUK = "http://myphphosting.site/GoogleTranslate.php";
+    var _serviceUrl = "http://myphphosting.site/GoogleTranslate.php";
     var _targetLanguageStorePrefix = "amazon.acx.translator.toLang.";
     var _defaultItemSuffix = "default_no_id";
     var _localStorage = initializer.localStorage || kindle.localStorage;

[edit] 8. Add languages of your choice to the list

diff -ur a/translator.acx/script/strings.js b/translator.acx/script/strings.js
--- a/translator.acx/script/strings.js  2014-02-12 09:53:26.547106894 +0100
+++ b/translator.acx/script/strings.js  2014-02-12 09:32:43.000000000 +0100
@@ -27,6 +27,9 @@
     lang_no: "Norwegian",
     lang_hi: "Hindi",
     lang_da: "Danish",
+    lang_cs: "Czech",
+    lang_sk: "Slovak",
+    lang_pl: "Polish",
     turn_off_airplane_mode: "Turn off Airplane Mode",
     airplane_mode_message: "Airplane Mode must be turned off to use this feature.",
     turn_on_wireless: "Turn on Wireless",
diff -ur a/translator.acx/locales/en-gb/strings.js b/translator.acx/locales/en-gb/strings.js
--- a/translator.acx/locales/en-gb/strings.js  2014-02-12 09:53:26.547106894 +0100
+++ b/translator.acx/locales/en-gb/strings.js  2014-02-12 09:32:43.000000000 +0100
@@ -27,6 +27,9 @@
     lang_no: "Norwegian",
     lang_hi: "Hindi",
     lang_da: "Danish",
+    lang_cs: "Czech",
+    lang_sk: "Slovak",
+    lang_pl: "Polish",
     turn_off_airplane_mode: "Turn off Airplane Mode",
     airplane_mode_message: "Airplane Mode must be turned off to use this feature.",
     turn_on_wireless: "Turn on Wireless",
diff -ur a/translator.acx/script/translator.js b/translator.acx/script/translator.js
--- a/translator.acx/script/translator.js       2014-02-12 09:45:56.636731547 +0100
+++ b/translator.acx/script/translator.js       2014-02-12 09:32:18.000000000 +0100
@@ -618,7 +618,10 @@
     fi: "lang_fi",
     no: "lang_no",
     hi: "lang_hi",
-    da: "lang_da"
+    da: "lang_da",
+    cs: "lang_cs",
+    sk: "lang_sk",
+    pl: "lang_pl"
 };
 var sortedLanguageArray;
 var populateLanguageOptions = function(selectElement, selectionOptionValue, includeAutoDetect) {

[edit] 9. Zip the ACX contents and sign the result by following instructions from the top of this page

zip -r gtranslator.acx *
java -jar $path_to_widgetsigner/widgetsigner.jar -w gtranslator.acx -s 0 -k $path_to_key/author.p12 -a 'HomebrewActiveContentExtensions' -p 

[edit] 10. Copy the result to Kindle

Either to [userstore]/system/acw/ or to /opt/amazon/acw/

Make sure you have updated keystore_mesquite.xml and defaultPolicies.json first.

[edit] You can use these resources for a faster start:

https://github.com/dsmid/kindle-pw2-l10n-cs/blob/master/signing/widgetsigner.jar?raw=true

https://github.com/dsmid/kindle-pw2-l10n-cs/blob/master/signing/author.p12?raw=true

https://raw2.github.com/dsmid/kindle-pw2-l10n-cs/master/translation_5.4.2/other/defaultPolicies.json

https://raw2.github.com/dsmid/kindle-pw2-l10n-cs/master/translation_5.4.2/other/keystore_mesquite.xml

Personal tools
Namespaces

Variants
Actions
Navigation
MobileRead Networks
Toolbox