diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f1a08139..5205c6159 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ All notable changes to this project will be documented in this file. ## [Unreleased] ### Added - [#1998] Setting for maximum number of items in the cache - allows less memory consumption (Defaults to 500 per cache) +- [#2038], [#2028], [#2034], [#2036] Support for Harman AIR encrypted SWFs (Read-only) ### Fixed - [#2004] Freezing when a shape has nonimage character set as fill @@ -3035,6 +3036,10 @@ All notable changes to this project will be documented in this file. [alpha 8]: https://github.com/jindrapetrik/jpexs-decompiler/compare/alpha7...alpha8 [alpha 7]: https://github.com/jindrapetrik/jpexs-decompiler/releases/tag/alpha7 [#1998]: https://www.free-decompiler.com/flash/issues/1998 +[#2038]: https://www.free-decompiler.com/flash/issues/2038 +[#2028]: https://www.free-decompiler.com/flash/issues/2028 +[#2034]: https://www.free-decompiler.com/flash/issues/2034 +[#2036]: https://www.free-decompiler.com/flash/issues/2036 [#2004]: https://www.free-decompiler.com/flash/issues/2004 [#2008]: https://www.free-decompiler.com/flash/issues/2008 [#2007]: https://www.free-decompiler.com/flash/issues/2007 diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/HarmanDecryption.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/HarmanDecryption.java new file mode 100644 index 000000000..dcd92497b --- /dev/null +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/HarmanDecryption.java @@ -0,0 +1,141 @@ +/* + * 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.ByteArrayOutputStream; +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]; + //int aesKeyIdx = 8 + 4 + paddedLength; //skip header, size, data + + 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); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + baos.write(decryptedData); + + 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 285a7197c..c0fcc0b64 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWF.java @@ -190,6 +190,9 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; @@ -211,6 +214,9 @@ import java.util.logging.Level; import java.util.logging.Logger; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterInputStream; +import javax.crypto.BadPaddingException; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; /** * Class representing SWF file @@ -293,6 +299,11 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { * ScaleForm GFx */ public boolean gfx = false; + + /** + * HARMAN encryption + */ + public boolean encrypted = false; @Internal public OpenableList openableList; @@ -414,7 +425,10 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { "ZWS", // LZMA compressed Flash "GFX", // Uncompressed ScaleForm GFx "CFX", // Compressed ScaleForm GFx - "ABC" // Non-standard LZMA compressed Flash + "ABC", // Non-standard LZMA compressed Flash + "fWS", //Harman encrypted uncompressed Flash, + "cWS", //Harman encrypted ZLib compressed Flash, + "zWS" //Harman encrypted LZMA compressed Flash ); /** @@ -1106,10 +1120,13 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { } public byte[] getHeaderBytes() { - return getHeaderBytes(compression, gfx); + return getHeaderBytes(compression, gfx, encrypted); } private static byte[] getHeaderBytes(SWFCompression compression, boolean gfx) { + return getHeaderBytes(compression, gfx, false); + } + private static byte[] getHeaderBytes(SWFCompression compression, boolean gfx, boolean encrypted) { if (compression == SWFCompression.LZMA_ABC) { return new byte[]{'A', 'B', 'C'}; } @@ -1133,6 +1150,10 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { ret[1] = 'W'; ret[2] = 'S'; } + + if (!gfx && encrypted) { + ret[0] += 32; //to lowercase + } return ret; } @@ -1463,6 +1484,7 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { ByteArrayOutputStream baos = new ByteArrayOutputStream(); SWFHeader header = decompress(is, baos, true); gfx = header.gfx; + encrypted = header.encrypted; compression = header.compression; lzmaProperties = header.lzmaProperties; uncompressedData = baos.toByteArray(); @@ -1964,7 +1986,7 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { SWFHeader header = new SWFHeader(); header.version = version; header.fileSize = fileSize; - header.gfx = headerData[1] == 'F' && headerData[2] == 'X'; + header.gfx = headerData[1] == 'F' && headerData[2] == 'X'; return header; } @@ -1984,6 +2006,20 @@ public final class SWF implements SWFContainerItem, Timelined, Openable { sos.write(getHeaderBytes(SWFCompression.NONE, header.gfx)); sos.writeUI8(header.version); sos.writeUI32(fileSize); + + switch (hdr[0]) { + case 'c': + case 'z': + case 'f': + header.encrypted = true; + HarmanDecryption dec = new HarmanDecryption(); + try { + is = dec.decrypt(is, hdr); //Note: this call will uppercase hdr[0] + } catch (IOException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchAlgorithmException | BadPaddingException | IllegalBlockSizeException | NoSuchPaddingException ex) { + throw new SwfOpenException(AppResources.translate("error.swf.decryptionProblem")); + } + break; + } switch (hdr[0]) { case 'C': { // CWS, CFX diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWFHeader.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWFHeader.java index a7b0a8560..f19021e26 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWFHeader.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/SWFHeader.java @@ -46,6 +46,11 @@ public class SWFHeader { * ScaleForm GFx */ public boolean gfx = false; + + /** + * Harman SWF Encryption + */ + public boolean encrypted = false; /** * Frame rate diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java index 7cb1a3244..4d1363afd 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/configuration/Configuration.java @@ -911,6 +911,9 @@ public final class Configuration { @ConfigurationCategory("script") public static ConfigurationItem warningInitializersClass = null; + @ConfigurationDefaultBoolean(true) + @ConfigurationName("warning.cannotencrypt") + public static ConfigurationItem warningCannotEncrypt = null; private enum OSId { WINDOWS, OSX, UNIX diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/locales/AppResources.properties b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/locales/AppResources.properties index 249778db4..129a7dfec 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/locales/AppResources.properties +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/locales/AppResources.properties @@ -47,3 +47,4 @@ package.default = error.swf.invalid = Invalid SWF file, wrong signature. error.swf.headerTooShort = SWF header is too short. error.abc.invalid = Invalid ABC file. +error.swf.decryptionProblem = Invalid SWF file, decryption failed. diff --git a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/locales/AppResources_cs.properties b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/locales/AppResources_cs.properties index a42a8a0d2..6f4e2ddca 100644 --- a/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/locales/AppResources_cs.properties +++ b/libsrc/ffdec_lib/src/com/jpexs/decompiler/flash/locales/AppResources_cs.properties @@ -47,3 +47,6 @@ package.default = error.swf.invalid = Neplatn\u00fd SWF soubor, \u0161patn\u00e1 signatura. error.swf.headerTooShort = SWF hlavi\u010dka je p\u0159\u00edli\u0161 kr\u00e1tk\u00e1. error.abc.invalid = Neplatn\u00fd ABC soubor. + +#after 18.4.1 +error.swf.decryptionProblem = Neplatn\u00fd SWF soubor, selhalo de\u0161ifrov\u00e1n\u00ed. \ No newline at end of file diff --git a/src/com/jpexs/decompiler/flash/gui/Main.java b/src/com/jpexs/decompiler/flash/gui/Main.java index aa85624e0..ea736f25c 100644 --- a/src/com/jpexs/decompiler/flash/gui/Main.java +++ b/src/com/jpexs/decompiler/flash/gui/Main.java @@ -1268,6 +1268,7 @@ public class Main { result.sourceInfo = sourceInfo; boolean hasVideoStreams = false; + boolean hasEncrypted = false; for (Openable openable : result) { openable.setOpenableList(result); @@ -1289,6 +1290,10 @@ public class Main { } } + if (swf.encrypted) { + hasEncrypted = true; + } + swf.addEventListener(new EventListener() { @Override public void handleExportingEvent(String type, int index, int count, Object data) { @@ -1344,6 +1349,16 @@ public class Main { }); } + + if (hasEncrypted) { + View.execInEventDispatchLater(new Runnable() { + @Override + public void run() { + ViewMessages.showMessageDialog(getDefaultMessagesComponent(), AppStrings.translate("warning.cannotencrypt").replace("%file%", sourceInfo.getFileTitleOrName()), AppStrings.translate("message.warning"), JOptionPane.WARNING_MESSAGE, Configuration.warningCannotEncrypt); + } + + }); + } return result; } diff --git a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties index cc101e136..0a3b8bae4 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog.properties @@ -691,3 +691,6 @@ config.description.warning.initializers.class = Show warning on AS3 class trait #after 18.4.1 config.name.maxCachedNum = Maximum number of cached items per single cache config.description.maxCachedNum = Maximum number of cached items before older items are removed from cache. Lower value = less memory, slower app. Higher value = more memory, faster app. Set this to 0 to unlimited caching. + +config.name.warning.cannotencrypt = Warn when cannot save encrypted +config.description.warning.cannotencrypt = Show warning when cannot save SWF file which was encrypted using HARMAN Air encryption. diff --git a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_cs.properties b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_cs.properties index 033f12e50..31356da17 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_cs.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/AdvancedSettingsDialog_cs.properties @@ -681,3 +681,6 @@ config.description.warning.initializers.class = Zobrazovat varov\u00e1n\u00ed p\ #after 18.4.1 config.name.maxCachedNum = Maxim\u00e1ln\u00ed po\u010det prvk\u016f v jedn\u00e9 cache config.description.maxCachedNum = Maxim\u00e1ln\u00ed po\u010det cachovan\u00fdch prvk\u016f p\u0159ed t\u00edm, ne\u017e jsou star\u0161\u00ed prvky vymaz\u00e1ny. Ni\u017e\u0161\u00ed hodnota = m\u00e9n\u011b pam\u011bti, pomalej\u0161\u00ed aplikace. Vy\u0161\u0161\u00ed hodnota = v\u00edce pam\u011bti, rychlej\u0161\u00ed aplikace. Nastavte sem hodnotu 0 pro nekone\u010dn\u00e9 cachov\u00e1n\u00ed. + +config.name.warning.cannotencrypt = Varovat kdy\u017e nelze ulo\u017eit za\u0161ifrovan\u00e9 +config.description.warning.cannotencrypt = Zobrazit varov\u00e1n\u00ed kdy\u017e nelze ulo\u017eit SWF soubor kter\u00fd byl \u0161ifrov\u00e1n pomoc\u00ed HARMAN Air \u0161ifrov\u00e1n\u00ed. diff --git a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties index 2316ecdbe..fa0353420 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame.properties @@ -1135,4 +1135,7 @@ warning.initializers.class = The "extends" object is also used in script initial contextmenu.addScript.doaction = Add frame script - DoAction contextmenu.addScript.buttoncondaction = Add button event script - BUTTONCONDACTION contextmenu.addScript.clipactionrecord = Add instance event script - CLIPACTIONRECORD -contextmenu.addScript.doinitaction = Add sprite init script - DoInitAction \ No newline at end of file +contextmenu.addScript.doinitaction = Add sprite init script - DoInitAction + +#after 18.4.1 +warning.cannotencrypt = WARNING: The file %file% was encrypted using HARMAN Air encryption.\r\nIt was successfully decrypted to be loaded, but if you want to save modified file later,\r\nthe encryption will be stripped ( = not encrypted). 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 1a901f262..c2c4f6ebf 100644 --- a/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties +++ b/src/com/jpexs/decompiler/flash/gui/locales/MainFrame_cs.properties @@ -1120,4 +1120,7 @@ warning.initializers.class = Polo\u017eky class jako extends a/nebo implements j contextmenu.addScript.doaction = P\u0159idat frame skript - DoAction contextmenu.addScript.buttoncondaction = P\u0159idat skript ud\u00e1losti tla\u010d\u00edtka - BUTTONCONDACTION contextmenu.addScript.clipactionrecord = P\u0159idat skript ud\u00e1losti instance - CLIPACTIONRECORD -contextmenu.addScript.doinitaction = P\u0159idat inicializa\u010dn\u00ed skript spritu - DoInitAction \ No newline at end of file +contextmenu.addScript.doinitaction = P\u0159idat inicializa\u010dn\u00ed skript spritu - DoInitAction + +#after 18.4.1 +warning.cannotencrypt = VAROV\u00c1N\u00cd: Soubor %file% byl za\u0161ifrov\u00e1n pomoc\u00ed HARMAN Air \u0161ifrov\u00e1n\u00ed.\r\nByl \u00fasp\u011b\u0161n\u011b roz\u0161ifrov\u00e1n pro na\u010dten\u00ed, ale pokud budete cht\u00edt soubor ulo\u017eit,\r\n\u0161ifrov\u00e1n\u00ed se ztrat\u00ed ( = nebude za\u0161ifrov\u00e1n).