| // Copyright 2015 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package main |
| |
| // APK is the archival format used for Android apps. It is a ZIP archive with |
| // three extra files: |
| // |
| // META-INF/MANIFEST.MF |
| // META-INF/CERT.SF |
| // META-INF/CERT.RSA |
| // |
| // The MANIFEST.MF comes from the Java JAR archive format. It is a list of |
| // files included in the archive along with a SHA1 hash, for example: |
| // |
| // Name: lib/armeabi/libbasic.so |
| // SHA1-Digest: ntLSc1eLCS2Tq1oB4Vw6jvkranw= |
| // |
| // For debugging, the equivalent SHA1-Digest can be generated with OpenSSL: |
| // |
| // cat lib/armeabi/libbasic.so | openssl sha1 -binary | openssl base64 |
| // |
| // CERT.SF is a similar manifest. It begins with a SHA1 digest of the entire |
| // manifest file: |
| // |
| // Signature-Version: 1.0 |
| // Created-By: 1.0 (Android) |
| // SHA1-Digest-Manifest: aJw+u+10C3Enbg8XRCN6jepluYA= |
| // |
| // Then for each entry in the manifest it has a SHA1 digest of the manfiest's |
| // hash combined with the file name: |
| // |
| // Name: lib/armeabi/libbasic.so |
| // SHA1-Digest: Q7NAS6uzrJr6WjePXSGT+vvmdiw= |
| // |
| // This can also be generated with openssl: |
| // |
| // echo -en "Name: lib/armeabi/libbasic.so\r\nSHA1-Digest: ntLSc1eLCS2Tq1oB4Vw6jvkranw=\r\n\r\n" | openssl sha1 -binary | openssl base64 |
| // |
| // Note the \r\n line breaks. |
| // |
| // CERT.RSA is an RSA signature block made of CERT.SF. Verify it with: |
| // |
| // openssl smime -verify -in CERT.RSA -inform DER -content CERT.SF cert.pem |
| // |
| // The APK format imposes two extra restrictions on the ZIP format. First, |
| // it is uncompressed. Second, each contained file is 4-byte aligned. This |
| // allows the Android OS to mmap contents without unpacking the archive. |
| |
| // Note: to make life a little harder, Android Studio stores the RSA key used |
| // for signing in an Oracle Java proprietary keystore format, JKS. For example, |
| // the generated debug key is in ~/.android/debug.keystore, and can be |
| // extracted using the JDK's keytool utility: |
| // |
| // keytool -importkeystore -srckeystore ~/.android/debug.keystore -destkeystore ~/.android/debug.p12 -deststoretype PKCS12 |
| // |
| // Once in standard PKCS12, the key can be converted to PEM for use in the |
| // Go crypto packages: |
| // |
| // openssl pkcs12 -in ~/.android/debug.p12 -nocerts -nodes -out ~/.android/debug.pem |
| // |
| // Fortunately for debug builds, all that matters is that the APK is signed. |
| // The choice of key is unimportant, so we can generate one for normal builds. |
| // For production builds, we can ask users to provide a PEM file. |
| |
| import ( |
| "archive/zip" |
| "bytes" |
| "crypto/rand" |
| "crypto/rsa" |
| "crypto/sha1" |
| "encoding/base64" |
| "fmt" |
| "hash" |
| "io" |
| |
| "golang.org/x/mobile/internal/binres" |
| ) |
| |
| // NewWriter returns a new Writer writing an APK file to w. |
| // The APK will be signed with key. |
| func NewWriter(w io.Writer, priv *rsa.PrivateKey) *Writer { |
| apkw := &Writer{priv: priv} |
| apkw.w = zip.NewWriter(&countWriter{apkw: apkw, w: w}) |
| return apkw |
| } |
| |
| // Writer implements an APK file writer. |
| type Writer struct { |
| offset int |
| w *zip.Writer |
| priv *rsa.PrivateKey |
| manifest []manifestEntry |
| cur *fileWriter |
| } |
| |
| // Create adds a file to the APK archive using the provided name. |
| // |
| // The name must be a relative path. The file's contents must be written to |
| // the returned io.Writer before the next call to Create or Close. |
| func (w *Writer) Create(name string) (io.Writer, error) { |
| if err := w.clearCur(); err != nil { |
| return nil, fmt.Errorf("apk: Create(%s): %v", name, err) |
| } |
| if name == "AndroidManifest.xml" { |
| w.cur = &fileWriter{ |
| name: name, |
| w: new(bytes.Buffer), |
| sha1: sha1.New(), |
| } |
| return w.cur, nil |
| } |
| res, err := w.create(name) |
| if err != nil { |
| return nil, fmt.Errorf("apk: Create(%s): %v", name, err) |
| } |
| return res, nil |
| } |
| |
| func (w *Writer) create(name string) (io.Writer, error) { |
| // Align start of file contents by using Extra as padding. |
| if err := w.w.Flush(); err != nil { // for exact offset |
| return nil, err |
| } |
| const fileHeaderLen = 30 // + filename + extra |
| start := w.offset + fileHeaderLen + len(name) |
| extra := start % 4 |
| |
| zipfw, err := w.w.CreateHeader(&zip.FileHeader{ |
| Name: name, |
| Extra: make([]byte, extra), |
| }) |
| if err != nil { |
| return nil, err |
| } |
| w.cur = &fileWriter{ |
| name: name, |
| w: zipfw, |
| sha1: sha1.New(), |
| } |
| return w.cur, nil |
| } |
| |
| // Close finishes writing the APK. This includes writing the manifest and |
| // signing the archive, and writing the ZIP central directory. |
| // |
| // It does not close the underlying writer. |
| func (w *Writer) Close() error { |
| if err := w.clearCur(); err != nil { |
| return fmt.Errorf("apk: %v", err) |
| } |
| |
| hasDex := false |
| for _, entry := range w.manifest { |
| if entry.name == "classes.dex" { |
| hasDex = true |
| break |
| } |
| } |
| |
| manifest := new(bytes.Buffer) |
| if hasDex { |
| fmt.Fprint(manifest, manifestDexHeader) |
| } else { |
| fmt.Fprint(manifest, manifestHeader) |
| } |
| certBody := new(bytes.Buffer) |
| |
| for _, entry := range w.manifest { |
| n := entry.name |
| h := base64.StdEncoding.EncodeToString(entry.sha1.Sum(nil)) |
| fmt.Fprintf(manifest, "Name: %s\nSHA1-Digest: %s\n\n", n, h) |
| cHash := sha1.New() |
| fmt.Fprintf(cHash, "Name: %s\r\nSHA1-Digest: %s\r\n\r\n", n, h) |
| ch := base64.StdEncoding.EncodeToString(cHash.Sum(nil)) |
| fmt.Fprintf(certBody, "Name: %s\nSHA1-Digest: %s\n\n", n, ch) |
| } |
| |
| mHash := sha1.New() |
| mHash.Write(manifest.Bytes()) |
| cert := new(bytes.Buffer) |
| fmt.Fprint(cert, certHeader) |
| fmt.Fprintf(cert, "SHA1-Digest-Manifest: %s\n\n", base64.StdEncoding.EncodeToString(mHash.Sum(nil))) |
| cert.Write(certBody.Bytes()) |
| |
| mw, err := w.Create("META-INF/MANIFEST.MF") |
| if err != nil { |
| return err |
| } |
| if _, err := mw.Write(manifest.Bytes()); err != nil { |
| return err |
| } |
| |
| cw, err := w.Create("META-INF/CERT.SF") |
| if err != nil { |
| return err |
| } |
| if _, err := cw.Write(cert.Bytes()); err != nil { |
| return err |
| } |
| |
| rsa, err := signPKCS7(rand.Reader, w.priv, cert.Bytes()) |
| if err != nil { |
| return fmt.Errorf("apk: %v", err) |
| } |
| rw, err := w.Create("META-INF/CERT.RSA") |
| if err != nil { |
| return err |
| } |
| if _, err := rw.Write(rsa); err != nil { |
| return err |
| } |
| |
| return w.w.Close() |
| } |
| |
| const manifestHeader = `Manifest-Version: 1.0 |
| Created-By: 1.0 (Go) |
| |
| ` |
| |
| const manifestDexHeader = `Manifest-Version: 1.0 |
| Dex-Location: classes.dex |
| Created-By: 1.0 (Go) |
| |
| ` |
| |
| const certHeader = `Signature-Version: 1.0 |
| Created-By: 1.0 (Go) |
| ` |
| |
| func (w *Writer) clearCur() error { |
| if w.cur == nil { |
| return nil |
| } |
| if w.cur.name == "AndroidManifest.xml" { |
| buf := w.cur.w.(*bytes.Buffer) |
| bxml, err := binres.UnmarshalXML(buf) |
| if err != nil { |
| return err |
| } |
| b, err := bxml.MarshalBinary() |
| if err != nil { |
| return err |
| } |
| f, err := w.create("AndroidManifest.xml") |
| if err != nil { |
| return err |
| } |
| if _, err := f.Write(b); err != nil { |
| return err |
| } |
| } |
| w.manifest = append(w.manifest, manifestEntry{ |
| name: w.cur.name, |
| sha1: w.cur.sha1, |
| }) |
| w.cur.closed = true |
| w.cur = nil |
| return nil |
| } |
| |
| type manifestEntry struct { |
| name string |
| sha1 hash.Hash |
| } |
| |
| type countWriter struct { |
| apkw *Writer |
| w io.Writer |
| } |
| |
| func (c *countWriter) Write(p []byte) (n int, err error) { |
| n, err = c.w.Write(p) |
| c.apkw.offset += n |
| return n, err |
| } |
| |
| type fileWriter struct { |
| name string |
| w io.Writer |
| sha1 hash.Hash |
| closed bool |
| } |
| |
| func (w *fileWriter) Write(p []byte) (n int, err error) { |
| if w.closed { |
| return 0, fmt.Errorf("apk: write to closed file %q", w.name) |
| } |
| w.sha1.Write(p) |
| n, err = w.w.Write(p) |
| if err != nil { |
| err = fmt.Errorf("apk: %v", err) |
| } |
| return n, err |
| } |