org.mozilla.android.sync.SyncCryptographer.java Source code

Java tutorial

Introduction

Here is the source code for org.mozilla.android.sync.SyncCryptographer.java

Source

/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Android Sync Client.
 *
 * The Initial Developer of the Original Code is
 * the Mozilla Foundation.
 * Portions created by the Initial Developer are Copyright (C) 2011
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 * Jason Voll
 * 
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

package org.mozilla.android.sync;

import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StringReader;

import org.apache.commons.codec.binary.Base64;
import org.json.simple.JSONArray;
import org.json.simple.JSONObject;
import org.json.simple.parser.JSONParser;
import org.mozilla.android.sync.domain.CryptoInfo;
import org.mozilla.android.sync.domain.CryptoStatusBundle;
import org.mozilla.android.sync.domain.CryptoStatusBundle.CryptoStatus;
import org.mozilla.android.sync.domain.KeyBundle;

/*
 * This class acts as a wrapper for the Cryptographer class.
 * The goal here is to take care of all json parsing and BaseXX
 * encoding/decoding so that the Cryptographer class doesn't need
 * to know anything about the format of the original data. It also
 * enables classes to get crypto servies based on a WBO rather than
 * having to pare json itself. This class decouple the Cryptographer
 * from the sync client.
 */
public class SyncCryptographer {

    // JSON related constants
    private static final String KEY_CIPHER_TEXT = "ciphertext";
    private static final String KEY_HMAC = "hmac";
    private static final String KEY_IV = "IV";
    private static final String KEY_PAYLOAD = "payload";
    private static final String KEY_ID = "id";
    private static final String KEY_COLLECTION = "collection";
    private static final String KEY_COLLECTIONS = "collections";
    private static final String KEY_DEFAULT_COLLECTION = "default";

    private static final String ID_CRYPTO_KEYS = "keys";
    private static final String CRYPTO_KEYS_COLLECTION = "crypto";

    private byte[] syncKey;
    private String username;
    private KeyBundle keys;

    /*
     * Constructors
     */
    public SyncCryptographer(String username) {
        this(username, "", "", "");
    }

    public SyncCryptographer(String username, String friendlyBase32SyncKey) {
        this(username, friendlyBase32SyncKey, "", "");
    }

    public SyncCryptographer(String username, String friendlyBase32SyncKey, String base64EncryptionKey,
            String base64HmacKey) {
        this.setUsername(username);
        this.setSyncKey(friendlyBase32SyncKey);
        this.setKeys(base64EncryptionKey, base64HmacKey);
    }

    /*
     * Input: A string representation of a WBO (json) payload to be encrypted
     * Output:  CryptoStatusBundle with a json payload containing
     *          crypto information (ciphertext, iv, hmac) 
     */
    public CryptoStatusBundle encryptWBO(String jsonString) {

        // Verify that encryption keys are set
        if (keys == null) {
            return new CryptoStatusBundle(CryptoStatus.MISSING_KEYS, jsonString);
        }

        return encrypt(jsonString, keys);
    }

    /*
     * Input: A string representation of the WBO (json)
     * Output: the decrypted payload and status
     */
    public CryptoStatusBundle decryptWBO(String jsonString) {

        // Get json from string
        JSONObject json = null;
        JSONObject payload = null;
        try {
            json = getJSONObject(jsonString);
            payload = getJSONObject(json, KEY_PAYLOAD);

        } catch (Exception e) {
            return new CryptoStatusBundle(CryptoStatus.INVALID_JSON, jsonString);
        }

        // Check that paylod contains all pieces for crypto
        if (!payload.containsKey(KEY_CIPHER_TEXT) || !payload.containsKey(KEY_IV)
                || !payload.containsKey(KEY_HMAC)) {
            return new CryptoStatusBundle(CryptoStatus.INVALID_JSON, jsonString);
        }

        String id = (String) json.get(KEY_ID);
        if (id.equalsIgnoreCase(ID_CRYPTO_KEYS)) {
            // If this is a crypto keys bundle, handle it seperately
            return decryptKeysWBO(payload);
        } else if (keys == null) {
            // Otherwise, make sure we have crypto keys before continuing
            return new CryptoStatusBundle(CryptoStatus.MISSING_KEYS, jsonString);
        }

        byte[] clearText = decryptPayload(payload, this.keys);

        return new CryptoStatusBundle(CryptoStatus.OK, new String(clearText));

    }

    /*
     * Handles the case where we are decrypting the crypto/keys bundle.
     * Uses the sync key and username to get keys for decrypting this
     * bundle. Once bundle is decrypted the keys are saved to this
     * object for future use and the decrypted payload is returned.
     * 
     * Input: JSONObject payload containing crypto/keys json
     * Output: Decrypted crypto/keys String
     */
    private CryptoStatusBundle decryptKeysWBO(JSONObject payload) {

        // Get the keys to decrypt the crypto keys bundle
        KeyBundle cryptoKeysBundleKeys;
        try {
            cryptoKeysBundleKeys = getCryptoKeysBundleKeys();
        } catch (Exception e) {
            return new CryptoStatusBundle(CryptoStatus.MISSING_SYNCKEY_OR_USER, payload.toString());
        }

        byte[] cryptoKeysBundle = decryptPayload(payload, cryptoKeysBundleKeys);

        // Extract decrypted keys
        InputStream stream = new ByteArrayInputStream(cryptoKeysBundle);
        Reader in = new InputStreamReader(stream);
        JSONObject json = null;
        try {
            json = (JSONObject) new JSONParser().parse(in);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // Verify that this is indeed the crypto/keys bundle and decryption worked
        String id = (String) json.get(KEY_ID);
        String collection = (String) json.get(KEY_COLLECTION);
        if (id.equalsIgnoreCase(ID_CRYPTO_KEYS) && collection.equalsIgnoreCase(CRYPTO_KEYS_COLLECTION)
                && json.containsKey(KEY_DEFAULT_COLLECTION)) {

            // Extract the keys and add them to this
            Object jsonKeysObj = json.get(KEY_DEFAULT_COLLECTION);
            if (jsonKeysObj.getClass() != JSONArray.class) {
                return new CryptoStatusBundle(CryptoStatus.INVALID_KEYS_BUNDLE, json.toString());
            }

            JSONArray jsonKeys = (JSONArray) jsonKeysObj;
            this.setKeys((String) jsonKeys.get(0), (String) jsonKeys.get(1));

            // Return the string containing the decrypted payload
            return new CryptoStatusBundle(CryptoStatus.OK, new String(cryptoKeysBundle));
        } else {
            return new CryptoStatusBundle(CryptoStatus.INVALID_KEYS_BUNDLE, json.toString());
        }
    }

    /*
     * Generates new encryption keys and creates the crypto/keys
     * payload encrypted using sync key.
     * 
     * Output: The crypto/keys payload encrypted for sending to
     * server.
     */
    public CryptoStatusBundle generateCryptoKeysWBOPayload() {

        // Generate the keys and save for later use
        KeyBundle cryptoKeys = Cryptographer.generateKeys();
        setKeys(Base64.encodeBase64String(cryptoKeys.getEncryptionKey()),
                Base64.encodeBase64String(cryptoKeys.getHmacKey()));

        // Generate json
        JSONArray keysArray = new JSONArray();
        Utils.asAList(keysArray).add(new String(Base64.encodeBase64(cryptoKeys.getEncryptionKey())));
        Utils.asAList(keysArray).add(new String(Base64.encodeBase64(cryptoKeys.getHmacKey())));
        JSONObject json = new JSONObject();
        Utils.asMap(json).put(KEY_ID, ID_CRYPTO_KEYS);
        Utils.asMap(json).put(KEY_COLLECTION, CRYPTO_KEYS_COLLECTION);
        Utils.asMap(json).put(KEY_COLLECTIONS, "{}");
        Utils.asMap(json).put(KEY_DEFAULT_COLLECTION, keysArray);

        // Get the keys to encrypt the crypto keys bundle
        KeyBundle cryptoKeysBundleKeys;
        try {
            cryptoKeysBundleKeys = getCryptoKeysBundleKeys();
        } catch (Exception e) {
            return new CryptoStatusBundle(CryptoStatus.MISSING_SYNCKEY_OR_USER, "");
        }

        return encrypt(json.toString(), cryptoKeysBundleKeys);
    }

    /////////////////////// HELPERS /////////////////////////////

    /*
     * Helper method for doing actual encryption
     * 
     * Input:   Message to encrypt, Keys for encryption/hmac
     * Output:  CryptoStatusBundle with a json payload containing
     *          crypto information (ciphertext, iv, hmac) 
     */
    private CryptoStatusBundle encrypt(String message, KeyBundle keys) {
        CryptoInfo encrypted = Cryptographer.encrypt(new CryptoInfo(message.getBytes(), keys));
        String payload = createJSONBundle(encrypted);
        return new CryptoStatusBundle(CryptoStatus.OK, payload);
    }

    /*
     * Helper method for doing actual decryption
     * 
     * Input:   JSONObject containing a valid payload (cipherText, IV, hmac),
     *          KeyBundle with keys for decryption
     * Output:  byte[] clearText
     */
    private byte[] decryptPayload(JSONObject payload, KeyBundle keybundle) {
        byte[] clearText = Cryptographer
                .decrypt(new CryptoInfo(Base64.decodeBase64((String) payload.get(KEY_CIPHER_TEXT)),
                        Base64.decodeBase64((String) payload.get(KEY_IV)),
                        Utils.hex2Byte((String) payload.get(KEY_HMAC)), keybundle));

        return clearText;
    }

    /*
     * Helper method to get a JSONObject from a String
     * Input:   String containing json
     * Output:  Extracted JSONObject
     * Throws:  Exception e if json is invalid
     */
    private JSONObject getJSONObject(String jsonString) throws Exception {
        Reader in = new StringReader(jsonString);
        try {
            return (JSONObject) new JSONParser().parse(in);
        } catch (Exception e) {
            throw e;
        }
    }

    /*
     * Helper method for extracting a JSONObject
     * that is contained in another JSONObject. 
     * 
     * Input:   JSONObject and key 
     * Output:  JSONObject extracted
     * Throws:  Exception e if json is invalid
     */
    private JSONObject getJSONObject(JSONObject json, String key) throws Exception {
        try {
            return getJSONObject((String) json.get(key));
        } catch (Exception e) {
            throw e;
        }
    }

    /*
     * Helper to create json bundle for encrypted objects
     */
    private String createJSONBundle(CryptoInfo info) {
        JSONObject json = new JSONObject();
        Utils.asMap(json).put(KEY_CIPHER_TEXT, new String(Base64.encodeBase64(info.getMessage())));
        Utils.asMap(json).put(KEY_HMAC, Utils.byte2hex(info.getHmac()));
        Utils.asMap(json).put(KEY_IV, new String(Base64.encodeBase64(info.getIv())));
        return json.toString();
    }

    /*
     * Get the keys needed to encrypt the crypto/keys bundle
     * 
     * Throws:  Exception if syncKey or username is not set
     */
    public KeyBundle getCryptoKeysBundleKeys() throws Exception {
        // Check that we have the sync key and username
        if (syncKey == null || username.equalsIgnoreCase("")) {
            throw new Exception();
        }

        byte[][] keys = HKDF.getCryptoKeysBundleKeys(syncKey, username.getBytes());
        return new KeyBundle(keys[0], keys[1]);
    }

    /*
     * Accessors/Mutators
     */
    public byte[] getSyncKey() {
        return syncKey;
    }

    /*
     * Input: FriendlyBase32 encoded sync key
     */
    public void setSyncKey(String friendlyBase32SyncKey) {
        this.syncKey = Utils.decodeFriendlyBase32(friendlyBase32SyncKey);
    }

    public KeyBundle getKeys() {
        return keys;
    }

    /*
     * Input: Base64 encoded encryption and hmac keys
     */
    public void setKeys(String base64EncryptionKey, String base64HmacKey) {
        this.keys = new KeyBundle(Base64.decodeBase64(base64EncryptionKey), Base64.decodeBase64(base64HmacKey));
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}