org.apache.shindig.common.crypto.BasicBlobCrypter.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.shindig.common.crypto.BasicBlobCrypter.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (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.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.shindig.common.crypto;

import com.google.common.base.Charsets;
import com.google.common.collect.Maps;
import com.google.common.base.Preconditions;

import org.apache.commons.lang.StringUtils;
import org.apache.shindig.common.util.CharsetUtil;
import org.apache.shindig.common.util.TimeSource;

import org.apache.commons.codec.binary.Base64;
import org.apache.commons.codec.digest.DigestUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.GeneralSecurityException;
import java.util.Map;

/**
 * Simple implementation of BlobCrypter.
 */
public class BasicBlobCrypter implements BlobCrypter {

    // Labels for key derivation
    private static final byte CIPHER_KEY_LABEL = 0;
    private static final byte HMAC_KEY_LABEL = 1;

    /** Key used for time stamp (in seconds) of data */
    public static final String TIMESTAMP_KEY = "t";

    /** minimum length of master key */
    public static final int MASTER_KEY_MIN_LEN = 16;

    /** allow three minutes for clock skew */
    private static final long CLOCK_SKEW_ALLOWANCE = 180;

    private static final String UTF8 = "UTF-8";

    public TimeSource timeSource = new TimeSource();
    private byte[] cipherKey;
    private byte[] hmacKey;

    /**
     * Creates a crypter based on a key in a file.  The key is the first line
     * in the file, whitespace trimmed from either end, as UTF-8 bytes.
     *
     * The following *nix command line will create an excellent key:
     * <pre>
     * dd if=/dev/random bs=32 count=1  | openssl base64 > /tmp/key.txt
     * </pre>
     *
     * @throws IOException if the file can't be read.
     */
    public BasicBlobCrypter(File keyfile) throws IOException {
        BufferedReader reader = null;
        try {
            FileInputStream openFile = new FileInputStream(keyfile);
            reader = new BufferedReader(new InputStreamReader(openFile, Charsets.UTF_8));
            String line = reader.readLine();
            if (line == null) {
                throw new IOException("Unexpectedly empty keyfile:" + keyfile);
            }
            line = line.trim();
            byte[] keyBytes = CharsetUtil.getUtf8Bytes(line);
            init(keyBytes);
        } finally {
            try {
                if (reader != null) {
                    reader.close();
                }
            } catch (IOException e) {
                // oh well.
            }
        }
    }

    /**
     * Builds a BlobCrypter from the specified master key
     *
     * @param masterKey
     */
    public BasicBlobCrypter(byte[] masterKey) {
        init(masterKey);
    }

    private void init(byte[] masterKey) {
        Preconditions.checkArgument(masterKey.length >= MASTER_KEY_MIN_LEN, "Master key needs at least %s bytes",
                MASTER_KEY_MIN_LEN);

        cipherKey = deriveKey(CIPHER_KEY_LABEL, masterKey, Crypto.CIPHER_KEY_LEN);
        hmacKey = deriveKey(HMAC_KEY_LABEL, masterKey, 0);
    }

    /**
     * Generates unique keys from a master key.
     *
     * @param label type of key to derive
     * @param masterKey master key
     * @param len length of key needed, less than 20 bytes.  20 bytes are
     * returned if len is 0.
     *
     * @return a derived key of the specified length
     */
    private byte[] deriveKey(byte label, byte[] masterKey, int len) {
        byte[] base = Crypto.concat(new byte[] { label }, masterKey);
        byte[] hash = DigestUtils.sha(base);
        if (len == 0) {
            return hash;
        }
        byte[] out = new byte[len];
        System.arraycopy(hash, 0, out, 0, out.length);
        return out;
    }

    /* (non-Javadoc)
     * @see org.apache.shindig.util.BlobCrypter#wrap(java.util.Map)
     */
    public String wrap(Map<String, String> in) throws BlobCrypterException {
        Preconditions.checkArgument(!in.containsKey(TIMESTAMP_KEY), "No '%s' key allowed for BlobCrypter",
                TIMESTAMP_KEY);

        try {
            byte[] encoded = serializeAndTimestamp(in);
            byte[] cipherText = Crypto.aes128cbcEncrypt(cipherKey, encoded);
            byte[] hmac = Crypto.hmacSha1(hmacKey, cipherText);
            byte[] b64 = Base64.encodeBase64URLSafe(Crypto.concat(cipherText, hmac));
            return new String(b64, UTF8);
        } catch (UnsupportedEncodingException e) {
            throw new BlobCrypterException(e);
        } catch (GeneralSecurityException e) {
            throw new BlobCrypterException(e);
        }
    }

    /**
     * Encode the input for transfer.  We use something a lot like HTML form
     * encodings.  The time stamp is in seconds since the epoch.
     */
    private byte[] serializeAndTimestamp(Map<String, String> in) throws UnsupportedEncodingException {
        StringBuilder sb = new StringBuilder();

        for (Map.Entry<String, String> val : in.entrySet()) {
            sb.append(URLEncoder.encode(val.getKey(), UTF8));
            sb.append('=');
            sb.append(URLEncoder.encode(val.getValue(), UTF8));
            sb.append('&');
        }
        sb.append(TIMESTAMP_KEY);
        sb.append('=');
        sb.append(timeSource.currentTimeMillis() / 1000);
        return sb.toString().getBytes(UTF8);
    }

    /* (non-Javadoc)
     * @see org.apache.shindig.util.BlobCrypter#unwrap(java.lang.String, int)
     */
    public Map<String, String> unwrap(String in, int maxAgeSec) throws BlobCrypterException {
        try {
            byte[] bin = Base64.decodeBase64(in.getBytes("UTF-8"));
            byte[] hmac = new byte[Crypto.HMAC_SHA1_LEN];
            byte[] cipherText = new byte[bin.length - Crypto.HMAC_SHA1_LEN];
            System.arraycopy(bin, 0, cipherText, 0, cipherText.length);
            System.arraycopy(bin, cipherText.length, hmac, 0, hmac.length);
            Crypto.hmacSha1Verify(hmacKey, cipherText, hmac);
            byte[] plain = Crypto.aes128cbcDecrypt(cipherKey, cipherText);
            Map<String, String> out = deserialize(plain);
            checkTimestamp(out, maxAgeSec);
            return out;
        } catch (GeneralSecurityException e) {
            throw new BlobCrypterException("Invalid token signature", e);
        } catch (ArrayIndexOutOfBoundsException e) {
            throw new BlobCrypterException("Invalid token format", e);
        } catch (NegativeArraySizeException e) {
            throw new BlobCrypterException("Invalid token format", e);
        } catch (UnsupportedEncodingException e) {
            throw new BlobCrypterException(e);
        }

    }

    private Map<String, String> deserialize(byte[] plain) throws UnsupportedEncodingException {
        String base = new String(plain, UTF8);
        // replaces [&=] regex
        String[] items = StringUtils.splitPreserveAllTokens(base, "&=");
        Map<String, String> map = Maps.newHashMapWithExpectedSize(items.length);
        for (int i = 0; i < items.length;) {
            String key = URLDecoder.decode(items[i++], UTF8);
            String val = URLDecoder.decode(items[i++], UTF8);
            map.put(key, val);
        }
        return map;
    }

    /**
     * We allow a few minutes on either side of the validity window to account
     * for clock skew.
     */
    private void checkTimestamp(Map<String, String> out, int maxAge) throws BlobExpiredException {
        long origin = Long.parseLong(out.get(TIMESTAMP_KEY));
        long minTime = origin - CLOCK_SKEW_ALLOWANCE;
        long maxTime = origin + maxAge + CLOCK_SKEW_ALLOWANCE;
        long now = timeSource.currentTimeMillis() / 1000;
        if (!(minTime < now && now < maxTime)) {
            throw new BlobExpiredException(minTime, now, maxTime);
        }
    }

}