diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e5469bbf..d026a26f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ All notable changes to this project will be documented in this file. - [#2119] Bulk imported assets can also match filenames based on assigned classname, not just character id prefix - Debugger shows (logs) unhandled exceptions - [#2129] MEMORY and STACK_SIZE parameters now can be set via external variables FFDEC_MEMORY, FFDEC_STACK_SIZE +- Saving Harman encrypted SWFs +- Editing encrypted flag on header panel +- `-encrypt` command on CLI for Harman encryption ### Fixed - [#2021], [#2000] Caret position in editors when using tabs and / or unicode diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/HarmanDecryption.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/HarmanDecryption.java deleted file mode 100644 index 002428e7c..000000000 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/HarmanDecryption.java +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Copyright (C) 2010-2023 JPEXS, All rights reserved. - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 3.0 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library. - */ -package com.jpexs.decompiler.flash; - -import java.io.ByteArrayInputStream; -import java.io.DataInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.util.Arrays; -import javax.crypto.BadPaddingException; -import javax.crypto.Cipher; -import javax.crypto.IllegalBlockSizeException; -import javax.crypto.NoSuchPaddingException; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; - -/** - * - * @author JPEXS - * - * Based on swfdecrypt.py by NathaanTFM - */ -public class HarmanDecryption { - - private static final String GLOBAL_KEY = "Adobe AIR SDK (c) 2021 HARMAN Internation Industries Incorporated"; - - private static int sum(byte[] data) { - int s = 0; - for (int i = 0; i < data.length; i++) { - s += data[i] & 0xff; - } - return s; - } - - private static long getkey(byte[] data) { - int dsum = sum(data); - int dmod = dsum % GLOBAL_KEY.length(); - String s = GLOBAL_KEY.substring(dmod) + GLOBAL_KEY.substring(0, dmod); - s += " EncryptSWF "; - s += "" + dsum; - - long ret = 0; - for (int i = 0; i < s.length(); i++) { - int code = s.charAt(i); - ret *= 31; - ret += code; - } - - return ret & 0xffffffffL; - } - - private static long unpack(byte[] data, int start) { - return (data[start] & 0xff) - + ((long) (data[start + 1] & 0xff) << 8) - + ((long) (data[start + 2] & 0xff) << 16) - + ((long) (data[start + 3] & 0xff) << 24); - } - - public InputStream decrypt(InputStream is, byte[] header) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { - header[0] -= 32; // to uppercase - - long key = getkey(header); - - //get the length - DataInputStream dais = new DataInputStream(is); - - byte[] encryptedLengthBytes = new byte[4]; - dais.readFully(encryptedLengthBytes); - long encryptedLength = unpack(encryptedLengthBytes, 0); - int decryptedLength = (int) (encryptedLength ^ key); - - //padded length - int paddedLength = (int) (decryptedLength + 0x1F) & ~0x1F; - - //aes iv - byte[] aesIV = new byte[16]; - System.arraycopy(header, 0, aesIV, 0, header.length); //header - System.arraycopy(encryptedLengthBytes, 0, aesIV, 8, 4); //encrypted length - aesIV[12] = (byte) (key & 0xff); - aesIV[13] = (byte) ((key >> 8) & 0xff); - aesIV[14] = (byte) ((key >> 16) & 0xff); - aesIV[15] = (byte) ((key >> 24) & 0xff); - - for (int i = 0; i < 16; i++) { - aesIV[i] ^= GLOBAL_KEY.charAt(i); - } - - // aes key - // this one is stored at the end of the file - byte[] aesKey = new byte[32]; - byte[] data = new byte[paddedLength]; - dais.readFully(data); - - byte[] aesKeyData = new byte[32]; - dais.readFully(aesKeyData); - - for (int i = 0; i < 32; i += 4) { - long value = unpack(aesKeyData, i); - if ((i & 4) == 4) { - value -= key; - } else { - value += key; - } - aesKey[i] = (byte) (value & 0xff); - aesKey[i + 1] = (byte) ((value >> 8) & 0xff); - aesKey[i + 2] = (byte) ((value >> 16) & 0xff); - aesKey[i + 3] = (byte) ((value >> 24) & 0xff); - } - - SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, "AES"); - Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(aesIV)); - - byte[] decryptedData = cipher.doFinal(data); - return new ByteArrayInputStream(Arrays.copyOfRange(decryptedData, 0, decryptedLength)); - } -} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java index 4a19035be..20ce51a43 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java @@ -88,6 +88,7 @@ import com.jpexs.decompiler.flash.exporters.script.AS2ScriptExporter; import com.jpexs.decompiler.flash.exporters.script.AS3ScriptExporter; import com.jpexs.decompiler.flash.exporters.settings.ScriptExportSettings; import com.jpexs.decompiler.flash.exporters.shape.ShapeExportData; +import com.jpexs.decompiler.flash.harman.HarmanSwfEncrypt; import com.jpexs.decompiler.flash.helpers.HighlightedText; import com.jpexs.decompiler.flash.helpers.HighlightedTextWriter; import com.jpexs.decompiler.flash.helpers.NulWriter; @@ -1140,15 +1141,20 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { */ @Override public void saveTo(OutputStream os) throws IOException { - checkCharset(); - byte[] uncompressedData = saveToByteArray(false); - compress(new ByteArrayInputStream(uncompressedData), os, compression, lzmaProperties); + saveTo(os, gfx, false); } public void saveTo(OutputStream os, boolean gfx, boolean includeImported) throws IOException { checkCharset(); - byte[] uncompressedData = saveToByteArray(gfx, includeImported); - compress(new ByteArrayInputStream(uncompressedData), os, compression, lzmaProperties); + byte[] newUncompressedData = saveToByteArray(gfx, includeImported); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + compress(new ByteArrayInputStream(newUncompressedData), baos, compression, lzmaProperties); + byte[] newCompressedData = baos.toByteArray(); + if (encrypted) { + encrypt(new ByteArrayInputStream(newCompressedData), os); + } else { + os.write(newCompressedData); + } } public byte[] getHeaderBytes() { @@ -2002,8 +2008,30 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { } /** - * Decrypts Harman AIR encryption - * + * Encrypts Harman AIR encryption + */ + public static boolean encrypt(InputStream is, OutputStream os) throws IOException { + byte[] hdr = new byte[8]; + + // SWFheader: signature, version and fileSize + if (is.read(hdr) != 8) { + throw new SwfOpenException(AppResources.translate("error.swf.headerTooShort")); + } + + decodeHeader(hdr); + + byte[] encrypted; + try { + encrypted = HarmanSwfEncrypt.encrypt(is, hdr); + } catch (NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException ex) { + return false; + } + os.write(encrypted); + return true; + } + + /** + * Decrypts Harman AIR encryption */ public static boolean decrypt(InputStream is, OutputStream os) throws IOException { byte[] hdr = new byte[8]; @@ -2019,11 +2047,10 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { case 'c': case 'z': case 'f': - HarmanDecryption dec = new HarmanDecryption(); try { - is = dec.decrypt(is, hdr); //Note: this call will uppercase hdr[0] + byte[] decrypted = HarmanSwfEncrypt.decrypt(is, hdr); //Note: this call will uppercase hdr[0] os.write(hdr); - Helper.copyStream(is, os); + os.write(decrypted); return true; } catch (IOException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException ex) { throw new SwfOpenException(AppResources.translate("error.swf.decryptionProblem")); @@ -2091,14 +2118,16 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { case 'c': case 'z': case 'f': - header.encrypted = true; - HarmanDecryption dec = new HarmanDecryption(); + header.encrypted = true; + byte[] decrypted; try { - is = dec.decrypt(is, hdr); //Note: this call will uppercase hdr[0] - } catch (IOException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException ex) { + decrypted = HarmanSwfEncrypt.decrypt(is, hdr); + } catch (IOException | NoSuchPaddingException | NoSuchAlgorithmException | InvalidKeyException | InvalidAlgorithmParameterException | IllegalBlockSizeException | BadPaddingException ex) { throw new SwfOpenException(AppResources.translate("error.swf.decryptionProblem")); - } + } + is = new ByteArrayInputStream(decrypted); break; + } switch (hdr[0]) { diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/harman/HarmanBinaryDataEncrypt.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/harman/HarmanBinaryDataEncrypt.java new file mode 100644 index 000000000..60f2e4037 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/harman/HarmanBinaryDataEncrypt.java @@ -0,0 +1,199 @@ +/* + * Copyright (C) 2010-2023 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.harman; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class HarmanBinaryDataEncrypt { + + private static final String GLOBAL_KEY = "Adobe AIR SDK (c) 2021 HARMAN Internation Industries Incorporated"; + + private static final String[] hexStringCache; + + static { + hexStringCache = new String[256]; + for (int i = 0; i < hexStringCache.length; i++) { + hexStringCache[i] = String.format("%02x", i); + } + } + + public static String byteArrayToHex(byte[] data) { + StringBuilder sb = new StringBuilder(data.length * 2); + for (byte b : data) { + sb.append(hexStringCache[b & 0xff]); + } + + return sb.toString(); + } + + public static byte[] encrypt(byte[] data) { + byte[] result; + try { + SecureRandom random = new SecureRandom(); + int dataLen = data.length; + int encryptedDataLen = (dataLen + 0x1F) & ~0x1F; + int resultLen = 32 + encryptedDataLen; + byte[] hashBytes = new byte[4]; + byte[] randomBytes1 = new byte[4]; + byte[] randomBytes2 = new byte[4]; + byte[] keyBytes = new byte[16]; + random.nextBytes(hashBytes); + random.nextBytes(randomBytes1); + random.nextBytes(randomBytes2); + random.nextBytes(keyBytes); + long random1 = unpack(randomBytes1, 0); + long random2 = unpack(randomBytes2, 0); + + + byte[] ivBytes = getIv(hashBytes, random1, random2); + IvParameterSpec ivSpec = new IvParameterSpec(ivBytes); + SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); + byte[] dataPadded = new byte[(int) encryptedDataLen]; + random.nextBytes(dataPadded); + System.arraycopy(data, 0, dataPadded, 0, data.length); + + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec); + byte[] encryptedBytes = cipher.doFinal(dataPadded); + result = new byte[resultLen]; + long hash = unpack(hashBytes, 0); + long resultLenXorHash = resultLen ^ hash; + System.arraycopy(pack(resultLenXorHash), 0, result, 0, 4); + long dataLenXorRandom1 = dataLen ^ random1; + System.arraycopy(pack(dataLenXorRandom1), 0, result, 4, 4); + System.arraycopy(randomBytes1, 0, result, 8, 4); + System.arraycopy(randomBytes2, 0, result, 12, 4); + + addBytes(keyBytes, 0, randomBytes2, 1); + addBytes(keyBytes, 4, randomBytes2, -1); + addBytes(keyBytes, 8, randomBytes2, 1); + addBytes(keyBytes, 12, randomBytes2, -1); + + System.arraycopy(keyBytes, 0, result, 16, 16); + System.arraycopy(encryptedBytes, 0, result, 32, encryptedDataLen); + } catch (UnsupportedEncodingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException ex) { + result = null; + } + return result; + } + + private static long unpack(byte[] data, int start) { + return (data[start] & 0xff) + + ((long) (data[start + 1] & 0xff) << 8) + + ((long) (data[start + 2] & 0xff) << 16) + + ((long) (data[start + 3] & 0xff) << 24); + } + + private static void addBytes(byte[] toChangeBytes, int offset, byte[] data, int multiplier) { + for (int i = 0; i < 4; i++) { + toChangeBytes[offset + i] = (byte) (toChangeBytes[offset + i] + multiplier * data[i]); + } + } + + private static byte[] getIv(byte[] hashBytes, long random1, long random2) throws UnsupportedEncodingException { + byte[] ivBytes = new byte[16]; + int offset = 0; + + int offset2; + for (offset2 = 0; offset2 < 4; offset2++) { + offset += hashBytes[offset2] & 0xff; + } + + offset2 = offset % GLOBAL_KEY.length(); + String hashString = GLOBAL_KEY.substring(offset2) + GLOBAL_KEY.substring(0, offset2) + " EncryptSWF " + offset; + byte[] hashStringBytes = hashString.getBytes("UTF-8"); + long hashCodeString = 0L; + + for (int i = 0; i < hashStringBytes.length; ++i) { + hashCodeString = 31 * hashCodeString + (hashStringBytes[i] & 0xff); + } + System.arraycopy(pack(hashCodeString), 0, ivBytes, 0, 4); + System.arraycopy(hashBytes, 0, ivBytes, 4, 4); + long hashSum = unpack(hashBytes, 0) + hashCodeString; + long sumXorRandom1 = random1 ^ hashSum; + long sumXorRandom2 = random2 ^ hashSum; + System.arraycopy(pack(sumXorRandom1), 0, ivBytes, 8, 4); + System.arraycopy(pack(sumXorRandom2), 0, ivBytes, 12, 4); + return ivBytes; + } + + private static byte[] pack(long value) { + byte[] ret = new byte[4]; + ret[0] = (byte) (value & 0xff); + ret[1] = (byte) ((value >> 8) & 0xff); + ret[2] = (byte) ((value >> 16) & 0xff); + ret[3] = (byte) ((value >> 24) & 0xff); + return ret; + } + + public static byte[] decrypt(byte[] data) { + long encryptedLen = data.length; + long encryptedLenXorHash = unpack(data, 0); + long decryptedLenXorRandom1 = unpack(data, 4); + + long random1 = unpack(data, 8); + long random2 = unpack(data, 12); + byte[] random2Bytes = Arrays.copyOfRange(data, 12, 16); + byte[] keyBytes = Arrays.copyOfRange(data, 16, 32); + byte[] encryptedBytes = Arrays.copyOfRange(data, 32, data.length); + + long decryptedLen = decryptedLenXorRandom1 ^ random1; + long hash = encryptedLen ^ encryptedLenXorHash; + byte[] hashBytes = pack(hash); + addBytes(keyBytes, 0, random2Bytes, -1); + addBytes(keyBytes, 4, random2Bytes, 1); + addBytes(keyBytes, 8, random2Bytes, -1); + addBytes(keyBytes, 12, random2Bytes, 1); + + try { + byte[] ivBytes = getIv(hashBytes, random1, random2); + IvParameterSpec ivSpec = new IvParameterSpec(ivBytes); + + SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES"); + + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); + byte[] decryptedBytes = cipher.doFinal(encryptedBytes); + byte[] ret = new byte[(int) decryptedLen]; + System.arraycopy(decryptedBytes, 0, ret, 0, (int) decryptedLen); + return ret; + } catch (UnsupportedEncodingException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException ex) { + return null; + } + } + + public static void main(String[] args) throws IOException { + byte[] data = new byte[] {'A', 'B', 'C'}; + byte[] encrypted = encrypt(data); + byte[] decrypted = decrypt(encrypted); + if (!Arrays.equals(data, decrypted)) { + throw new RuntimeException("Cannot encrypt/decrypt"); + } + } +} diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/harman/HarmanSwfEncrypt.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/harman/HarmanSwfEncrypt.java new file mode 100644 index 000000000..7729f9fc8 --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/harman/HarmanSwfEncrypt.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2010-2023 JPEXS, All rights reserved. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3.0 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library. + */ +package com.jpexs.decompiler.flash.harman; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.DataInputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Arrays; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +/** + * + * @author JPEXS + * + * Based on swfdecrypt.py by NathaanTFM + */ +public class HarmanSwfEncrypt { + + private static final String GLOBAL_KEY = "Adobe AIR SDK (c) 2021 HARMAN Internation Industries Incorporated"; + + + private static final String[] hexStringCache; + + static { + hexStringCache = new String[256]; + for (int i = 0; i < hexStringCache.length; i++) { + hexStringCache[i] = String.format("%02x", i); + } + } + + public static String byteArrayToHex(byte[] data) { + StringBuilder sb = new StringBuilder(data.length * 2); + for (byte b : data) { + sb.append(hexStringCache[b & 0xff]); + } + + return sb.toString(); + } + + private static int sum(byte[] data) { + int s = 0; + for (int i = 0; i < data.length; i++) { + s += data[i] & 0xff; + } + return s; + } + + private static long getkey(byte[] data) { + int dsum = sum(data); + int dmod = dsum % GLOBAL_KEY.length(); + String s = GLOBAL_KEY.substring(dmod) + GLOBAL_KEY.substring(0, dmod); + s += " EncryptSWF "; + s += "" + dsum; + + long ret = 0; + for (int i = 0; i < s.length(); i++) { + int code = s.charAt(i); + ret *= 31; + ret += code; + } + + return ret & 0xffffffffL; + } + + private static long unpack(byte[] data, int start) { + return (data[start] & 0xff) + + ((long) (data[start + 1] & 0xff) << 8) + + ((long) (data[start + 2] & 0xff) << 16) + + ((long) (data[start + 3] & 0xff) << 24); + } + + private static byte[] pack(long value) { + return new byte[]{ + (byte) (value & 0xff), + (byte) ((value >> 8) & 0xff), + (byte) ((value >> 16) & 0xff), + (byte) ((value >> 24) & 0xff) + }; + } + + private static byte[] readStream(InputStream is) throws IOException { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; // velikost bufferu, můžeš ji upravit podle potřeby + + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + + return outputStream.toByteArray(); + } + + public static byte[] encrypt(byte[] data) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + return encrypt(new ByteArrayInputStream(data)); + } + + public static byte[] encrypt(InputStream is) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + byte[] header = new byte[8]; + DataInputStream dais = new DataInputStream(is); + dais.readFully(header); + return encrypt(is, header); + } + + public static byte[] encrypt(InputStream is, byte[] header) throws IOException, NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + long key = getkey(header); + + byte[] data = readStream(is); + + int decryptedLength = data.length; + long encryptedLength = decryptedLength ^ key; + byte[] encryptedLengthBytes = pack(encryptedLength); + + + int paddedLength = (int) (decryptedLength + 0x1F) & ~0x1F; + + byte[] dataPadded = new byte[paddedLength]; + System.arraycopy(data, 0, dataPadded, 0, data.length); + + byte[] aesIV = new byte[16]; + System.arraycopy(header, 0, aesIV, 0, header.length); //header + System.arraycopy(encryptedLengthBytes, 0, aesIV, 8, 4); //encrypted length + aesIV[12] = (byte) (key & 0xff); + aesIV[13] = (byte) ((key >> 8) & 0xff); + aesIV[14] = (byte) ((key >> 16) & 0xff); + aesIV[15] = (byte) ((key >> 24) & 0xff); + + for (int i = 0; i < 16; i++) { + aesIV[i] ^= GLOBAL_KEY.charAt(i); + } + + byte[] aesKey = new byte[32]; + + SecureRandom random = new SecureRandom(); + random.nextBytes(aesKey); + + SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, "AES"); + Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(aesIV)); + byte[] encryptedData = cipher.doFinal(dataPadded); + header[0] += 32; //to lowercase + + baos.write(header); + baos.write(pack(encryptedLength)); + baos.write(encryptedData, 0, paddedLength); + + byte[] aesKeyData = new byte[32]; + for (int i = 0; i < 32; i += 4) { + long value = unpack(aesKey, i); + if ((i & 4) == 4) { + value += key; + } else { + value -= key; + } + aesKeyData[i] = (byte) (value & 0xff); + aesKeyData[i + 1] = (byte) ((value >> 8) & 0xff); + aesKeyData[i + 2] = (byte) ((value >> 16) & 0xff); + aesKeyData[i + 3] = (byte) ((value >> 24) & 0xff); + } + + baos.write(aesKeyData); + + return baos.toByteArray(); + } + + + public static byte[] decrypt(byte[] data) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + return decrypt(new ByteArrayInputStream(data)); + } + + public static byte[] decrypt(InputStream is) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + DataInputStream dais = new DataInputStream(is); + byte[] header = new byte[8]; + dais.readFully(header); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + byte[] dec = decrypt(is, header); + baos.write(header); + baos.write(dec); + return baos.toByteArray(); + } + + public static byte[] decrypt(InputStream is, byte[] header) throws IOException, NoSuchPaddingException, NoSuchAlgorithmException, InvalidKeyException, InvalidAlgorithmParameterException, IllegalBlockSizeException, BadPaddingException { + header[0] -= 32; // to uppercase + + long key = getkey(header); + + DataInputStream dais = new DataInputStream(is); + + byte[] encryptedLengthBytes = new byte[4]; + dais.readFully(encryptedLengthBytes); + long encryptedLength = unpack(encryptedLengthBytes, 0); + int decryptedLength = (int) (encryptedLength ^ key); + + int paddedLength = (int) (decryptedLength + 0x1F) & ~0x1F; + + byte[] aesIV = new byte[16]; + System.arraycopy(header, 0, aesIV, 0, header.length); //header + System.arraycopy(encryptedLengthBytes, 0, aesIV, 8, 4); //encrypted length + aesIV[12] = (byte) (key & 0xff); + aesIV[13] = (byte) ((key >> 8) & 0xff); + aesIV[14] = (byte) ((key >> 16) & 0xff); + aesIV[15] = (byte) ((key >> 24) & 0xff); + + for (int i = 0; i < 16; i++) { + aesIV[i] ^= GLOBAL_KEY.charAt(i); + } + + // aes key + // this one is stored at the end of the file + byte[] aesKey = new byte[32]; + byte[] data = new byte[paddedLength]; + dais.readFully(data); + + byte[] aesKeyData = new byte[32]; + dais.readFully(aesKeyData); + + for (int i = 0; i < 32; i += 4) { + long value = unpack(aesKeyData, i); + if ((i & 4) == 4) { + value -= key; + } else { + value += key; + } + aesKey[i] = (byte) (value & 0xff); + aesKey[i + 1] = (byte) ((value >> 8) & 0xff); + aesKey[i + 2] = (byte) ((value >> 16) & 0xff); + aesKey[i + 3] = (byte) ((value >> 24) & 0xff); + } + + SecretKeySpec secretKeySpec = new SecretKeySpec(aesKey, "AES"); + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(aesIV)); + + byte[] decryptedData = cipher.doFinal(data); + return Arrays.copyOfRange(decryptedData, 0, decryptedLength); + } + + public static void main(String[] args) throws Exception { + byte[] data = new byte[] {'C', 'W', 'S', 0x32, 0x01, 0x02, 0x03, 0x04, 0x41, 0x42, 0x43, 0xd, 0xa}; + byte[] encrypted = encrypt(data); + byte[] decrypted = decrypt(encrypted); + + if (!Arrays.equals(data, decrypted)) { + throw new RuntimeException("Cannot encrypt/decrypt"); + } + } +} diff --git a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java index afa8a7298..f76c658cd 100644 --- a/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java +++ b/src/com/jpexs/decompiler/flash/console/CommandLineArgumentParser.java @@ -485,9 +485,14 @@ public class CommandLineArgumentParser { out.println(" ...Decompress and save it to "); } + if (filter == null || filter.equals("encrypt")) { + out.println(" " + (cnt++) + ") -encrypt "); + out.println(" ...Encrypts file with HARMAN Air encryption and saves it to "); + } + if (filter == null || filter.equals("decrypt")) { out.println(" " + (cnt++) + ") -decrypt "); - out.println(" ...Decrypts HARMAN Air encrypted file and save it to "); + out.println(" ...Decrypts HARMAN Air encrypted file and saves it to "); } if (filter == null || filter.equals("swf2xml")) { @@ -1057,6 +1062,9 @@ public class CommandLineArgumentParser { } else if (command.equals("decompress")) { parseDecompress(args); System.exit(0); + } else if (command.equals("encrypt")) { + parseDecrypt(args); + System.exit(0); } else if (command.equals("decrypt")) { parseDecrypt(args); System.exit(0); @@ -2764,6 +2772,27 @@ public class CommandLineArgumentParser { System.exit(result ? 0 : 1); } + private static void parseEncrypt(Stack args) { + if (args.size() < 2) { + badArguments("encrypt"); + } + + boolean result = false; + try { + try (InputStream fis = new BufferedInputStream(new StdInAwareFileInputStream(args.pop())); OutputStream fos = new BufferedOutputStream(new FileOutputStream(args.pop()))) { + result = SWF.encrypt(fis, fos); + System.out.println(result ? "OK" : "FAIL"); + } catch (FileNotFoundException ex) { + System.err.println("File not found."); + System.exit(1); + } + } catch (IOException ex) { + logger.log(Level.SEVERE, null, ex); + } + + System.exit(result ? 0 : 1); + } + private static void parseDecrypt(Stack args) { if (args.size() < 2) { badArguments("decrypt"); @@ -4535,6 +4564,7 @@ public class CommandLineArgumentParser { pw.println("fileSize=" + swf.fileSize); pw.println("version=" + swf.version); pw.println("compression=" + swf.compression); + pw.println("encrypted=" + swf.encrypted); pw.println("gfx=" + swf.gfx); pw.println("width=" + doubleToString(swf.displayRect.getWidth() / SWF.unitDivisor)); pw.println("height=" + doubleToString(swf.displayRect.getHeight() / SWF.unitDivisor)); @@ -4564,9 +4594,11 @@ public class CommandLineArgumentParser { pw.println("actionScript3=" + fa.actionScript3); pw.println("hasMetadata=" + fa.hasMetadata); pw.println("noCrossDomainCache=" + fa.noCrossDomainCache); + pw.println("swfRelativeUrls=" + fa.swfRelativeUrls); pw.println("useDirectBlit=" + fa.useDirectBlit); pw.println("useGPU=" + fa.useGPU); pw.println("useNetwork=" + fa.useNetwork); + pw.println(); } @@ -4886,6 +4918,7 @@ public class CommandLineArgumentParser { pw.println("fileSize=" + swf.fileSize); pw.println("version=" + swf.version); pw.println("compression=" + swf.compression); + pw.println("encrypted=" + swf.encrypted); pw.println("gfx=" + swf.gfx); pw.println("displayRect=[" + swf.displayRect.Xmin + ", " + swf.displayRect.Ymin + ", " + swf.displayRect.Xmax + ", " + swf.displayRect.Ymax + "]"); pw.println("width=" + swf.displayRect.getWidth()); diff --git a/src/com/jpexs/decompiler/flash/gui/HeaderInfoPanel.java b/src/com/jpexs/decompiler/flash/gui/HeaderInfoPanel.java index 692ceda91..5d396d2f9 100644 --- a/src/com/jpexs/decompiler/flash/gui/HeaderInfoPanel.java +++ b/src/com/jpexs/decompiler/flash/gui/HeaderInfoPanel.java @@ -57,6 +57,8 @@ public class HeaderInfoPanel extends JPanel implements TagEditorPanel { private final JLabel gfxLabel = new JLabel(); + private final JLabel encryptedLabel = new JLabel(); + private final JLabel versionLabel = new JLabel(); private final JLabel fileSizeLabel = new JLabel(); @@ -82,6 +84,8 @@ public class HeaderInfoPanel extends JPanel implements TagEditorPanel { private final JSpinner versionEditor = new JSpinner(); private final JCheckBox gfxCheckBox = new JCheckBox(); + + private final JCheckBox encryptedCheckBox = new JCheckBox(); private final JPanel frameRateEditorPanel = new JPanel(); @@ -132,7 +136,7 @@ public class HeaderInfoPanel extends JPanel implements TagEditorPanel { {TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED, TableLayout.PREFERRED, - TableLayout.PREFERRED} + TableLayout.PREFERRED, TableLayout.PREFERRED} })); FlowLayout layout = new FlowLayout(SwingConstants.WEST); @@ -155,7 +159,11 @@ public class HeaderInfoPanel extends JPanel implements TagEditorPanel { }); versionEditorPanel.add(versionEditor); - gfxCheckBox.addActionListener((ActionEvent e) -> { + encryptedCheckBox.addChangeListener((ChangeEvent e) -> { + validateHeader(); + }); + + gfxCheckBox.addChangeListener((ChangeEvent e) -> { validateHeader(); }); @@ -237,23 +245,26 @@ public class HeaderInfoPanel extends JPanel implements TagEditorPanel { propertiesPanel.add(new JLabel(AppStrings.translate("header.version")), "0,2"); propertiesPanel.add(versionLabel, "1,2"); propertiesPanel.add(versionEditorPanel, "1,2"); - propertiesPanel.add(new JLabel(AppStrings.translate("header.gfx")), "0,3"); - propertiesPanel.add(gfxLabel, "1,3"); - propertiesPanel.add(gfxCheckBox, "1,3"); - propertiesPanel.add(new JLabel(AppStrings.translate("header.filesize")), "0,4"); - propertiesPanel.add(fileSizeLabel, "1,4"); - propertiesPanel.add(new JLabel(AppStrings.translate("header.framerate")), "0,5"); - propertiesPanel.add(frameRateLabel, "1,5"); - propertiesPanel.add(frameRateEditorPanel, "1,5"); - propertiesPanel.add(new JLabel(AppStrings.translate("header.framecount")), "0,6"); - propertiesPanel.add(frameCountLabel, "1,6"); - propertiesPanel.add(frameCountEditorPanel, "1,6"); - propertiesPanel.add(new JLabel(AppStrings.translate("header.displayrect")), "0,7"); - propertiesPanel.add(displayRectTwipsLabel, "1,7"); - propertiesPanel.add(displayRectEditorPanel, "1,7"); - propertiesPanel.add(new JLabel(""), "0,8"); - propertiesPanel.add(displayRectPixelsLabel, "1,8"); - propertiesPanel.add(warningPanel, "0,9,1,9"); + propertiesPanel.add(new JLabel(AppStrings.translate("header.encrypted")), "0,3"); + propertiesPanel.add(encryptedLabel, "1,3"); + propertiesPanel.add(encryptedCheckBox, "1,3"); + propertiesPanel.add(new JLabel(AppStrings.translate("header.gfx")), "0,4"); + propertiesPanel.add(gfxLabel, "1,4"); + propertiesPanel.add(gfxCheckBox, "1,4"); + propertiesPanel.add(new JLabel(AppStrings.translate("header.filesize")), "0,5"); + propertiesPanel.add(fileSizeLabel, "1,5"); + propertiesPanel.add(new JLabel(AppStrings.translate("header.framerate")), "0,6"); + propertiesPanel.add(frameRateLabel, "1,6"); + propertiesPanel.add(frameRateEditorPanel, "1,6"); + propertiesPanel.add(new JLabel(AppStrings.translate("header.framecount")), "0,7"); + propertiesPanel.add(frameCountLabel, "1,7"); + propertiesPanel.add(frameCountEditorPanel, "1,7"); + propertiesPanel.add(new JLabel(AppStrings.translate("header.displayrect")), "0,8"); + propertiesPanel.add(displayRectTwipsLabel, "1,8"); + propertiesPanel.add(displayRectEditorPanel, "1,8"); + propertiesPanel.add(new JLabel(""), "0,9"); + propertiesPanel.add(displayRectPixelsLabel, "1,9"); + propertiesPanel.add(warningPanel, "0,10,1,10"); add(propertiesPanel, BorderLayout.CENTER); @@ -305,6 +316,7 @@ public class HeaderInfoPanel extends JPanel implements TagEditorPanel { swf.compression = getCompression(); swf.version = getVersionNumber(); swf.gfx = gfxCheckBox.isSelected(); + swf.encrypted = encryptedCheckBox.isSelected() && !gfxCheckBox.isSelected(); swf.frameRate = ((Number) (frameRateEditor.getModel().getValue())).floatValue(); swf.frameCount = ((Number) (frameCountEditor.getModel().getValue())).intValue(); double multiplier = 1.0; @@ -350,9 +362,12 @@ public class HeaderInfoPanel extends JPanel implements TagEditorPanel { versionLabel.setText(Integer.toString(swf.version)); versionEditor.setModel(new SpinnerNumberModel(swf.version, 1, SWF.MAX_VERSION, 1)); + encryptedLabel.setText(swf.encrypted ? AppStrings.translate("yes") : AppStrings.translate("no")); + encryptedCheckBox.setSelected(swf.encrypted); + gfxLabel.setText(swf.gfx ? AppStrings.translate("yes") : AppStrings.translate("no")); gfxCheckBox.setSelected(swf.gfx); - + fileSizeLabel.setText(Long.toString(swf.fileSize)); frameRateLabel.setText(Float.toString(swf.frameRate)); @@ -391,6 +406,9 @@ public class HeaderInfoPanel extends JPanel implements TagEditorPanel { versionEditor.addChangeListener((ChangeEvent e) -> { setModified(); }); + encryptedCheckBox.addChangeListener((ChangeEvent e) -> { + setModified(); + }); gfxCheckBox.addChangeListener((ChangeEvent e) -> { setModified(); }); @@ -441,6 +459,8 @@ public class HeaderInfoPanel extends JPanel implements TagEditorPanel { compressionEditorPanel.setVisible(edit); versionLabel.setVisible(!edit); versionEditorPanel.setVisible(edit); + encryptedLabel.setVisible(!edit); + encryptedCheckBox.setVisible(edit); gfxLabel.setVisible(!edit); gfxCheckBox.setVisible(edit); frameRateLabel.setVisible(!edit); @@ -470,6 +490,7 @@ public class HeaderInfoPanel extends JPanel implements TagEditorPanel { private boolean validateHeader() { int version = getVersionNumber(); boolean gfx = gfxCheckBox.isSelected(); + boolean encrypted = encryptedCheckBox.isSelected(); SWFCompression compression = getCompression(); String resultStr = ""; boolean result = true; @@ -477,6 +498,11 @@ public class HeaderInfoPanel extends JPanel implements TagEditorPanel { resultStr += AppStrings.translate("header.warning.unsupportedGfxCompression") + " "; result = false; } + + if (gfx && encrypted) { + resultStr += AppStrings.translate("header.warning.unsupportedGfxEncryption") + " "; + result = false; + } if (compression == SWFCompression.ZLIB && version < 6) { resultStr += AppStrings.translate("header.warning.minimumZlibVersion") + " "; diff --git a/src/com/jpexs/decompiler/flash/gui/Main.java b/src/com/jpexs/decompiler/flash/gui/Main.java index 19c01a215..158f2daa6 100644 --- a/src/com/jpexs/decompiler/flash/gui/Main.java +++ b/src/com/jpexs/decompiler/flash/gui/Main.java @@ -1320,7 +1320,7 @@ public class Main { result.sourceInfo = sourceInfo; boolean hasVideoStreams = false; - boolean hasEncrypted = false; + //boolean hasEncrypted = false; for (Openable openable : result) { openable.setOpenableList(result); @@ -1342,9 +1342,9 @@ public class Main { } } - if (swf.encrypted) { + /*if (swf.encrypted) { hasEncrypted = true; - } + }*/ swf.addEventListener(new EventListener() { @Override @@ -1402,7 +1402,7 @@ public class Main { }); } - if (hasEncrypted) { + /*if (hasEncrypted) { View.execInEventDispatchLater(new Runnable() { @Override public void run() { @@ -1410,7 +1410,7 @@ public class Main { } }); - } + }*/ return result; } diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties index 8288a9317..3fd0cb63a 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties @@ -1236,4 +1236,7 @@ debug.export.bytearray = Export byte array data... debug.import = Import to %name% debug.import.bytearray = Import byte array data... -action.edit.flex = (Flex compiler) \ No newline at end of file +action.edit.flex = (Flex compiler) + +header.encrypted = Harman encrypted: +header.warning.unsupportedGfxEncryption = GFX does not support Harman encryption. \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties index a0b170a5e..252f248ea 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties @@ -1213,4 +1213,7 @@ debug.export.bytearray = Exportovat byte array data... debug.import = Importovat do %name% debug.import.bytearray = Importovat byte array data... -action.edit.flex = (kompil\u00e1tor Flex) \ No newline at end of file +action.edit.flex = (kompil\u00e1tor Flex) + +header.encrypted = Harman \u0161ifrov\u00e1n\u00ed: +header.warning.unsupportedGfxEncryption = GFX nepodporuje Harman \u0161ifrov\u00e1n\u00ed. \ No newline at end of file