Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
vladimir-ivanov-epam committed Oct 20, 2018
1 parent 0c0273d commit 59b9283
Show file tree
Hide file tree
Showing 56 changed files with 1,500 additions and 0 deletions.
1 change: 1 addition & 0 deletions app/.gitignore
@@ -0,0 +1 @@
/build
75 changes: 75 additions & 0 deletions app/build.gradle
@@ -0,0 +1,75 @@
apply plugin: 'com.android.library'
apply plugin: 'com.jfrog.artifactory'
apply plugin: 'maven-publish'

def keystorePropertiesFile = rootProject.file("keystore.properties")
/*
you should create file keystore.properties and fields like this
username=Andre_Gus@epam.com
password=""
*/
def keystoreProperties = new Properties()
keystoreProperties.load(new FileInputStream(keystorePropertiesFile))
android {
compileSdkVersion 27
defaultConfig {
minSdkVersion 21
targetSdkVersion 27
versionCode 1
versionName "1.0"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
minifyEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
}
publishing {
publications {
aar(MavenPublication) {
def packageName = 'com.epam.android.keystore'
def libraryVersion = '1.0.0'
groupId packageName
version = libraryVersion
artifactId project.getName()

// Tell maven to prepare the generated "*.aar" file for publishing
artifact("$buildDir/outputs/aar/${project.getName()}-release.aar")
}
}
}

artifactory {
contextUrl = 'https://artifactory.epam.com/artifactory'
publish {
repository {
// The Artifactory repository key to publish to
repoKey = 'libs-release-local'

username = keystoreProperties['username']
password = keystoreProperties['password']
}
defaults {
// Tell the Artifactory Plugin which artifacts should be published to Artifactory.
publications('aar')
publishArtifacts = true

// Properties to be attached to the published artifacts.
properties = ['qa.level': 'basic', 'dev.team': 'core']
// Publish generated POM files to Artifactory (true by default)
publishPom = true
}
}
}

dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])

compileOnly "com.android.support:support-annotations:27.1.1"

testImplementation 'junit:junit:4.12'
androidTestImplementation('com.android.support.test:runner:1.0.1')
androidTestImplementation('com.android.support.test.espresso:espresso-core:3.0.1')
}
21 changes: 21 additions & 0 deletions app/proguard-rules.pro
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html

# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable

# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,114 @@
package com.epam.android.keystore;

import android.content.Context;
import android.support.test.InstrumentationRegistry;
import android.support.test.runner.AndroidJUnit4;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;


import static org.junit.Assert.*;

/**
* Instrumented test, which will execute on an Android device.
* This test need launch in two devices for 18 - 22 and 23-27 version API
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class StorageReadWriteInstrumentedTest {
Context context;
SecureStorage storage;

@Before
public void before() throws Exception {
context = InstrumentationRegistry.getTargetContext();
storage = new SecureStorage();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.M) {
storage.setStrategy(new SafeStorageM(context));
} else
storage.setStrategy(new SafeStoragePreM(context));
}

@Test(expected = IllegalArgumentException.class)
public void shouldThrowIllegalArgumentException() throws Exception {
storage.get(null);
}

@Test
public void shouldGetNullValueIfNotSet() throws Exception {
String value = storage.get("blabla");
assertEquals(null, value);
}

@Test
public void shouldSaveValue() throws Exception {
storage.save("key", "passWORD");
assertEquals("passWORD", storage.get("key"));
}

@Test
public void shouldSaveOtherKeyValue() throws Exception {
storage.save("key1", "passWORD");
assertEquals("passWORD", storage.get("key1"));
}

@Test
public void shouldSaveOtherKeyValue2() throws Exception {
storage.save("key1", "passWORD");
assertEquals("passWORD", storage.get("key1"));
storage.save("key2", "passWORD");
assertEquals("passWORD", storage.get("key2"));
assertEquals("passWORD", storage.get("key1"));
storage.get("key1");
assertEquals("passWORD", storage.get("key2"));
assertEquals("passWORD", storage.get("key1"));
}

@Test
public void shouldClearStorage() throws Exception {
storage.save("key12", "1");
assertEquals("1", storage.get("key12"));
storage.clear("key12");
assertNull(storage.get("key12"));
}

@Test
public void shouldEraseValues() throws Exception {
storage.save("key123", "12093qqwoiejqow812312312123poqj[ 9wpe7nrpwiercwe9rucpn[w9e7rnc;lwiehr pb8ry");
assertEquals("12093qqwoiejqow812312312123poqj[ 9wpe7nrpwiercwe9rucpn[w9e7rnc;lwiehr pb8ry", storage.get("key123"));
storage.erase();
assertNotEquals("12093qqwoiejqow812312312123poqj[ 9wpe7nrpwiercwe9rucpn[w9e7rnc;lwiehr pb8ry", storage.get("key123"));
assertEquals(null, storage.get("key123"));
}

@Test
public void shouldReturnNullIfNoKeyWithWhitespaces() throws Exception {
assertEquals(null, storage.get("bad key"));
}

@Test
public void shouldSaveValueForKeyWithWhitespaces() throws Exception {
storage.save("KEY", "@");
assertEquals(null, storage.get("bad key"));
}

@Test
public void shouldClearForKey() throws Exception {
storage.save("KEY", "@");
storage.clear("KEY");
assertEquals(null, storage.get("KEY"));
}

@Test
public void shouldClearKeys() throws Exception {
storage.save("KEY", "1");
storage.save("KEY2", "2");
storage.clear("KEY");
assertEquals("2", storage.get("KEY2"));
storage.erase();
assertEquals(null, storage.get("KEY2"));
}
}
11 changes: 11 additions & 0 deletions app/src/main/AndroidManifest.xml
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.epam.android.keystore">

<application
android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true">
</application>

</manifest>
158 changes: 158 additions & 0 deletions app/src/main/java/com/epam/android/keystore/SafeStorageM.java
@@ -0,0 +1,158 @@
package com.epam.android.keystore;

import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.security.keystore.KeyGenParameterSpec;
import android.security.keystore.KeyProperties;
import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi;
import android.util.Base64;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.UnrecoverableEntryException;
import java.util.Arrays;

import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;

import static com.epam.android.keystore.SecureStorage.ANDROID_KEY_STORE;
import static com.epam.android.keystore.SecureStorage.KEY_ALIAS;

public class SafeStorageM implements SensitiveInfoModule {

private static final java.lang.String AESGCMNOPADDING = "AES/CBC/PKCS7Padding";
private static final String I_VECTOR = "valueV";
private SecretKey secretKey;
private Cipher cipher;
private SharedPreferences preferences;
private KeyStore keyStore;

@RequiresApi(api = Build.VERSION_CODES.M)
public SafeStorageM(Context context) throws Exception {
cipher = Cipher.getInstance(AESGCMNOPADDING);
secretKey = initSecretKey(KEY_ALIAS);
preferences = PreferenceManager.getDefaultSharedPreferences(context);
}

@RequiresApi(api = Build.VERSION_CODES.M)
public SafeStorageM(SharedPreferences preferences) throws Exception {
cipher = Cipher.getInstance(AESGCMNOPADDING);
secretKey = initSecretKey(KEY_ALIAS);
this.preferences = preferences;
}

@RequiresApi(api = Build.VERSION_CODES.M)
private SecretKey generatorKey(String alias) throws Exception {
KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec
.Builder(alias, KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT)
.setBlockModes(KeyProperties.BLOCK_MODE_CBC)
.setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_PKCS7)
.build();
KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, ANDROID_KEY_STORE);
keyGenerator.init(keyGenParameterSpec);
return keyGenerator.generateKey();
}

@RequiresApi(api = Build.VERSION_CODES.M)
private SecretKey initSecretKey(String alias) throws Exception {
keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
keyStore.load(null);
if (keyStore.containsAlias(alias)) {
KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(alias, null);
return secretKeyEntry.getSecretKey();
} else {
return generatorKey(alias);
}
}

@Override
public void erase() throws KeyStoreException {
keyStore.deleteEntry(KEY_ALIAS);
}

@Override
public void save(String key, String password) throws SecureStorageException {
try {
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
putPref(I_VECTOR + key, Arrays.toString(cipher.getIV()));
byte[] encryption = cipher.doFinal(password.getBytes("UTF-8"));
String encryptedBase64Encoded = Base64.encodeToString(encryption, Base64.DEFAULT);
putPref(key, encryptedBase64Encoded);
} catch (InvalidKeyException | IOException | BadPaddingException | IllegalBlockSizeException e) {
e.printStackTrace();
throw new SecureStorageException("Error save or cypher value to the storage");
}
}

@Override
public void clear(String key) {
preferences.edit().remove(key).apply();
}

@Nullable
@RequiresApi(api = Build.VERSION_CODES.M)
@Override
public String get(String key) throws SecureStorageException {
if (key == null || key.isEmpty()) {
throw new IllegalArgumentException("Key should not be null or empty");
}

if (!isSet(I_VECTOR + key) || !isSet(key)) {
return null;
}

try {
String value = getPref(key);
byte[] iv = getByteArray(getPref(I_VECTOR + key));
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore.getEntry(KEY_ALIAS, null);
if (secretKeyEntry == null) return null;
cipher.init(Cipher.DECRYPT_MODE, secretKeyEntry.getSecretKey(), ivParameterSpec);
if (value.isEmpty()) return null;
return new String(cipher.doFinal(Base64.decode(value, Base64.DEFAULT)), StandardCharsets.UTF_8);
} catch (InvalidKeyException | BadPaddingException | IllegalBlockSizeException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | UnrecoverableEntryException | KeyStoreException e) {
e.printStackTrace();
throw new SecureStorageException("Error get value from the storage");
}
}

@Nullable
private byte[] getByteArray(String stringArray) {
if (stringArray != null) {
String[] split = stringArray.substring(1, stringArray.length() - 1).split(", ");
byte[] array = new byte[split.length];
for (int i = 0; i < split.length; i++) {
array[i] = Byte.parseByte(split[i]);
}
return array;
} else
return null;
}

private boolean isSet(String key) {
return preferences.contains(key);
}

private String getPref(String key) {
return preferences.getString(key, "");
}

private void putPref(String key, String value) {
preferences.edit().putString(key, value).apply();
}

}

0 comments on commit 59b9283

Please sign in to comment.