AES 加密,在解密文件中有额外的垃圾字符
AES encryption, got extra trash characters in decrypted file
我正在 android 应用程序中制作调试登录功能。
我有一个简单的 class,它是 使用 128 位 AES 加密记录到 .txt 文件。
记录完成后,我用一个简单的JAVA程序解密记录的文件。
问题是当我解密加密的日志时我得到了一些奇怪的内容,我也得到了加密的内容,但是有一些额外的字符,见下文。
Android 应用日志部分:
public class FileLogger {
//file and folder name
public static String LOG_FILE_NAME = "my_log.txt";
public static String LOG_FOLDER_NAME = "my_log_folder";
static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS");
//My secret key, 16 bytes = 128 bit
static byte[] key = {1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6};
//Appends to a log file, using encryption
public static void appendToLog(Context context, Object msg) {
String msgStr;
String timestamp = "t:" + formatter.format(new java.util.Date());
msgStr = msg + "|" + timestamp + "\n";
File sdcard = Environment.getExternalStorageDirectory();
File dir = new File(sdcard.getAbsolutePath() + "/" + LOG_FOLDER_NAME);
if (!dir.exists()) {
dir.mkdir();
}
File encryptedFile = new File(dir, LOG_FILE_NAME);
try {
//Encryption using my key above defined
Key secretKey = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] outputBytes = cipher.doFinal(msgStr.getBytes());
//Writing to the file using append mode
FileOutputStream outputStream = new FileOutputStream(encryptedFile, true);
outputStream.write(outputBytes);
outputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
}
}
这是解密程序JAVA:
public class Main {
//output file name after decryption
private static String decryptedFileName;
//input encrypted file
private static String fileSource;
//a prefix tag for output file name
private static String outputFilePrefix = "decrypted_";
//My key for decryption, its the same as in the encrypter program.
static byte[] key = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6 };
//Decrypting function
public static void decrypt(byte[] key, File inputFile, File outputFile) throws Exception {
try {
Key secretKey = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
FileInputStream inputStream = new FileInputStream(inputFile);
byte[] inputBytes = new byte[(int) inputFile.length()];
inputStream.read(inputBytes);
byte[] outputBytes = cipher.doFinal(inputBytes);
FileOutputStream outputStream = new FileOutputStream(outputFile, true);
outputStream.write(outputBytes);
inputStream.close();
outputStream.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
//first argument is the intput file source
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Add log file name as a parameter.");
} else {
fileSource = args[0];
try {
File sourceFile = new File(fileSource);
if (sourceFile.exists()) {
//Decrption
decryptedFileName = outputFilePrefix + sourceFile.getName();
File decryptedFile = new File(decryptedFileName);
decrypt(key, sourceFile, decryptedFile);
} else {
System.out.println("Log file not found: " + fileSource);
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Decryption done, output file: " + decryptedFileName);
}
}
}
输出解密日志(用notepad++打开):
T这是有效内容,但您也可以看到额外的 thrash 字符。 如果我使用默认 windows 文本编辑器打开,我也会看到 thrash 字符, 但不同。
这是我第一次尝试加密解密,我做错了什么?
有什么想法吗?
您已使用不同的加密上下文对每条日志消息进行了加密。当您在密码对象上调用 doFinal
方法时,明文被填充为 16 的倍数。实际上,您的日志文件是许多小的加密消息的序列。但是,在解密时,您将忽略这些消息边界并将文件视为单个加密消息。结果是填充字符没有被正确去除。您看到的 'trash' 个字符可能是这些填充字节。您将需要重新设计您的日志文件格式,以保留消息边界以便解密器可以发现它们或完全消除它们。
此外,不要在 Java 密码学中使用默认值:它们不可移植。例如,Cipher.getInstance()
采用 alg/mode/padding
形式的字符串。始终指定所有三个。我注意到您还使用默认的无参数 String.getBytes()
方法。总是指定一个字符集,几乎总是 "UTF8" 是最好的选择。
AES 是一种仅适用于块的分组密码。您要加密的明文可以是任意长度,因此密码必须始终填充明文以将其填充到块大小的倍数(或者当它已经是块大小的倍数时添加一个完整的块)。在这个PKCS#5/PKCS#7 padding中,每个padding byte表示被填充的字节数。
简单的解决方法是在解密过程中迭代 outputBytes
并删除那些总是在下一行的填充字节。一旦您使用多行日志消息或使用语义安全模式(稍后会详细介绍),这就会中断。
更好的解决方法是在消息之前写入每条日志消息的字节数,读取该消息并仅解密那么多字节。这也可能更容易用文件流实现。
您当前使用的 Cipher.getInstance("AES");
是 Cipher.getInstance("AES/ECB/PKCS5Padding");
的非完全限定版本。 ECB 模式在语义上不安全。它只是用 AES 和密钥对每个块(16 字节)进行加密。因此,相同的块在密文中将相同。这尤其糟糕,因为一些日志消息以相同的方式开始,攻击者可能能够区分它们。这也是为什么整个文件的解密在块加密的情况下仍然有效的原因。您应该使用带有随机 IV 的 CBC 模式。
下面是一些示例代码,用于在 CBC 模式下使用流使用随机 IV 正确使用 AES:
private static SecretKey key = generateAESkey();
private static String cipherString = "AES/CBC/PKCS5Padding";
public static void main(String[] args) throws Exception {
ByteArrayOutputStream log = new ByteArrayOutputStream();
appendToLog("Test1", log);
appendToLog("Test2 is longer", log);
appendToLog("Test3 is multiple of block size!", log);
appendToLog("Test4 is shorter.", log);
byte[] encLog = log.toByteArray();
List<String> logs = decryptLog(new ByteArrayInputStream(encLog));
for(String logLine : logs) {
System.out.println(logLine);
}
}
private static SecretKey generateAESkey() {
try {
return KeyGenerator.getInstance("AES").generateKey();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
private static byte[] generateIV() {
SecureRandom random = new SecureRandom();
byte[] iv = new byte[16];
random.nextBytes(iv);
return iv;
}
public static void appendToLog(String s, OutputStream os) throws Exception {
Cipher cipher = Cipher.getInstance(cipherString);
byte[] iv = generateIV();
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] data = cipher.doFinal(s.getBytes("UTF-8"));
os.write(data.length);
os.write(iv);
os.write(data);
}
public static List<String> decryptLog(InputStream is) throws Exception{
ArrayList<String> logs = new ArrayList<String>();
while(is.available() > 0) {
int len = is.read();
byte[] encLogLine = new byte[len];
byte[] iv = new byte[16];
is.read(iv);
is.read(encLogLine);
Cipher cipher = Cipher.getInstance(cipherString);
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
byte[] data = cipher.doFinal(encLogLine);
logs.add(new String(data, "UTF-8"));
}
return logs;
}
我正在 android 应用程序中制作调试登录功能。 我有一个简单的 class,它是 使用 128 位 AES 加密记录到 .txt 文件。
记录完成后,我用一个简单的JAVA程序解密记录的文件。
问题是当我解密加密的日志时我得到了一些奇怪的内容,我也得到了加密的内容,但是有一些额外的字符,见下文。
Android 应用日志部分:
public class FileLogger {
//file and folder name
public static String LOG_FILE_NAME = "my_log.txt";
public static String LOG_FOLDER_NAME = "my_log_folder";
static SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd_HH-mm-ss_SSS");
//My secret key, 16 bytes = 128 bit
static byte[] key = {1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6};
//Appends to a log file, using encryption
public static void appendToLog(Context context, Object msg) {
String msgStr;
String timestamp = "t:" + formatter.format(new java.util.Date());
msgStr = msg + "|" + timestamp + "\n";
File sdcard = Environment.getExternalStorageDirectory();
File dir = new File(sdcard.getAbsolutePath() + "/" + LOG_FOLDER_NAME);
if (!dir.exists()) {
dir.mkdir();
}
File encryptedFile = new File(dir, LOG_FILE_NAME);
try {
//Encryption using my key above defined
Key secretKey = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] outputBytes = cipher.doFinal(msgStr.getBytes());
//Writing to the file using append mode
FileOutputStream outputStream = new FileOutputStream(encryptedFile, true);
outputStream.write(outputBytes);
outputStream.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
} catch (NoSuchPaddingException e) {
e.printStackTrace();
} catch (IllegalBlockSizeException e) {
e.printStackTrace();
} catch (BadPaddingException e) {
e.printStackTrace();
} catch (InvalidKeyException e) {
e.printStackTrace();
}
}
}
这是解密程序JAVA:
public class Main {
//output file name after decryption
private static String decryptedFileName;
//input encrypted file
private static String fileSource;
//a prefix tag for output file name
private static String outputFilePrefix = "decrypted_";
//My key for decryption, its the same as in the encrypter program.
static byte[] key = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, 5, 6 };
//Decrypting function
public static void decrypt(byte[] key, File inputFile, File outputFile) throws Exception {
try {
Key secretKey = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES");
cipher.init(Cipher.DECRYPT_MODE, secretKey);
FileInputStream inputStream = new FileInputStream(inputFile);
byte[] inputBytes = new byte[(int) inputFile.length()];
inputStream.read(inputBytes);
byte[] outputBytes = cipher.doFinal(inputBytes);
FileOutputStream outputStream = new FileOutputStream(outputFile, true);
outputStream.write(outputBytes);
inputStream.close();
outputStream.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
//first argument is the intput file source
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Add log file name as a parameter.");
} else {
fileSource = args[0];
try {
File sourceFile = new File(fileSource);
if (sourceFile.exists()) {
//Decrption
decryptedFileName = outputFilePrefix + sourceFile.getName();
File decryptedFile = new File(decryptedFileName);
decrypt(key, sourceFile, decryptedFile);
} else {
System.out.println("Log file not found: " + fileSource);
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("Decryption done, output file: " + decryptedFileName);
}
}
}
输出解密日志(用notepad++打开):
T这是有效内容,但您也可以看到额外的 thrash 字符。 如果我使用默认 windows 文本编辑器打开,我也会看到 thrash 字符, 但不同。
这是我第一次尝试加密解密,我做错了什么? 有什么想法吗?
您已使用不同的加密上下文对每条日志消息进行了加密。当您在密码对象上调用 doFinal
方法时,明文被填充为 16 的倍数。实际上,您的日志文件是许多小的加密消息的序列。但是,在解密时,您将忽略这些消息边界并将文件视为单个加密消息。结果是填充字符没有被正确去除。您看到的 'trash' 个字符可能是这些填充字节。您将需要重新设计您的日志文件格式,以保留消息边界以便解密器可以发现它们或完全消除它们。
此外,不要在 Java 密码学中使用默认值:它们不可移植。例如,Cipher.getInstance()
采用 alg/mode/padding
形式的字符串。始终指定所有三个。我注意到您还使用默认的无参数 String.getBytes()
方法。总是指定一个字符集,几乎总是 "UTF8" 是最好的选择。
AES 是一种仅适用于块的分组密码。您要加密的明文可以是任意长度,因此密码必须始终填充明文以将其填充到块大小的倍数(或者当它已经是块大小的倍数时添加一个完整的块)。在这个PKCS#5/PKCS#7 padding中,每个padding byte表示被填充的字节数。
简单的解决方法是在解密过程中迭代 outputBytes
并删除那些总是在下一行的填充字节。一旦您使用多行日志消息或使用语义安全模式(稍后会详细介绍),这就会中断。
更好的解决方法是在消息之前写入每条日志消息的字节数,读取该消息并仅解密那么多字节。这也可能更容易用文件流实现。
您当前使用的 Cipher.getInstance("AES");
是 Cipher.getInstance("AES/ECB/PKCS5Padding");
的非完全限定版本。 ECB 模式在语义上不安全。它只是用 AES 和密钥对每个块(16 字节)进行加密。因此,相同的块在密文中将相同。这尤其糟糕,因为一些日志消息以相同的方式开始,攻击者可能能够区分它们。这也是为什么整个文件的解密在块加密的情况下仍然有效的原因。您应该使用带有随机 IV 的 CBC 模式。
下面是一些示例代码,用于在 CBC 模式下使用流使用随机 IV 正确使用 AES:
private static SecretKey key = generateAESkey();
private static String cipherString = "AES/CBC/PKCS5Padding";
public static void main(String[] args) throws Exception {
ByteArrayOutputStream log = new ByteArrayOutputStream();
appendToLog("Test1", log);
appendToLog("Test2 is longer", log);
appendToLog("Test3 is multiple of block size!", log);
appendToLog("Test4 is shorter.", log);
byte[] encLog = log.toByteArray();
List<String> logs = decryptLog(new ByteArrayInputStream(encLog));
for(String logLine : logs) {
System.out.println(logLine);
}
}
private static SecretKey generateAESkey() {
try {
return KeyGenerator.getInstance("AES").generateKey();
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
return null;
}
private static byte[] generateIV() {
SecureRandom random = new SecureRandom();
byte[] iv = new byte[16];
random.nextBytes(iv);
return iv;
}
public static void appendToLog(String s, OutputStream os) throws Exception {
Cipher cipher = Cipher.getInstance(cipherString);
byte[] iv = generateIV();
cipher.init(Cipher.ENCRYPT_MODE, key, new IvParameterSpec(iv));
byte[] data = cipher.doFinal(s.getBytes("UTF-8"));
os.write(data.length);
os.write(iv);
os.write(data);
}
public static List<String> decryptLog(InputStream is) throws Exception{
ArrayList<String> logs = new ArrayList<String>();
while(is.available() > 0) {
int len = is.read();
byte[] encLogLine = new byte[len];
byte[] iv = new byte[16];
is.read(iv);
is.read(encLogLine);
Cipher cipher = Cipher.getInstance(cipherString);
cipher.init(Cipher.DECRYPT_MODE, key, new IvParameterSpec(iv));
byte[] data = cipher.doFinal(encLogLine);
logs.add(new String(data, "UTF-8"));
}
return logs;
}