I'm implementing a simple password store using Blowfish. All was fine until I tried out a few different password/key combinations and came across numerous instances where the decrypted values were still garbage.
Below is a standalone class that demonstrates the issue. I get the following output:
'Aaaaaaa7' encrypted: 'r?—èLèdÓ,·Ã¸ÍÒ'*
'Aaaaaaa7' decrypted: 'ñü=€¼(T'*
Any idea what I need to do to guarantee it always decrypts correctly.
(Using jce.jar in JDK 1.6.0_26)
Thanks,
David
import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
public class BlowfishTwoWayHashImpl {
static {
test();
}
public static void test() {
String key = "wibble";
String passwordToEnrypt = "Aaaaaaa7";
String enc = BlowfishTwoWayHashImpl.encryptBlowfish(passwordToEnrypt, key);
System.out.println("'" + passwordToEnrypt + "' encrypted: '" + enc + "'");
String dec = BlowfishTwoWayHashImpl.decryptBlowfish(enc, key);
System.out.println("'" + passwordToEnrypt + "' decrypted: '" + dec + "'");
}
private static final String CIPHER_NAME = "Blowfish";
public static String encryptBlowfish(String toEncrypt, String key) {
return processString(toEncrypt, key, Cipher.ENCRYPT_MODE);
}
public static String decryptBlowfish(String toDecrypt, String key) {
return processString(toDecrypt, key, Cipher.DECRYPT_MODE);
}
private static String processString(String toEncrypt, String key, int encryptDecryptMode) {
SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes(), CIPHER_NAME);
Cipher cipher;
try {
cipher = Cipher.getInstance(CIPHER_NAME);
cipher.init(encryptDecryptMode, secretKeySpec);
return new String(cipher.doFinal(toEncrypt.getBytes()));
}
catch (Exception e) {
throw new RuntimeException(e.toString());
}
}
}
Don't do this:
return new String(cipher.doFinal(toEncrypt.getBytes()));
You're using the platform default encoding all over the place in your code. Don't do this. It will lose data.
When you're converting genuine text to bytes (e.g. on encryption) use a specific charset - UTF-8 is a good choice. Use the same charset to decode from "encoded text" to String.
When you're converting arbitrary binary data to text, use base64 encoding, e.g. via this public domain Base64 encoding library.
Basically, when you create a new string with the String(byte[]) or String(byte[], String) constructors, you're saying, "This is genuine text data - please just decode it as a string." When the data is actually the result of encryption, it's not text data... it's an arbitrary bunch of bytes.
Related
I need to create two simple methods for string DES encryption/decruption. The goal is to have these two methods in the following form
public static String desEcnrypt(String key, String clearMessage)
{
.....
}
public static String desDecrypt(String key, String encryptedMessage)
{
.....
}
I haven't found yet any example in this form.
Use the "not-yet-commons-ssl.jar" from http://juliusdavies.ca/commons-ssl/.
http://juliusdavies.ca/commons-ssl/pbe.html
PBE code example (DES-3):*
char[] password = {'c','h','a','n','g','e','i','t'};
byte[] data = "Hello World!".getBytes();
// Encrypt!
byte[] encrypted = OpenSSL.encrypt("des3", password, data);
System.out.println("ENCRYPTED: [" + new String(encrypted) + "]");
// Decrypt results of previous!
data = OpenSSL.decrypt("des3", password, encrypted);
System.out.println("DECRYPTED: [" + new String(data) + "]");
OUTPUT:
=======================
ENCRYPTED: [U2FsdGVkX19qplb9qVDVVEYxH8wjJDGpMS+F4/2pS2c=]
DECRYPTED: [Hello World!]
I'm attempting with little success to port over Google's code to generate a secure token for their captcha (https://github.com/google/recaptcha-java/blob/master/appengine/src/main/java/com/google/recaptcha/STokenUtils.java):
The original utility has the following:
private static final String CIPHER_INSTANCE_NAME = "AES/ECB/PKCS5Padding";
private static String encryptAes(String input, String siteSecret) {
try {
SecretKeySpec secretKey = getKey(siteSecret);
Cipher cipher = Cipher.getInstance(CIPHER_INSTANCE_NAME);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return BaseEncoding.base64Url().omitPadding().encode(cipher.doFinal(input.getBytes("UTF-8")));
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static SecretKeySpec getKey(String siteSecret){
try {
byte[] key = siteSecret.getBytes("UTF-8");
key = Arrays.copyOf(MessageDigest.getInstance("SHA").digest(key), 16);
return new SecretKeySpec(key, "AES");
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
public static void main(String [] args) throws Exception {
//Hard coded the following to get a repeatable result
String siteSecret = "12345678";
String jsonToken = "{'session_id':'abf52ca5-9d87-4061-b109-334abb7e637a','ts_ms':1445705791480}";
System.out.println(" json token: " + jsonToken);
System.out.println(" siteSecret: " + siteSecret);
System.out.println(" Encrypted stoken: " + encryptAes(jsonToken, siteSecret));
Given the values I hardcoded, I get Irez-rWkCEqnsiRLWfol0IXQu1JPs3qL_G_9HfUViMG9u4XhffHqAyju6SRvMhFS86czHX9s1tbzd6B15r1vmY6s5S8odXT-ZE9A-y1lHns" back as my encrypted token.
My Java and crypto skills are more than a little rusty, and there aren't always direct analogs in C#. I attempted to merge encrypeAes() and getKey() with the following, which isn't correct:
public static string EncryptText(string PlainText, string siteSecret)
{
using (RijndaelManaged aes = new RijndaelManaged())
{
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
var bytes = Encoding.UTF8.GetBytes(siteSecret);
SHA1 sha1 = SHA1.Create();
var shaKey = sha1.ComputeHash(bytes);
byte[] targetArray = new byte[16];
Array.Copy(shaKey, targetArray, 16);
aes.Key = targetArray;
ICryptoTransform encrypto = aes.CreateEncryptor();
byte[] plainTextByte = ASCIIEncoding.UTF8.GetBytes(PlainText);
byte[] CipherText = encrypto.TransformFinalBlock(plainTextByte, 0, plainTextByte.Length);
return HttpServerUtility.UrlTokenEncode(CipherText); //Equivalent to java's BaseEncoding.base64Url()?
}
}
The C# version produces the incorrect value of: Ye+fySvneVUZJXth67+Si/e8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBGp5uUgKYJZCuQ4rc964vZigjlrJ/430LgYcathLLd9U=
Your code almost works as expected. It's just that you somehow mixed up the outputs of the Java version (and possibly the C# version).
If I execute your Java code (JDK 7 & 8 with Guava 18.0), I get
Ye-fySvneVUZJXth67-Si_e8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBGp5uUgKYJZCuQ4rc964vZigjlrJ_430LgYcathLLd9U
and if I execute your C# code (DEMO), I get
Ye-fySvneVUZJXth67-Si_e8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBGp5uUgKYJZCuQ4rc964vZigjlrJ_430LgYcathLLd9U1
So, the C# version has an additional "1" at the end. It should be a padding character, but isn't. This means that HttpServerUtility.UrlTokenEncode() doesn't provide a standards conform URL-safe Base64 encoding and you shouldn't use it. See also this Q&A.
The URL-safe Base64 encoding can be easily derived from the normal Base64 encoding (compare tables 1 and 2 in RFC4648) as seen in this answer by Marc Gravell:
string returnValue = System.Convert.ToBase64String(toEncodeAsBytes)
.TrimEnd(padding).Replace('+', '-').Replace('/', '_');
with:
static readonly char[] padding = { '=' };
That's not all. If we take your Java output of
Ye+fySvneVUZJXth67+Si/e8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBGp5uUgKYJZCuQ4rc964vZigjlrJ/430LgYcathLLd9U=
and decrypt it, then we get the following token:
{"session_id":"4182e173-3a24-4c10-b76c-b85a36be1173","ts_ms":1445786965574}
which is different from the token that you have in your code:
{'session_id':'abf52ca5-9d87-4061-b109-334abb7e637a','ts_ms':1445705791480}
The main remaining problem is that you're using invalid JSON. Strings and keys in JSON need to be wrapped in " and not '.
Which means that the encrypted token actually should have been (using a valid version of the token from your code):
D9rOP07fYgBfza5vbGsvdPe8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBsAWBDgtdSozv4jS_auBU-CgjlrJ_430LgYcathLLd9U
Here's a C# implementation that reproduces the same result as your Java code:
class Program
{
public static byte[] GetKey(string siteSecret)
{
byte[] key = Encoding.UTF8.GetBytes(siteSecret);
return SHA1.Create().ComputeHash(key).Take(16).ToArray();
}
public static string EncryptAes(string input, string siteSecret)
{
var key = GetKey(siteSecret);
using (var aes = AesManaged.Create())
{
if (aes == null) return null;
aes.Mode = CipherMode.ECB;
aes.Padding = PaddingMode.PKCS7;
aes.Key = key;
byte[] inputBytes = Encoding.UTF8.GetBytes(input);
var enc = aes.CreateEncryptor(key, new byte[16]);
return UrlSafeBase64(enc.TransformFinalBlock(inputBytes,0,input.Length));
}
}
// http://stackoverflow.com/a/26354677/162671
public static string UrlSafeBase64(byte[] bytes)
{
return Convert.ToBase64String(bytes).TrimEnd('=')
.Replace('+', '-')
.Replace('/', '_');
}
static void Main(string[] args)
{
string siteSecret = "12345678";
string jsonToken = "{'session_id':'abf52ca5-9d87-4061-b109-334abb7e637a','ts_ms':1445705791480}";
Console.WriteLine(" json token: " + jsonToken);
Console.WriteLine(" siteSecret: " + siteSecret);
Console.WriteLine(EncryptAes(jsonToken, siteSecret));
Console.ReadLine();
}
}
I don't know why you said you're getting Irez-rWkCEqnsiRLWfol0IXQu1JPs3qL_G_9HfUViMG9u4XhffHqAyju6SRvMhFS86czHX9s1tbzd6B15r1vmY6s5S8odXT-ZE9A-y1lHns from the Java program because I'm not getting that output. The output I'm getting from both the C# version and the Java version is this:
Ye-fySvneVUZJXth67-Si_e8fBUV4Sxs7wEXVDEOJjBMHl1encvt65gGIj8CiFzBGp5uUgKYJZCuQ4rc964vZigjlrJ_430LgYcathLLd9U
As you can see here:
The code for both versions is available here
Live demo of the C# version.
The Java version was copy/pasted from your code and is using guava-18.0 and compiled with JDK8 x64 (I'm not a java expert so I'm just adding these in case it makes any difference).
I am trying to encrypt a Client' name (string format) storing it in a database and then retrieving it and decrypting it. As i need to avoid any third part libraries, i have used classes which are readily available with Java distribution.
The process was working fine, until I encountered a name with a special character (Ascii : 48910). This was geting displayed as a question mark(?). The encryption and descryption went fine, but after the decryption the special character was replaced with the question mark.
So i changed the Encoding format from 'UTF-8' to 'ISO-8859-1'. This solved the display problem, but still the special character gets replaced after decryption.
The code being used and the output is given below (i have removed the unnecessary code):
package crypt;
import java.io.PrintStream;
import java.nio.charset.Charset;
import java.security.spec.KeySpec;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import javax.xml.bind.DatatypeConverter;
public class SecretKeyEncryptionExample {
private static final String FORMAT = "ISO-8859-1";
public static final String DESEDE_ENCRYPTION_SCHEME = "DESede";
private KeySpec ks;
private SecretKeyFactory skf;
private Cipher cipher;
SecretKey key;
public SecretKeyEncryptionExample() throws Exception {
String myEncryptionKey = "4A144BEBF7E5E7B7DCF26491AE79C54C768C514CF1547D23";
ks = new DESedeKeySpec(myEncryptionKey.getBytes(FORMAT));
skf = SecretKeyFactory.getInstance(DESEDE_ENCRYPTION_SCHEME);
cipher = Cipher.getInstance(DESEDE_ENCRYPTION_SCHEME);
key = skf.generateSecret(ks);
}
public String encrypt(String unencryptedString) throws Exception {
String encryptedString = null;
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] plainText = unencryptedString.getBytes(FORMAT);
byte[] encryptedText = cipher.doFinal(plainText);
encryptedString = DatatypeConverter.printBase64Binary(encryptedText);
return encryptedString;
}
public String decrypt(String encryptedString) throws Exception {
String decryptedText = null;
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] encryptedText = DatatypeConverter.parseBase64Binary(encryptedString);
byte[] plainText = cipher.doFinal(encryptedText);
decryptedText = new String(plainText);
return decryptedText;
}
public static void main(String args[]) throws Exception {
SecretKeyEncryptionExample td = new SecretKeyEncryptionExample();
String target = "Expendable" + getSpecialCharacter(49810) + "s Pte Ltd";
String encrypted = td.encrypt(target);
String decrypted = td.decrypt(encrypted);
PrintStream out = new PrintStream(System.out, true, FORMAT);
out.println("String To Encrypt: " + target);
out.println("Encrypted String: " + encrypted);
out.println("Decrypted String: " + decrypted);
}
public static String getSpecialCharacter(int code) {
Charset charSet = Charset.forName(FORMAT);
String specialCharacter = new String(new byte[] { (byte) code }, charSet);
specialCharacter = String.format("%s", specialCharacter);
return specialCharacter;
}
}
OUTPUT:
String To Encrypt: Expendable’s Pte Ltd
Encrypted String: TAAJuF7KOmBZHBXFHsW0FB9YBwH7Tcif
Decrypted String: Expendable?s Pte Ltd
Please let know how the decryption can be attained, without getting the special character replaced.
I think you should specify your encoding every time you go from a string to a byte array and back. In particular, this line:
decryptedText = new String(plainText);
should read:
decryptedText = new String(plainText, FORMAT);
Otherwise you rely on your environment's encoding, which in all likelihood differs from FORMAT and result in the special character being printed as "?".
Some things which may be useful to know.
System.out.println((int) getSpecialCharacter(49810).charAt(0));
prints
146
This is the character you are actually creating here.
System.out.println("The Falcon" + (char) 146 + "s Hangar Pte Ltd");
prints
The Falcon’s Hangar Pte Ltd
I think the problem is that you get the bytes using the ISO-8859-1 character set with
byte[] plainText = unencryptedString.getBytes(FORMAT);
but when you turn it back into a String you use the system default.
decryptedText = new String(plainText);
I suspect this should be
decryptedText = new String(plainText, FORMAT); // use the same Charset
I wrote an encrypt and a decrypt function. The encrypt works fine, but I always get IllegalBlockSizeException in the decrypt.
public static String aes_encrypt (String text, String key)
{
SecretKey skey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding", "SunJCE");
cipher.init(Cipher.ENCRYPT_MODE, skey);
return new String((cipher.doFinal(text.getBytes())));
}
And here's the decrypt function:
public static String aes_decrypt (String text, String key)
{
SecretKey skey = new SecretKeySpec(key.getBytes(), "AES");
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding", "SunJCE");
cipher.init(Cipher.DECRYPT_MODE, skey);
return new String((cipher.doFinal(text.getBytes())));
}
Here's the simple main method that tests this:
public static void main (String args[])
{
String text = "Hello, world!";
String key = "nv93h50sk1zh508v";
String en, de;
System.out.println("Text: " + text);
System.out.println("Encrypted: " + (en = aes_encrypt(text, key))
+ " length = " + en.length());
System.out.println("Decrypted: " + (de = aes_decrypt(en, key)));
}
Does anyone know how to "pad" the encrypted string properly so that I can decrypt it?
(I tried padding the string with 0 until the length is a multiple of 16, but got something like string not properly padded.)
Thanks
I think the problem is in your using the String constructor. This is converting to string using a text encoding mechanism, which may not preserve every value in the byte array - unsupported ones in the system default encoding may be discarded, leaving the encoded data shorter than it should be. If you want a string representation, convert to hex or base 64 encoding instead. And reverse whatever encoding you use here at the start of the decryption method.
It's not a padding issue - the encryption call will pad this fine, you are short of bytes because of your means of encoding the byte array to a string.
You'll find some base 64 instructions in answers to this SO question.
I am currently encoding a password. I have to decode the password. Here is the code to encode. I am trying to get the original password compare it. I have researched about MessageDigest that says it is a one-way method. Not sure how to get the original message. We have a decode method but it isn't giving me the original password - Base64.decode.
public static synchronized String getMD5_Base64(String input) {
if (!isInited) {
isInited = true;
try {
digest = MessageDigest.getInstance("MD5");
} catch (Exception ex) {
}
}
if (digest == null)
return input;
// now everything is ok, go ahead
try {
digest.update(input.getBytes("UTF-8"));
} catch (java.io.UnsupportedEncodingException ex) {
}
byte[] rawData = digest.digest();
byte[] encoded = Base64.encode(rawData);
String retValue = new String(encoded);
return retValue;
}
}
You cannot get the original password. Keep in mind that the digest and Base64 encoding do two completely different things. The MD5 digest creates a cryptographic hash of the data supplied to it. This is irreversible. Base64 is an encoding mechanism to convert data (which might contain unprintable binary data) into a string that is guaranteed to contain only printable characters. This step is reversible.
The standard way of checking a password is not to decode the original password and compare the plain text. What you need to do is take the encoding (MD5 hash then Base64 encode) you did on the original password and apply it to the newly supplied password. Then compare the stored encoded version with the newly encoded version. If they're the same then the passwords matched.
This design is a more secure mechanism than storing passwords that could be decoded. This way, if someone steals your password database they don't automatically have access to all the passwords of your users. In order to break into the system they'd still have to find a password that encoded to the same value. The point of cryptographic hashes like MD5 is to make that very difficult. On the other hand, MD5 is not considered a very secure hash anymore. You'd be better off using SHA1 or SHA256 (but remember, you can't change the existing stored passwords from their MD5 hash to another hash without the original password, which you don't have, i.e. you can't just convert your database of stored passwords).
MessageDigest with MD5 is one way hash. So, why don't you use javax.crypto which can encrypt and decrypt easily. Here is the example:
import java.security.spec.KeySpec;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.DESedeKeySpec;
import org.apache.commons.codec.binary.Base64;
public class EncryptDecrypt {
private static final String UNICODE_FORMAT = "UTF8";
public static final String DESEDE_ENCRYPTION_SCHEME = "DESede";
private KeySpec ks;
private SecretKeyFactory skf;
private Cipher cipher;
byte[] arrayBytes;
private String myEncryptionKey;
private String myEncryptionScheme;
SecretKey key;
public EncryptDecrypt() throws Exception {
myEncryptionKey = "ThisIsSpartaThisIsSparta";
myEncryptionScheme = DESEDE_ENCRYPTION_SCHEME;
arrayBytes = myEncryptionKey.getBytes(UNICODE_FORMAT);
ks = new DESedeKeySpec(arrayBytes);
skf = SecretKeyFactory.getInstance(myEncryptionScheme);
cipher = Cipher.getInstance(myEncryptionScheme);
key = skf.generateSecret(ks);
}
public String encrypt(String unencryptedString) {
String encryptedString = null;
try {
cipher.init(Cipher.ENCRYPT_MODE, key);
byte[] plainText = unencryptedString.getBytes(UNICODE_FORMAT);
byte[] encryptedText = cipher.doFinal(plainText);
encryptedString = new String(Base64.encodeBase64(encryptedText));
} catch (Exception e) {
e.printStackTrace();
}
return encryptedString;
}
public String decrypt(String encryptedString) {
String decryptedText=null;
try {
cipher.init(Cipher.DECRYPT_MODE, key);
byte[] encryptedText = Base64.decodeBase64(encryptedString.getBytes());
byte[] plainText = cipher.doFinal(encryptedText);
decryptedText= new String(plainText);
} catch (Exception e) {
e.printStackTrace();
}
return decryptedText;
}
public static void main(String args []) throws Exception
{
EncryptDecrypt td= new EncryptDecrypt();
String target="password#123";
String encrypted=td.encrypt(target);
String decrypted=td.decrypt(encrypted);
System.out.println("String To Encrypt: "+ target);
System.out.println("Encrypted String: " + encrypted);
System.out.println("Decrypted String: " + decrypted);
}
}
The MD5 hash algorithm is, like all hash algorithms, one-way. The only way to recover the original password is to try every possibility until you get the one whose MD5 hash matches what you received.
If you're trying to compare the contents of the new password with the older passwords you can't use an MD5 hash. As Jherico noted, MD5 (and all hashes) are one-way meaning that you can't get the original text.
In order to do the compare you will have to keep the original value of the password around somewhere. The best way is probably to encrypt (and base64 the result) before storing it to the database. Then in order to do the compare, you decrypt each of the values and do the work that you want
One important note is that storing the user's passwords in any form that can be reversed is can be dangerous if not done properly.