Skip to content

Commit

Permalink
Based file encryption and decryption
Browse files Browse the repository at this point in the history
  • Loading branch information
fireduck64 committed Jul 17, 2019
1 parent 50a5799 commit 2ec4536
Show file tree
Hide file tree
Showing 7 changed files with 295 additions and 19 deletions.
6 changes: 6 additions & 0 deletions WORKSPACE
Expand Up @@ -116,6 +116,12 @@ maven_jar(
sha1 = "1b6c4ff09ce03f3052429139c2a68e295cae6604",
)

maven_jar(
name = "scrypt",
artifact = "com.lambdaworks:scrypt:1.4.0",
sha1 = "906506b74f30c8c20bccd9ed4a11112d8941fe87",
)

# Used for HD wallet and seed stuff only
maven_jar(
name = "bitcoinj",
Expand Down
1 change: 1 addition & 0 deletions client/BUILD
Expand Up @@ -23,6 +23,7 @@ java_library(
"@slf4j_api//jar",
"@io_grpc_grpc_java//netty",
"@io_netty_netty_handler//:io_netty_netty_handler",
"@scrypt//jar",
],
)

Expand Down
196 changes: 196 additions & 0 deletions client/src/PasswordCrypt.java
@@ -0,0 +1,196 @@
package snowblossom.client;

import com.google.protobuf.ByteString;
import com.lambdaworks.crypto.SCrypt;
import java.security.Key;
import java.security.MessageDigest;
import java.util.Random;
import java.util.TreeMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import snowblossom.proto.*;

/**
* Simple, but hopefully secure encrypting and decrypting of data
*/
public class PasswordCrypt
{
private static final Logger logger = Logger.getLogger("snowblossom.client");

private static TreeMap<String, ByteString> scrypt_params_to_key_map = new TreeMap<>();

public static final int SCRYPT_MEMORY_COST=2;
public static final int SCRYPT_PARALLELIZATION_COST=128;
public static final int SCRYPT_CPU_COST=128*1024;
public static final String SCRYPT_SALT="snowblossom";
public static final String ENCRYPTION_MODE = "AES/CBC/PKCS5PADDING";
public static final int BLOCK_SIZE = 16;

/**
* Attempts to parse the input file as a snowblossom.proto.EncryptedFile and decrypt with password.
* Only returns non-null if the checksum matches inside the encrypted payload
*
* @returns null on any trouble
*/
public static ByteString decrypt(ByteString input, String password)
{
try
{
EncryptedFile outer = EncryptedFile.parseFrom(input);
String function = outer.getFunction();
if (!function.equals("scrypt"))
{
throw new RuntimeException("Unknown function: " + function);
}
ByteString key = getScryptKey(password, outer, BLOCK_SIZE);
ByteString iv = outer.getIv();

Key k = new SecretKeySpec(key.toByteArray(), "AES");

Cipher cipher = Cipher.getInstance(ENCRYPTION_MODE);

cipher.init(Cipher.DECRYPT_MODE, k, new IvParameterSpec(iv.toByteArray()));

ByteString outer_payload = ByteString.copyFrom( cipher.doFinal( outer.getPayload().toByteArray() ) );

EncryptedFilePayload inner = EncryptedFilePayload.parseFrom(outer_payload);

MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(inner.getPayload().toByteArray());
ByteString hash_result = ByteString.copyFrom(md.digest());

if (hash_result.equals(inner.getSha256Hash()))
{
return inner.getPayload();
}

}
catch(com.google.protobuf.InvalidProtocolBufferException e)
{
// This will happen if the outer data isn't actually an EncryptedFile
// or we have the wrong key (based on wrong password) and can't parse the inner payload
return null;
}
catch(java.security.GeneralSecurityException e)
{
// If we have the wrong key, this could be a passing error (very likely)
return null;
}

// This is pretty much if everything decrytped and parsed, but the checksum doesn't match
// could possibly be the wrong key but by dumb luck the padding doesn't have an error
// and the protobuf somehow parsed (which might be easy, protobuf is pretty terse)
return null;
}


public static ByteString encrypt(ByteString input, String password, String function)
{
if (!function.equals("scrypt"))
{
throw new RuntimeException("Unknown function: " + function);
}
try
{

// The inner object that gets encrypted
// simple has the data payload and a hash of it
EncryptedFilePayload.Builder inner = EncryptedFilePayload.newBuilder();
inner.setPayload(input);

MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(input.toByteArray());
inner.setSha256Hash(ByteString.copyFrom(md.digest()));

ByteString data_payload = inner.build().toByteString();

// The outer data object, has the encrypted payload and parameter information
EncryptedFile.Builder file = EncryptedFile.newBuilder();

file.setFunction(function);
file.setScryptMemoryCost(SCRYPT_MEMORY_COST);
file.setScryptParallelizationCost(SCRYPT_PARALLELIZATION_COST);
file.setScryptCpuCost(SCRYPT_CPU_COST);

ByteString key = getScryptKey(password, file.build(), BLOCK_SIZE);

Cipher cipher = Cipher.getInstance(ENCRYPTION_MODE);

Random rnd = new Random();
byte[] iv = new byte[BLOCK_SIZE];
rnd.nextBytes(iv);

Key k = new SecretKeySpec(key.toByteArray(), "AES");

file.setIv(ByteString.copyFrom(iv));

cipher.init(Cipher.ENCRYPT_MODE, k, new IvParameterSpec(iv));

file.setPayload( ByteString.copyFrom( cipher.doFinal( data_payload.toByteArray() ) ) );

return file.build().toByteString();

}
catch(java.security.NoSuchAlgorithmException e)
{
throw new RuntimeException(e);
}
catch(java.security.GeneralSecurityException e)
{
throw new RuntimeException(e);
}

}

/**
* Get the key bytes based on a password using the params in EncryptedFile.
* Cache the result so we can decrypt/encrypt multiple files with the same
* password quickly
*/
protected static ByteString getScryptKey(String pass, EncryptedFile params, int key_len)
{
String param_token = String.format("%s.%d.%d.%d.%d", pass, params.getScryptCpuCost(),
params.getScryptMemoryCost(),
params.getScryptParallelizationCost(),
key_len);

synchronized(scrypt_params_to_key_map)
{
if (scrypt_params_to_key_map.containsKey(param_token))
{
return scrypt_params_to_key_map.get(param_token);
}
}

try
{
long t1 = System.currentTimeMillis();
byte[] key = SCrypt.scrypt(pass.getBytes(), SCRYPT_SALT.getBytes(),
params.getScryptCpuCost(),
params.getScryptMemoryCost(),
params.getScryptParallelizationCost(),
key_len);
long t2 = System.currentTimeMillis();

logger.log(Level.FINE, "Scrypt gen took: " + (t2 - t1) + " ms");

ByteString key_bs = ByteString.copyFrom(key);

synchronized(scrypt_params_to_key_map)
{
scrypt_params_to_key_map.put(param_token, key_bs);
}

return key_bs;

}
catch(java.security.GeneralSecurityException e)
{
throw new RuntimeException(e);
}
}

}
3 changes: 2 additions & 1 deletion client/test/AllTests.java
Expand Up @@ -9,7 +9,8 @@
WalletTest.class,
PurseTest.class,
SeedTest.class,
TransactionFactoryTest.class
TransactionFactoryTest.class,
PasswordCryptTest.class
})
public class AllTests
{}
76 changes: 76 additions & 0 deletions client/test/PasswordCryptTest.java
@@ -0,0 +1,76 @@
package client.test;

import com.google.protobuf.ByteString;
import java.util.Random;
import org.junit.Assert;
import org.junit.BeforeClass;
import org.junit.Test;
import snowblossom.client.PasswordCrypt;
import snowblossom.lib.Globals;

public class PasswordCryptTest
{

@BeforeClass
public static void loadProvider()
{
Globals.addCryptoProvider();
}

@Test
public void basicEncryptTest()
throws Exception
{
Random rnd = new Random();
for(int i=0; i<5; i++)
{
int len = 1+rnd.nextInt(100000);
byte[] in_data = new byte[len];
rnd.nextBytes(in_data);

ByteString enc_data = PasswordCrypt.encrypt(ByteString.copyFrom(in_data), "test_pass_1", "scrypt");

ByteString result = PasswordCrypt.decrypt(enc_data, "test_pass_1");
Assert.assertEquals(ByteString.copyFrom(in_data), result);
}
}

@Test
public void badPassTest()
throws Exception
{
Random rnd = new Random();
byte[] in_data = new byte[8192];
rnd.nextBytes(in_data);

ByteString enc_data = PasswordCrypt.encrypt(ByteString.copyFrom(in_data), "test_pass_1", "scrypt");

ByteString result = PasswordCrypt.decrypt(enc_data, "test_pass_2");

Assert.assertNull(result);
}

@Test
public void badDataTest()
throws Exception
{
Random rnd = new Random();

for(int i=0; i<100; i++)
{
int len = 16+rnd.nextInt(100000);

byte[] in_data = new byte[len];
rnd.nextBytes(in_data);

ByteString result = PasswordCrypt.decrypt(ByteString.copyFrom(in_data), "test_pass_1");

Assert.assertNull(result);
}

}




}
2 changes: 1 addition & 1 deletion client/test/SeedTest.java
Expand Up @@ -117,7 +117,7 @@ private void testGen(int words)
String xpub = SeedUtil.getSeedXpub( new NetworkParamsTestnet(), seed, "", 0);

for(int c=0; c<2; c++)
for(int i=0; i<100; i++)
for(int i=0; i<20; i++)
{
WalletKeyPair wkp = SeedUtil.getKey( new NetworkParamsTestnet(), seed, "", 0, c, i);

Expand Down
30 changes: 13 additions & 17 deletions protolib/snowblossom.proto
Expand Up @@ -341,6 +341,19 @@ message WalletDatabase {
map<string,SeedStatus> xpubs = 13;
}

message EncryptedFile {
bytes payload = 1; //Encrypted EncryptedFilePayload
string function = 2;
bytes iv = 3;
int32 scrypt_memory_cost = 4;
int32 scrypt_parallelization_cost = 5;
int32 scrypt_cpu_cost = 6;
}
message EncryptedFilePayload {
bytes payload = 1; // Actual data contents
bytes sha256_hash = 2;
}

message SeedStatus
{
bytes seed_id = 1;
Expand All @@ -365,23 +378,6 @@ message WalletKeyPair
int32 hd_index = 7;
}

message EncryptedPrivateKey
{
bytes cipher_id = 1;
bytes iv = 2;
bytes payload = 3;
}

message CipherKey
{
int64 cipher_id = 1;
int64 pbkdf_rounds = 2;
bytes salt = 3;
bytes public_key = 4;

}


message SignedMessage {
bytes payload = 1; //A serialized SignedMessagePayload
bytes signature = 2;
Expand Down

0 comments on commit 2ec4536

Please sign in to comment.