org.sufficientlysecure.privacybox.EncryptedDocument.java Source code

Java tutorial

Introduction

Here is the source code for org.sufficientlysecure.privacybox.EncryptedDocument.java

Source

/*
 * Copyright (C) 2014 Dominik Schrmann <dominik@dominikschuermann.de>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package org.sufficientlysecure.privacybox;

import static org.sufficientlysecure.privacybox.VaultProvider.TAG;

import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.IntentSender;
import android.database.MatrixCursor;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.os.Messenger;
import android.os.ParcelFileDescriptor;
import android.os.RemoteException;
import android.provider.DocumentsContract.Document;
import android.util.Log;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.openintents.openpgp.OpenPgpError;
import org.openintents.openpgp.OpenPgpMetadata;
import org.openintents.openpgp.util.OpenPgpApi;
import org.openintents.openpgp.util.OpenPgpServiceConnection;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.lang.reflect.Constructor;
import java.net.ProtocolException;
import java.nio.charset.StandardCharsets;
import java.security.DigestException;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;

import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

/**
 * Represents a single encrypted document stored on disk. Handles encryption,
 * decryption, and authentication of the document when requested.
 * <p/>
 * Not inherently thread safe.
 */
public class EncryptedDocument {

    private final long mDocId;
    private final File mFile;

    OpenPgpServiceConnection mServiceConnection;
    Context mContext;

    /**
     * Create an encrypted document.
     *
     * @param docId     the expected {@link Document#COLUMN_DOCUMENT_ID} to be
     *                  validated when reading metadata.
     * @param directory location on disk where the encrypted document is stored. May
     *                  not exist yet.
     */
    public EncryptedDocument(long docId, File directory, Context context,
            OpenPgpServiceConnection serviceConnection) throws GeneralSecurityException {
        mServiceConnection = serviceConnection;
        mContext = context;

        mDocId = docId;
        mFile = new File(directory, String.valueOf(docId) + ".gpg");
    }

    public File getFile() {
        return mFile;
    }

    @Override
    public String toString() {
        return mFile.getName();
    }

    /**
     * Decrypt and return parsed metadata section from this document.
     *
     * @throws DigestException if metadata fails MAC check, or if
     *                         {@link Document#COLUMN_DOCUMENT_ID} recorded in metadata is
     *                         unexpected.
     */
    public JSONObject readMetadata() throws IOException, GeneralSecurityException {
        InputStream fis = new FileInputStream(mFile);

        try {
            Intent data = new Intent();
            data.setAction(OpenPgpApi.ACTION_DECRYPT_METADATA);
            data.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, "default");
            OpenPgpApi api = new OpenPgpApi(mContext, mServiceConnection.getService());
            Intent result = api.executeApi(data, fis, null);

            switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
            case OpenPgpApi.RESULT_CODE_SUCCESS: {
                Log.d(VaultProvider.TAG, "readMetadata RESULT_CODE_SUCCESS");

                //                    tempFile.renameTo(mFile);

                // TODO: better handling of errors
                OpenPgpMetadata openPgpMeta;
                if (result.hasExtra(OpenPgpApi.RESULT_METADATA)) {
                    openPgpMeta = result.getParcelableExtra(OpenPgpApi.RESULT_METADATA);
                } else {
                    throw new IOException();
                }

                Log.d(TAG, "metadata for " + mDocId + ": " + openPgpMeta);

                String filenameHeader = openPgpMeta.getFilename();
                long size = openPgpMeta.getOriginalSize();
                long modTime = openPgpMeta.getModificationTime();

                final JSONObject meta = new JSONObject();

                /*
                 * If the filename header of the encrypted pgp file contains
                 * JSON, we are dealing with a directory.
                 * Instead of the actual filename, directories include JSON encoded mime type
                 * and an array of all child documents.
                 */
                if (filenameHeader.contains("{")) { // JSON with high probability
                    meta.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);

                    JSONObject json = new JSONObject(filenameHeader);
                    final String name = json.getString(Document.COLUMN_DISPLAY_NAME);
                    final JSONArray children = json.getJSONArray(VaultProvider.KEY_CHILDREN);

                    Log.d(VaultProvider.TAG, "json from filename header: " + json);
                    Log.d(VaultProvider.TAG, "name: " + name);
                    Log.d(VaultProvider.TAG, "children: " + children);

                    meta.put(Document.COLUMN_DISPLAY_NAME, name);
                    meta.put(VaultProvider.KEY_CHILDREN, children);
                } else {
                    String mimeType = openPgpMeta.getMimeType();
                    meta.put(Document.COLUMN_MIME_TYPE, mimeType);

                    meta.put(Document.COLUMN_DISPLAY_NAME, filenameHeader);
                }
                meta.put(Document.COLUMN_DOCUMENT_ID, mDocId);
                meta.put(Document.COLUMN_SIZE, size);
                meta.put(Document.COLUMN_LAST_MODIFIED, modTime);

                return meta;
            }
            case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: {
                Log.d(VaultProvider.TAG, "readMetadata RESULT_CODE_USER_INTERACTION_REQUIRED");

                // directly try again, something different needs user interaction again...
                PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);

                Handler handler = new Handler(mContext.getMainLooper(), new Handler.Callback() {
                    @Override
                    public boolean handleMessage(Message msg) {
                        Log.d(VaultProvider.TAG, "writeMetadataAndContent handleMessage");

                        // TODO: start again afterwards!!!
                        return true;
                    }
                });
                Messenger messenger = new Messenger(handler);

                // start proxy activity and wait here for it finishing...
                Intent proxy = new Intent(mContext, KeychainProxyActivity.class);
                proxy.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                proxy.putExtra(KeychainProxyActivity.EXTRA_MESSENGER, messenger);
                proxy.putExtra(KeychainProxyActivity.EXTRA_PENDING_INTENT, pi);

                mContext.startActivity(proxy);
                break;
            }
            case OpenPgpApi.RESULT_CODE_ERROR: {
                Log.d(VaultProvider.TAG, "readMetadata RESULT_CODE_ERROR");

                // TODO
                OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
                Log.e(VaultProvider.TAG, "error: " + error.getMessage());

                //                handleError(error);
                break;
            }
            }

            return null;
        } catch (JSONException e) {
            throw new IOException(e);
        } finally {
            fis.close();
        }
    }

    /**
     * Decrypt and read content section of this document, writing it into the
     * given pipe.
     * <p/>
     * Pipe is left open, so caller is responsible for calling
     * {@link ParcelFileDescriptor#close()} or
     * {@link ParcelFileDescriptor#closeWithError(String)}.
     *
     * @param contentOut write end of a pipe.
     * @throws DigestException if content fails MAC check. Some or all content
     *                         may have already been written to the pipe when the MAC is
     *                         validated.
     */
    public void readContent(ParcelFileDescriptor contentOut) throws IOException, GeneralSecurityException {
        //        final RandomAccessFile f = new RandomAccessFile(mFile, "r");
        //        try {
        //            assertMagic(f);
        //
        //            if (f.length() <= CONTENT_OFFSET) {
        //                throw new IOException("Document has no content");
        //            }
        //
        //            // Skip over metadata section
        //            f.seek(CONTENT_OFFSET);
        //            readSection(f, new FileOutputStream(contentOut.getFileDescriptor()));
        //
        //        } finally {
        //            f.close();
        //        }
    }

    /**
     * Encrypt and write both the metadata and content sections of this
     * document, reading the content from the given pipe. Internally uses
     * {@link ParcelFileDescriptor#checkError()} to verify that content arrives
     * without errors. Writes to temporary file to keep atomic view of contents,
     * swapping into place only when write is successful.
     * <p/>
     * Pipe is left open, so caller is responsible for calling
     * {@link ParcelFileDescriptor#close()} or
     * {@link ParcelFileDescriptor#closeWithError(String)}.
     *
     * @param contentIn read end of a pipe.
     */
    public void writeMetadataAndContent(JSONObject meta, ParcelFileDescriptor contentIn)
            throws IOException, GeneralSecurityException {
        // Write into temporary file to provide an atomic view of existing
        // contents during write, and also to recover from failed writes.
        final String tempName = mFile.getName() + ".tmp_" + Thread.currentThread().getId();
        final File tempFile = new File(mFile.getParentFile(), tempName);

        //        RandomAccessFile f = new RandomAccessFile(tempFile, "rw");
        try {

            // TODO: while not == RESULT_CODE_SUCCESS wait notify and stuff...
            // make this blocking!
            Intent data = new Intent();
            data.setAction(OpenPgpApi.ACTION_SIGN_AND_ENCRYPT);

            InputStream is;
            String mimeType = meta.getString(Document.COLUMN_MIME_TYPE);
            if (Document.MIME_TYPE_DIR.equals(mimeType)) {
                // this is a directory! write only dir into content...
                is = new ByteArrayInputStream("dir".getBytes());

                /*
                  * combine mime type, display name, and children into one json!
                  */
                JSONObject json = new JSONObject();
                json.put(Document.COLUMN_MIME_TYPE, Document.MIME_TYPE_DIR);
                json.put(Document.COLUMN_DISPLAY_NAME, meta.getString(Document.COLUMN_DISPLAY_NAME));
                json.put(VaultProvider.KEY_CHILDREN, meta.getJSONArray(VaultProvider.KEY_CHILDREN));

                Log.d(VaultProvider.TAG, "json: " + json.toString());

                // write json into
                data.putExtra(OpenPgpApi.EXTRA_ORIGINAL_FILENAME, json.toString());
            } else {
                // writing only metadata, no contentIn...
                if (contentIn == null) {
                    is = new ByteArrayInputStream("write content later".getBytes());
                } else {
                    is = new FileInputStream(contentIn.getFileDescriptor());
                }

                // TODO: no possibility to write mime type to pgp header, currently
                data.putExtra(OpenPgpApi.EXTRA_ORIGINAL_FILENAME, meta.getString(Document.COLUMN_DISPLAY_NAME));
            }

            data.putExtra(OpenPgpApi.EXTRA_USER_IDS, new String[] { "nopass@example.com" });
            data.putExtra(OpenPgpApi.EXTRA_ACCOUNT_NAME, "default");
            data.putExtra(OpenPgpApi.EXTRA_REQUEST_ASCII_ARMOR, true); // TODO: fix later to false!
            OpenPgpApi api = new OpenPgpApi(mContext, mServiceConnection.getService());

            Intent result = api.executeApi(data, is, new FileOutputStream(tempFile));

            switch (result.getIntExtra(OpenPgpApi.RESULT_CODE, OpenPgpApi.RESULT_CODE_ERROR)) {
            case OpenPgpApi.RESULT_CODE_SUCCESS: {
                Log.d(VaultProvider.TAG, "writeMetadataAndContent RESULT_CODE_SUCCESS");

                tempFile.renameTo(mFile);

                break;
            }
            case OpenPgpApi.RESULT_CODE_USER_INTERACTION_REQUIRED: {
                Log.d(VaultProvider.TAG, "writeMetadataAndContent RESULT_CODE_USER_INTERACTION_REQUIRED");

                // directly try again, something different needs user interaction again...
                PendingIntent pi = result.getParcelableExtra(OpenPgpApi.RESULT_INTENT);

                Handler handler = new Handler(mContext.getMainLooper(), new Handler.Callback() {
                    @Override
                    public boolean handleMessage(Message msg) {
                        Log.d(VaultProvider.TAG, "writeMetadataAndContent handleMessage");

                        // TODO: start again afterwards!!!
                        return true;
                    }
                });
                Messenger messenger = new Messenger(handler);

                // start proxy activity and wait here for it finishing...
                Intent proxy = new Intent(mContext, KeychainProxyActivity.class);
                proxy.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                proxy.putExtra(KeychainProxyActivity.EXTRA_MESSENGER, messenger);
                proxy.putExtra(KeychainProxyActivity.EXTRA_PENDING_INTENT, pi);

                mContext.startActivity(proxy);
                break;
            }
            case OpenPgpApi.RESULT_CODE_ERROR: {
                Log.d(VaultProvider.TAG, "writeMetadataAndContent RESULT_CODE_ERROR");

                // TODO
                OpenPgpError error = result.getParcelableExtra(OpenPgpApi.RESULT_ERROR);
                Log.e(VaultProvider.TAG, "error: " + error.getMessage());
                break;
            }
            }

            // Truncate any existing data
            //            f.setLength(0);

            // Write content first to detect size
            //            if (contentIn != null) {
            //                f.seek(CONTENT_OFFSET);
            //                final int plainLength = writeSection(
            //                        f, new FileInputStream(contentIn.getFileDescriptor()));
            //                meta.put(Document.COLUMN_SIZE, plainLength);
            //
            //                // Verify that remote side of pipe finished okay; if they
            //                // crashed or indicated an error then this throws and we
            //                // leave the original file intact and clean up temp below.
            //                contentIn.checkError();
            //            }

            //            meta.put(Document.COLUMN_DOCUMENT_ID, mDocId);
            //            meta.put(Document.COLUMN_LAST_MODIFIED, System.currentTimeMillis());

            // Rewind and write metadata section
            //            f.seek(0);
            //            f.writeInt(MAGIC_NUMBER);

            //            final ByteArrayInputStream metaIn = new ByteArrayInputStream(
            //                    meta.toString().getBytes(StandardCharsets.UTF_8));
            //            writeSection(f, metaIn);
            //
            //            if (f.getFilePointer() > CONTENT_OFFSET) {
            //                throw new IOException("Metadata section was too large");
            //            }

            // Everything written fine, atomically swap new data into place.
            // fsync() before close would be overkill, since rename() is an
            // atomic barrier.
            //            f.close();

        } catch (JSONException e) {
            throw new IOException(e);
        } finally {
            // Regardless of what happens, always try cleaning up.
            //            f.close();
            tempFile.delete();
        }
    }

    /**
     * Read and decrypt the section starting at the current file offset.
     * Validates MAC of decrypted data, throwing if mismatch. When finished,
     * file offset is at the end of the entire section.
     */
    //    private void readSection(RandomAccessFile f, OutputStream out)
    //            throws IOException, GeneralSecurityException {
    //        final long start = f.getFilePointer();
    //
    //        final Section section = new Section();
    //        section.read(f);
    //
    //        final IvParameterSpec ivSpec = new IvParameterSpec(section.iv);
    //        mCipher.init(Cipher.DECRYPT_MODE, mDataKey, ivSpec);
    //        mMac.init(mMacKey);
    //
    //        byte[] inbuf = new byte[8192];
    //        byte[] outbuf;
    //        int n;
    //        while ((n = f.read(inbuf, 0, (int) Math.min(section.length, inbuf.length))) != -1) {
    //            section.length -= n;
    //            mMac.update(inbuf, 0, n);
    //            outbuf = mCipher.update(inbuf, 0, n);
    //            if (outbuf != null) {
    //                out.write(outbuf);
    //            }
    //            if (section.length == 0) break;
    //        }
    //
    //        section.assertMac(mMac.doFinal());
    //
    //        outbuf = mCipher.doFinal();
    //        if (outbuf != null) {
    //            out.write(outbuf);
    //        }
    //    }

    /**
     * Encrypt and write the given stream as a full section. Writes section
     * header and encrypted data starting at the current file offset. When
     * finished, file offset is at the end of the entire section.
     */
    //    private int writeSection(RandomAccessFile f, InputStream in)
    //            throws IOException, GeneralSecurityException {
    //        final long start = f.getFilePointer();
    //
    //        // Write header; we'll come back later to finalize details
    //        final Section section = new Section();
    //        section.write(f);
    //
    //        final long dataStart = f.getFilePointer();
    //
    //        mRandom.nextBytes(section.iv);
    //
    //        final IvParameterSpec ivSpec = new IvParameterSpec(section.iv);
    //        mCipher.init(Cipher.ENCRYPT_MODE, mDataKey, ivSpec);
    //        mMac.init(mMacKey);
    //
    //        int plainLength = 0;
    //        byte[] inbuf = new byte[8192];
    //        byte[] outbuf;
    //        int n;
    //        while ((n = in.read(inbuf)) != -1) {
    //            plainLength += n;
    //            outbuf = mCipher.update(inbuf, 0, n);
    //            if (outbuf != null) {
    //                mMac.update(outbuf);
    //                f.write(outbuf);
    //            }
    //        }
    //
    //        outbuf = mCipher.doFinal();
    //        if (outbuf != null) {
    //            mMac.update(outbuf);
    //            f.write(outbuf);
    //        }
    //
    //        section.setMac(mMac.doFinal());
    //
    //        final long dataEnd = f.getFilePointer();
    //        section.length = dataEnd - dataStart;
    //
    //        // Rewind and update header
    //        f.seek(start);
    //        section.write(f);
    //        f.seek(dataEnd);
    //
    //        return plainLength;
    //    }

}