/*
HmacManager.java
Copyright (c) 2015 NTT DOCOMO,INC.
Released under the MIT license
http://opensource.org/licenses/mit-license.php
*/
package org.deviceconnect.android.manager.hmac;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteException;
import android.database.sqlite.SQLiteOpenHelper;
import android.provider.BaseColumns;
/**
* HMAC Manager.
* @author NTT DOCOMO, INC.
*/
public final class HmacManager {
/**
* The hash algorithm.
*/
private static final String HASH_ALGORITHM = "HmacSHA256";
/**
* The empty string.
*/
private static final String EMPTY = "";
/**
* The HMAC key database.
*/
private final HmacKeyDB mCache;
/**
* Constructor.
* @param context Context
*/
public HmacManager(final Context context) {
if (context == null) {
throw new IllegalArgumentException("context is null.");
}
mCache = new HmacKeyDB(context);
}
/**
* Updates HMAC key by the key included in the specified request.
*
* @param origin Origin of application
* @param key HMAC key
*/
public void updateKey(final String origin, final String key) {
if (origin == null) {
throw new IllegalArgumentException("origin is null.");
}
if (origin.equals(EMPTY)) {
throw new IllegalArgumentException("origin is an empty string.");
}
if (key == null) {
throw new IllegalArgumentException("key is null.");
}
if (key.equals(EMPTY)) {
mCache.removeKey(origin);
} else {
mCache.addKey(origin, key);
}
}
/**
* Returns whether the client uses HMAC or not.
*
* @param origin Origin of application
* @return true if the client uses HMAC, otherwise false
*/
public boolean usesHmac(final String origin) {
if (origin == null) {
throw new IllegalArgumentException("origin is null.");
}
return mCache.hasKey(origin);
}
/**
* Generates HMAC.
*
* @param origin Origin of application
* @param nonce Nonce
* @return HMAC
*/
public String generateHmac(final String origin, final String nonce) {
if (origin == null) {
throw new IllegalArgumentException("origin is null.");
}
if (nonce == null) {
throw new IllegalArgumentException("nonce is null.");
}
HmacKey hmacKey = mCache.getKey(origin);
if (hmacKey == null) {
return null;
}
// HMAC generation with key and nonce.
try {
Mac mac = Mac.getInstance(HASH_ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(toByteArray(hmacKey.getKey()), HASH_ALGORITHM);
mac.init(keySpec);
byte[] hmac = mac.doFinal(toByteArray(nonce));
return toHexString(hmac);
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(HASH_ALGORITHM + " is not supported.");
} catch (InvalidKeyException e) {
throw new RuntimeException("keySpec is null.");
}
}
/**
* Parse a hex string expression of a byte array to raw.
* @param b a hex string expression of a byte array
* @return A raw byte array
*/
private static byte[] toByteArray(final String b) {
String c = b;
if (c.length() % 2 != 0) {
c = "0" + c;
}
byte[] array = new byte[b.length() / 2];
for (int i = 0; i < b.length() / 2; i++) {
String hex = b.substring(2 * i, 2 * i + 2);
array[i] = (byte) Integer.parseInt(hex, 16);
}
return array;
}
/**
* Returns a hex string expression of a byte array.
*
* @param b A byte array
* @return A string expression of a byte array
*/
private static String toHexString(final byte[] b) {
if (b == null) {
throw new IllegalArgumentException("b is null.");
}
StringBuilder str = new StringBuilder();
for (int i = 0; i < b.length; i++) {
String substr = Integer.toHexString(b[i] & 0xff);
if (substr.length() < 2) {
str.append("0");
}
str.append(substr);
}
return str.toString();
}
/**
* HMAC key database.
*/
private static class HmacKeyDB extends SQLiteOpenHelper {
/**
* The DB file name.
*/
private static final String DB_NAME = "__device_connect_hmac.db";
/**
* The DB Version.
*/
private static final int DB_VERSION = 1;
/**
* The table name.
*/
private static final String TABLE_NAME = "HmacKey";
/**
* The unique ID for a row.
* <p>Type: INTEGER (long)</p>
*/
private static final String ID = BaseColumns._ID;
/**
* The origin.
* <p>Type: TEXT</p>
*/
private static final String ORIGIN = "origin";
/**
* The HMAC key.
* <p>Type: TEXT</p>
*/
private static final String HMAC_KEY = "hmac_key";
/**
* CREATE TABLE statement.
*/
private static final String CREATE = "CREATE TABLE " + TABLE_NAME + " ("
+ ID + " INTEGER PRIMARY KEY AUTOINCREMENT, "
+ ORIGIN + " TEXT NOT NULL, "
+ HMAC_KEY + " TEXT NOT NULL, UNIQUE(" + ORIGIN + "));";
/**
* DROP TABLE statement.
*/
private static final String DROP = "DROP TABLE IF EXISTS " + TABLE_NAME;
/**
* Constructor.
* @param context Context
*/
public HmacKeyDB(final Context context) {
super(context, DB_NAME, null, DB_VERSION);
}
@Override
public void onCreate(final SQLiteDatabase db) {
db.execSQL(CREATE);
}
@Override
public void onUpgrade(final SQLiteDatabase db, final int oldVersion, final int newVersion) {
db.execSQL(DROP);
}
/**
* Gets HMAC key for origin.
* @param origin Origin
* @return HMAC key
*/
synchronized HmacKey getKey(final String origin) {
SQLiteDatabase db = openDB();
if (db == null) {
return null;
}
Cursor c = db.query(TABLE_NAME, null, ORIGIN + "=?", new String[]{origin}, null, null, null);
HmacKey key = null;
if (c.moveToFirst()) {
key = new HmacKey(c.getString(1), c.getString(2));
}
c.close();
db.close();
return key;
}
/**
* Checks whether HMAC key exists for the specified origin.
* @param origin Origin
* @return true if HMAC key exists for the specified origin, otherwise false.
*/
boolean hasKey(final String origin) {
return getKey(origin) != null;
}
/**
* Adds a HMAC key for the specified origin.
* @param origin Origin
* @param key HMAC key
* @return Result
*/
synchronized HmacKeyError addKey(final String origin, final String key) {
if (origin == null) {
throw new IllegalArgumentException("origin is null");
}
if (origin.equals(EMPTY)) {
throw new IllegalArgumentException("origin is an empty string.");
}
if (key == null) {
throw new IllegalArgumentException("key is null");
}
if (key.equals(EMPTY)) {
throw new IllegalArgumentException("key is an empty string.");
}
SQLiteDatabase db = openDB();
if (db == null) {
return HmacKeyError.FAILED;
}
Cursor c = null;
try {
db.beginTransaction();
c = db.query(TABLE_NAME, null, ORIGIN + "=?", new String[]{origin}, null, null, null);
if (c.getCount() == 0) {
ContentValues values = new ContentValues();
values.put(ORIGIN, origin);
values.put(HMAC_KEY, key);
long id = db.insert(TABLE_NAME, null, values);
if (id == -1) {
return HmacKeyError.FAILED;
}
} else if (c.moveToFirst()) {
long id = c.getLong(0);
ContentValues values = new ContentValues();
values.put(HMAC_KEY, key);
int count = db.update(TABLE_NAME, values, ID + "=?", new String[]{"" + id});
if (count != 1) {
return HmacKeyError.FAILED;
}
} else {
return HmacKeyError.FAILED;
}
db.setTransactionSuccessful();
} finally {
if (c != null) {
c.close();
}
db.endTransaction();
db.close();
}
return HmacKeyError.NONE;
}
/**
* Removes a HMAC key for the specified origin.
* @param origin Origin
* @return Result
*/
synchronized HmacKeyError removeKey(final String origin) {
if (origin == null) {
throw new IllegalArgumentException("origin is null");
}
if (origin.equals(EMPTY)) {
throw new IllegalArgumentException("origin is an empty string.");
}
SQLiteDatabase db = openDB();
if (db == null) {
return HmacKeyError.FAILED;
}
try {
int count = db.delete(TABLE_NAME, ORIGIN + "=?", new String[]{origin});
if (count != 1) {
return HmacKeyError.FAILED;
}
} finally {
db.close();
}
return HmacKeyError.NONE;
}
/**
* Opens the database.
*
* @return An instance of the database
*/
SQLiteDatabase openDB() {
SQLiteDatabase db;
try {
db = getWritableDatabase();
} catch (SQLiteException e) {
db = null;
}
return db;
}
}
}