For PII purposes, we are encrypting the database fields like email etc.
Now for exact match queries we are also keeping a hashed form(HMAC) for the fields.
But how to run the autosuggestion from Solr / like queries from MySQL.
My encryption code is
public String encrypt(byte[] plaintext, byte[] dataKey, String version) throws Exception {
long startTime = System.currentTimeMillis();
// Generate Initialization Vector
byte[] IV = generateIV();
// Get Cipher Instance
Cipher cipher = getCipher();
// Store Version
byte[] versionArr = new byte[3];
versionArr = version.getBytes();
// Generate Key
SecretKeySpec keySpec = new SecretKeySpec(dataKey, "AES");
// Create GCMParameterSpec
GCMParameterSpec gcmParameterSpec = new GCMParameterSpec(GCM_TAG_SIZE_BYTES * 8, IV);
// Initialize Cipher for ENCRYPT_MODE
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmParameterSpec);
// Perform Encryption
byte[] cipherText = cipher.doFinal(plaintext);
int capacity = 3 + GCM_IV_SIZE_BYTES + plaintext.length + GCM_TAG_SIZE_BYTES;
// Create ByteBuffer & add IV & CipherText
ByteBuffer buffer = ByteBuffer.allocate(capacity);
buffer.put(versionArr);
buffer.put(IV);
buffer.put(cipherText);
long endTime = System.currentTimeMillis();
// return the final encrypted cipher txt
return Base64.getEncoder().encodeToString(buffer.array());
}
private static byte[] generateIV() {
final Random r = new SecureRandom();
byte[] IV = new byte[GCM_IV_SIZE_BYTES];
r.nextBytes(IV);
return IV;
}
private static Cipher getCipher() {
try {
return Cipher.getInstance("AES/GCM/NoPadding");
} catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
e.printStackTrace();
}
return null;
}
Short answer, it’s possible, but quite difficult.
Long answer:
One of the basics of hashes is that the hash changes a lot, by changing the input only a little, so there is no way to know if a hash is close to matching the input value.
Now you might think inputting the hash into the search engine might work and with some customization to the tokenizer (the thing that takes the search input and splits it into small parts for the engine to match), this would actually work. However, you did just make it possible to reverse the hash, let me explain:
For autocomplete of a single field, the tokenizer would use an edge n-gram. What this does is splitting a single string into multiple tokens that can be exactly matched, for example john#gmail.com would be split into:
j, jo, joh, john, john#, …, john#gmail.c, john#gmail.co, john#gmail.com
Now the search engine can search for exact matches on all the tokens and recommend the closest matches as possible values.
If you would customize the tokenizer, to hash everything before it is stored, and then hased the input upon search, this would absolutely work, but now lets look at what happens when an attacker gets the hashed tokenized data.
As in the example with john#gmail.com the first stored value would be the hash of j, which by brute force would take a fraction of a second to reverse. Since you now know the first letter, the second letter would go just as fast and so on. Rendering the whole point of using the hash in the first case obsolete.
ps. A secure hashing algorithm with a salt might work, but the tokenization is almost always done by the search engine, so the compute part of the search engine has to be "secure"
Sources:
https://en.wikipedia.org/wiki/N-gram
Related
This is the response I'm getting: The request signature we calculated does not match the signature you provided.
Here is the code I'm using to generate the signature:
static byte[] HmacSHA256(String data, byte[] key)
{
String algorithm = "HmacSHA256";
KeyedHashAlgorithm kha = KeyedHashAlgorithm.Create(algorithm);
kha.Key = key;
return kha.ComputeHash(Encoding.UTF8.GetBytes(data));
}
static byte[] getSignatureKey(String key, String dateStamp, String regionName, String serviceName)
{
byte[] kSecret = Encoding.UTF8.GetBytes(("AWS4" + key).ToCharArray());
byte[] kDate = HmacSHA256(dateStamp, kSecret);
byte[] kRegion = HmacSHA256(regionName, kDate);
byte[] kService = HmacSHA256(serviceName, kRegion);
byte[] kSigning = HmacSHA256("aws4_request", kService);
return kSigning;
}
When I use Amazon's test settings, I get the correct signature
key = 'wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY'
dateStamp = '20120215'
regionName = 'us-east-1'
serviceName = 'iam'
However, when I put my live settings in, I get the not matching error.
Here is what I'm using to get my signature:
var reqSig = getSignatureKey("[my secret key]", dateStamp, "us-west-2","AWSECommerceService");
This is what I'm submitting (I'm just testing it in a browser, for now):
ecs.amazonaws.com/onca/xml?Service=AWSECommerceService&AWSAccessKeyId=[my
access key that corresponds with the secret
key]&Operation=ItemLookup&IdType=UPC&ItemId=635753490879&Timestamp=2019-01-24T19:14:55.2644840Z&Signature=32BA07ECE67F3177BF2EA02923E624D612A45FAA144ED0E43BDDC0DF6574EAC3
I'm not sure if it has to do with the region -- us-west-2 in my case -- because there is no place on the request for a region. I'm not sure how Amazon can test my signature against my parameters if I can't specify what region it is and I've used the region to calculate the signature.
What am I missing?
Ok. After spending days going through this, here is what I had to do:
/*
DOCUMENTATION: https://docs.aws.amazon.com/AWSECommerceService/latest/DG/rest-signature.html#rest_detailedexample
*/
var itemID = "0679722769";
var accessKeyID = "AKIAIOSFODNN7EXAMPLE";
var timeStamp = DateTime.UtcNow.ToString("o");
var req = $"Service=AWSECommerceService&AWSAccessKeyId={accessKeyID}&Operation=ItemLookup&IdType=UPC&ItemId={itemID}&Version=2013-08-01&Timestamp={timeStamp}";
req = req.Replace(":", "%3A").Replace(",", "%2C"); //UrlDecode certain characters
var reqlist = req.Split('&').ToArray(); //we need to sort our key/value pairs
Array.Sort(reqlist);
req = String.Join("&", reqlist); //join everything back
var reqToSign = $#"GET
webservices.amazon.com
/onca/xml
{req}".Replace("\r", ""); //create the request for signing. We need to replace microsofts's crlf with just a lf; Make sure there are no leading spaces after the linefeeds.
var signage = getSignatureKey("1234567890",reqToSign);
req = $"http://webservices.amazon.com/onca/xml?{req}&Signature={signage}"; //create our request with the signature appended.
return req;
}
private static byte[] HmacSHA256(String data, byte[] key)
{
String algorithm = "HmacSHA256";
KeyedHashAlgorithm kha = KeyedHashAlgorithm.Create(algorithm);
kha.Key = key;
return kha.ComputeHash(Encoding.UTF8.GetBytes(data));
}
private static string getSignatureKey(string key, string stringToSign)
{
byte[] kSecret = Encoding.UTF8.GetBytes(key.ToCharArray());
byte[] kSigning = HmacSHA256(stringToSign, kSecret);
return WebUtility.UrlEncode(Convert.ToBase64String(kSigning));
}
Contrary to most of the answers found here and elsewhere, this is the only way that works. The entire request has to be hashed, not just particular parameters. I can't speak to other Amazon services, but the Commerce Service has to be done like this.
Quite a few answers referenced this: https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html or this: Amazon API generating a request signature in C# .NET
As I stated in my question, this is most certainly not correct. If you're not passing a region parameter, how can Amazon create the same signature since it doesn't have all the information.
Anyway, this works now.
Our database contains thousands of numbers in various formats and what I am attempting to do is remove all punctuation at index time and store only the digits and then when a user types digits into a keyword field, only match on those digits. I thought that a custom analyzer was the way to go but I think I am missing an important step...
#Override
protected TokenStreamComponents createComponents(String fieldName) {
log.debug("Creating Components for Analyzer...");
final Tokenizer source = new KeywordTokenizer();
LowerCaseFilter lcFilter = new LowerCaseFilter(source);
PatternReplaceFilter prFilter = new PatternReplaceFilter(lcFilter,
Pattern.compile("[^0-9]"), "", true);
TrimFilter trimFilter = new TrimFilter(prFilter);
return new TokenStreamComponents(source, trimFilter);
}
...
#KeywordSearch
#Analyzer(impl = com.jjkane.common.search.analyzer.PhoneNumberAnalyzer.class)
#Field(name = "phone", index = org.hibernate.search.annotations.Index.YES, analyze = Analyze.YES, store = Store.YES)
public String getPhone() {
return this.phone;
}
This may just be ignorance on my part in attempting to do this... From all the documentation, it seems like I am on the right track, but the query never matches unless I submit (555)555-5555 as an exact match to what was in my db. If I put in 5555555555, I get nothing...
To give you some background, my team and I are creating a program that stores usernames and passwords in a database. We are using Java and interacting with the Database through java code.
We use Jasypt to encrypt the usernames and passwords. I am using the BasicPasswordEncryptor in Jasypt to encrypt both. The usernames encrypt fine and are stored in the database fine. However, when the login is checked and said BasicPasswordEncryptor attempts to check the plaintext username against the encrypted password, it always returns false. I have done a series of checks to focus down where the problem is occuring. As far as I know, it's a problem with Jasypt. Does anyone know what the problem is, a possible solution, or a more optimal method? Thank you. I will post the code.
Here is where the encryption occurs.
public void register(String userName, String passWord){
String encryptedUsername = e.encryptPassword(userName);
String encryptedPassword = e.encryptPassword(passWord);
System.out.println("Registered eU: " + encryptedUsername);
try {
con = DriverManager.getConnection("jdbc:mysql://localhost:3306/PandaBox", "root", "");
statement = con.prepareStatement("insert into Users (username, password, logged) values (?,?,?)");
statement.setString(1, encryptedUsername);
statement.setString(2, encryptedPassword);
statement.setInt(3, 0);
boolean x = statement.execute();
System.out.println("IT REGISTERED");
} catch (SQLException o) {
o.printStackTrace();
}
}
Where "e" is the BasicPasswordEncryptor object. Here is the login check.
public boolean checkLogin(String inputedUsername, String inputedPassword) {
try {
con = DriverManager.getConnection("jdbc:mysql://localhost:3306/PandaBox", "root", "");
statement = con.prepareStatement("select * from Users");
rs = statement.executeQuery();
System.out.println(inputedUsername + " / " + inputedPassword);
while(rs.next()){
String usernameInDatabase = rs.getString("username");
System.out.println(usernameInDatabase);
if (e.checkPassword(inputedUsername, usernameInDatabase)) {
System.out.println("Username correct.");
statement = con.prepareStatement("select password from Users where username = ?");
statement.setString(1, usernameInDatabase);
rs = statement.executeQuery();
String passwordInDatabase = rs.toString();
if(passwordIsCorrect(inputedPassword, passwordInDatabase)){
return true;
}
}
}
return false;
} catch (SQLException o) {
// TODO Auto-generated catch block
o.printStackTrace();
return false;
}
}
I'm jasypt's author.
From your message, it isn't clear to me whether you are observing this issue when matching the user name or the password --you say 'attempts to check the plaintext username against the encrypted password', which makes no sense--. Nevertheless, one of the most common reasons for problems like yours is that your database columns are not big enough for storing your hashed user names and/or passwords.
The size of the hashing result will depend on the algorithm and salt configuration being used, but for a BasicPasswordEncryptor, which uses MD5 and a salt size of 8 bytes, you should expect your hashes to be 16-byte (hash) plus 8 bytes (salt), plus 8 additional bytes because of textual Base64 encoding. A total of 32 bytes.
Also think that many DBMS measure varchar fields in chars and not bytes, so you should do the appropiate conversion depending on the character encoding being used at your table.
I always recommend to check column sizes first because many DBMS's do not raise an error if you try to store a varchar which is too long for a column --they simply truncate it. I don't know MySQL's behaviour, but Oracle does exactly this. And when you try to decrypt it back... it doesn't match.
So checking your column sizes could be a good starting point. And remember jasypt has a users forum at http://forum.jasypt.org
Oh, and by the way-- forgive me if this is just ad-hoc demo code, but just in case: you should make sure you close all your Statement and ResultSet objects in 'finally' blocks before reusing them... so you should use different 'statement' and 'rs' variables in the inner iteration block, and close them each time.
Regards.
Optimisation 1 : Use a WHERE clause.
I'm working (actually more like playing) around with the Bit.ly API, and keep getting the error in the title of this question. So I'm going to show you the code and hopefuly someone can help me resolve this. First the client side code.
var x = service.GetClicks(url, service.BitlyLogin, service.BitlyAPIKey);
Console.WriteLine(x);
Console.ReadLine();
And this is the code that's being called
public List<int> GetClicks(string url, string login, string key)
{
List<int> clicks = new List<int>();
url = Uri.EscapeUriString(url);
string reqUri =
String.Format("http://api.bit.ly/v3/clicks?" +
"login={0}&apiKey={1}&shortUrl={2}&format=xml" +
login, key, url);
HttpWebRequest req = (HttpWebRequest)WebRequest.Create(reqUri);
req.Timeout = 10000; // 10 seconds
Stream stm = req.GetResponse().GetResponseStream();
XmlDocument doc = new XmlDocument();
doc.Load(stm);
// error checking for xml
if (doc["response"]["status_code"].InnerText != "200")
throw new WebException(doc["response"]["status_txt"].InnerText);
XmlElement el = doc["response"]["data"]["clicks"];
clicks.Add(int.Parse(el["global_clicks"].InnerText));
clicks.Add(int.Parse(el["user_clicks"].InnerText));
return clicks;
}
As you can see it's very simple code, nothing complicated, and I can see nothing that causes this error. Anyone out there who has worked with(the full error is Index (zero based) must be greater than or equal to zero and less than the size of the argument list.) the Bit.ly API and can lend a hand?
Instead this
string reqUri =
String.Format("http://api.bit.ly/v3/clicks?" +
"login={0}&apiKey={1}&shortUrl={2}&format=xml" + login, key, url);
Use this
string reqUri = String.Format("http://api.bit.ly/v3/clicks?login={0}&apiKey={1}&shortUrl={2}&format=xml", login, key, url);
Notice that I just changed the plus sign with the comma before "login, key, url);" at the end of the String.Format().
I narrowed it down to a place where I was using string.Format to build an array and has less in the string.Format than what was supposed to. I had it go to Index 3 but only filled to Index 2
Not for your specific case, but I ran into this: make sure that, if you have multiple parameters, you send them as an array of objects instead of an IEnumerable:
IEnumerable<object> myArgs = ...;
string toFormat = "{0} xyz {1}";
String.Format(toFormat, myArgs);
// ERROR, since myArgs is one argument whereas the string template requires two
String.Format(toFormat, myArgs.ToArray());
// Valid, as the Format() accepts an array of objects to fill all arguments in the string
I am generating salt and hash values from my passwords by using,
string salt = CreateSalt(TxtPassword.Text.Length);
string hash = CreatePasswordHash(TxtPassword.Text, salt);
private static string CreateSalt(int size)
{
//Generate a cryptographic random number.
RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();
byte[] buff = new byte[size];
rng.GetBytes(buff);
// Return a Base64 string representation of the random number.
return Convert.ToBase64String(buff);
}
private static string CreatePasswordHash(string pwd, string salt)
{
string saltAndPwd = String.Concat(pwd, salt);
string hashedPwd =
FormsAuthentication.HashPasswordForStoringInConfigFile(
saltAndPwd, "sha1");
return hashedPwd;
}
What datatype you would suggest for storing these values in sql server? Any suggestion...
Salt:9GsPWpFD
Hash:E778AF0DC5F2953A00B35B35D80F6262CDBB8567
ASPNET_DB says this - can't go wrong.
Password nvarchar(128) NOT NULL,
PasswordSalt nvarchar(128) NOT NULL,
while 128 may seem like a lot, various types of encryption can result in larger strings than you started out with. There is absolutely no reason not to follow the lead of the very smart people who have spend thousands of man hours developing the asp.net membership system.
We store our passwords as a binary SHA512 hash