diff --git a/iped-app/resources/config/conf/CategoriesConfig.json b/iped-app/resources/config/conf/CategoriesConfig.json index b25841d7d5..31957942cb 100644 --- a/iped-app/resources/config/conf/CategoriesConfig.json +++ b/iped-app/resources/config/conf/CategoriesConfig.json @@ -74,7 +74,7 @@ {"name": "GDrive File Entries", "mimes": ["application/x-gdrive-cloud-graph-registry", "application/x-gdrive-snapshot-registry"]} ]}, {"name": "Databases", "mimes": ["application/x-edb", "application/x-edb-table", "application/irpf", "application/x-msaccess", "application/x-dbf", "application/vnd.oasis.opendocument.database", "application/x-sqlite3", "application/x-mysql-db", "application/x-berkeley-db", "application/x-mssql-data", "application/x-database-table"]}, - {"name": "Compressed Archives", "mimes": ["application/x-tika-ooxml", "application/zlib", "application/applefile", "application/vnd.ms-tnef", "application/zip", "application/x-rar-compressed", "application/x-tar", "application/gzip", "application/x-gzip", "application/x-xz", "application/x-bzip", "application/x-bzip2", "application/x-7z-compressed", "application/x-arj", "application/x-gtar", "application/x-archive", "application/x-cpio", "application/x-tika-unix-dump", "application/x-snappy-framed", "application/x-snappy", "application/x-snappy-raw", "application/x-compress", "application/x-java-pack200", "application/x-lzma", "application/x-lz4", "application/x-lz4-block", "application/x-brotli", "application/zstd", "application/deflate64", "image/x-emf-compressed", "application/x-lzfse"]}, + {"name": "Compressed Archives", "mimes": ["application/x-tika-ooxml", "application/zlib", "application/applefile", "application/vnd.ms-tnef", "application/zip", "application/x-rar-compressed", "application/x-tar", "application/gzip", "application/x-gzip", "application/x-xz", "application/x-bzip", "application/x-bzip2", "application/x-7z-compressed", "application/x-arj", "application/x-gtar", "application/x-archive", "application/x-cpio", "application/x-tika-unix-dump", "application/x-snappy-framed", "application/x-snappy", "application/x-snappy-raw", "application/x-compress", "application/x-java-pack200", "application/x-lzma", "application/x-lz4", "application/x-lz4-block", "application/x-brotli", "application/zstd", "application/deflate64", "image/x-emf-compressed", "application/x-lzfse", "application/x-android-backup"]}, {"name": "Contacts", "mimes": ["text/x-vcard", "application/x-vcard-html", "application/windows-adress-book", "application/outlook-contact", "application/x-livecontacts", "application/x-livecontacts-table", "contact/x-skype-contact", "application/x-whatsapp-wadb", "application/x-whatsapp-contactsv2", "contact/x-whatsapp-contact", "application/x-ufed-html-contacts", "application/x-ufed-contact", "contact/x-telegram-contact", "application/x-ios-addressbook-db", "application/x-win10-mail-contact"]}, {"name": "Chats", "categories":[ {"name": "WhatsApp", "mimes":["application/x-whatsapp-db", "application/x-whatsapp-chatstorage", "application/x-whatsapp-chat","application/x-ufed-chat-whatsapp","application/x-ufed-chat-preview-whatsapp"]}, diff --git a/iped-app/resources/config/conf/CustomSignatures.xml b/iped-app/resources/config/conf/CustomSignatures.xml index 25dc73e364..d0a98f8894 100644 --- a/iped-app/resources/config/conf/CustomSignatures.xml +++ b/iped-app/resources/config/conf/CustomSignatures.xml @@ -1443,6 +1443,12 @@ + + + + + + diff --git a/iped-app/resources/config/conf/ParserConfig.xml b/iped-app/resources/config/conf/ParserConfig.xml index fcbd0c302f..77b3b1f1af 100644 --- a/iped-app/resources/config/conf/ParserConfig.xml +++ b/iped-app/resources/config/conf/ParserConfig.xml @@ -269,6 +269,7 @@ + image/svg+xml diff --git a/iped-parsers/iped-parsers-impl/src/main/java/iped/parsers/android/backup/AndroidBackupParser.java b/iped-parsers/iped-parsers-impl/src/main/java/iped/parsers/android/backup/AndroidBackupParser.java new file mode 100644 index 0000000000..59a98ddc7a --- /dev/null +++ b/iped-parsers/iped-parsers-impl/src/main/java/iped/parsers/android/backup/AndroidBackupParser.java @@ -0,0 +1,286 @@ +package iped.parsers.android.backup; + +import java.io.Console; +import java.io.IOException; +import java.io.InputStream; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; +import java.util.Collections; +import java.util.Set; +import java.util.zip.Inflater; +import java.util.zip.InflaterInputStream; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +import org.apache.tika.exception.TikaException; +import org.apache.tika.metadata.Metadata; +import org.apache.tika.mime.MediaType; +import org.apache.tika.parser.ParseContext; +import org.bouncycastle.crypto.PBEParametersGenerator; +import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator; +import org.bouncycastle.crypto.params.KeyParameter; +import org.xml.sax.ContentHandler; +import org.xml.sax.SAXException; + +import iped.parsers.compress.PackageParser; + +/** + * Parses and extracts content from android backup files done with ADB. This + * work was based on code from + * https://github.com/nelenkov/android-backup-extractor. + * + * @author Patrick Dalla Bernardina + * + */ +public class AndroidBackupParser extends PackageParser { + private static final MediaType AB_MIMETYPE = MediaType.application("x-android-backup"); + + public static final Set SUPPORTED_TYPES = Collections.singleton(AB_MIMETYPE); + + private static final Object MAGIC = "ANDROID BACKUP"; + + private static final String AB_PREFIX = "androidBackup"; + private static final String ENC_METADATA = "encryption"; + + private static final String COMPRESSION_METADATA = "compression"; + + private static final String AB_VERSION_METADATA = "version"; + + private static final int MASTER_KEY_SIZE = 256; + + private static final int PBKDF2_SALT_SIZE = 512; + + private static final String ENCRYPTION_MECHANISM = "AES/CBC/PKCS5Padding"; + + private static final int PBKDF2_KEY_SIZE = 256; + + @Override + public Set getSupportedTypes(ParseContext arg0) { + return SUPPORTED_TYPES; + } + + @Override + public void parse(InputStream in, ContentHandler handler, Metadata metadata, ParseContext context) + throws IOException, SAXException, TikaException { + try { + String magic = readLine(in); + if (!magic.equals(MAGIC)) { + throw new TikaException("Invalid android backup magic."); + } + + int version = Integer.parseInt(readLine(in)); + + if (version < 1 || version > 5) { + throw new TikaException("Invalid android backup version:" + version + "."); + } + metadata.set(AB_PREFIX + ":" + AB_VERSION_METADATA, Integer.toString(version)); + + boolean isCompressed = Integer.parseInt(readLine(in)) == 1; + + String encryption = readLine(in); + metadata.set(AB_PREFIX + ":" + ENC_METADATA, encryption); + + if (encryption.equals("SHA-256")) { + try { + in = unencrypt(in, version, null); + } catch (Exception e) { + throw new TikaException(e.getMessage(), e); + } + if (in == null) { + throw new TikaException("Invalid password or master key checksum."); + } + } + + Inflater inflater; + if (isCompressed) { + inflater = new Inflater(); + in = new InflaterInputStream(in, inflater); + } + + try { + super.parse(in, handler, metadata, context); + }finally { + } + + + } catch (TikaException te) { + throw te; + } catch (Exception e) { + new TikaException(e.getMessage(), e); + } + + } + + private InputStream unencrypt(InputStream in, int version, String password) + throws InvalidKeyException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException, + NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException { + if (Cipher.getMaxAllowedKeyLength("AES") < MASTER_KEY_SIZE) { + System.err.println("WARNING: Maximum allowed key-length seems smaller than needed. " + + "Please check that unlimited strength cryptography is available, see README.md for details"); + } + + if (password == null || "".equals(password)) { + Console console = System.console(); + if (console != null) { + System.err.println("This backup is encrypted, please provide the password"); + password = new String(console.readPassword("Password: ")); + } else { + throw new IllegalArgumentException("Backup encrypted but password not specified"); + } + } + + String userSaltHex = readLine(in); // 5 + byte[] userSalt = hexToByteArray(userSaltHex); + if (userSalt.length != PBKDF2_SALT_SIZE / 8) { + throw new IllegalArgumentException("Invalid salt length: " + userSalt.length); + } + + String ckSaltHex = readLine(in); // 6 + byte[] ckSalt = hexToByteArray(ckSaltHex); + + int rounds = Integer.parseInt(readLine(in)); // 7 + String userIvHex = readLine(in); // 8 + + String masterKeyBlobHex = readLine(in); // 9 + + // decrypt the master key blob + Cipher c = Cipher.getInstance(ENCRYPTION_MECHANISM); + // XXX we don't support non-ASCII passwords + SecretKey userKey = buildPasswordKey(password, userSalt, rounds, false); + byte[] IV = hexToByteArray(userIvHex); + IvParameterSpec ivSpec = new IvParameterSpec(IV); + c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(userKey.getEncoded(), "AES"), ivSpec); + byte[] mkCipher = hexToByteArray(masterKeyBlobHex); + byte[] mkBlob = c.doFinal(mkCipher); + + // first, the master key IV + int offset = 0; + int len = mkBlob[offset++]; + IV = Arrays.copyOfRange(mkBlob, offset, offset + len); + offset += len; + // then the master key itself + len = mkBlob[offset++]; + byte[] mk = Arrays.copyOfRange(mkBlob, offset, offset + len); + offset += len; + // and finally the master key checksum hash + len = mkBlob[offset++]; + byte[] mkChecksum = Arrays.copyOfRange(mkBlob, offset, offset + len); + + // now validate the decrypted master key against the checksum + // first try the algorithm matching the archive version + boolean useUtf = version >= 2; + byte[] calculatedCk = makeKeyChecksum(mk, ckSalt, rounds, useUtf); + System.err.printf("Calculated MK checksum (use UTF-8: %s): %s\n", useUtf, toHex(calculatedCk)); + if (!Arrays.equals(calculatedCk, mkChecksum)) { + System.err.println("Checksum does not match."); + // try the reverse + calculatedCk = makeKeyChecksum(mk, ckSalt, rounds, !useUtf); + System.err.printf("Calculated MK checksum (use UTF-8: %s): %s\n", useUtf, toHex(calculatedCk)); + } + + if (Arrays.equals(calculatedCk, mkChecksum)) { + ivSpec = new IvParameterSpec(IV); + c.init(Cipher.DECRYPT_MODE, new SecretKeySpec(mk, "AES"), ivSpec); + // Only if all of the above worked properly will 'result' be + // assigned + return new CipherInputStream(in, c); + } + return null; + } + + public static String toHex(byte[] bytes) { + StringBuilder buff = new StringBuilder(); + for (byte b : bytes) { + buff.append(String.format("%02X", b)); + } + + return buff.toString(); + } + + public static byte[] makeKeyChecksum(byte[] pwBytes, byte[] salt, int rounds, boolean useUtf8) { + char[] mkAsChar = new char[pwBytes.length]; + for (int i = 0; i < pwBytes.length; i++) { + mkAsChar[i] = (char) pwBytes[i]; + } + + Key checksum = buildCharArrayKey(mkAsChar, salt, rounds, useUtf8); + return checksum.getEncoded(); + } + + public static byte[] hexToByteArray(String digits) { + final int bytes = digits.length() / 2; + if (2 * bytes != digits.length()) { + throw new IllegalArgumentException("Hex string must have an even number of digits"); + } + + byte[] result = new byte[bytes]; + for (int i = 0; i < digits.length(); i += 2) { + result[i / 2] = (byte) Integer.parseInt(digits.substring(i, i + 2), 16); + } + return result; + } + + private String readLine(InputStream in) throws IOException { + StringBuffer buff = new StringBuffer(); + char b = (char) in.read(); + while (b != '\n') { + buff.append(b); + b = (char) in.read(); + } + + return buff.toString(); + } + + public static SecretKey buildCharArrayKey(char[] pwArray, byte[] salt, int rounds, boolean useUtf8) { + // Original code from BackupManagerService + // this produces different results when run with Sun/Oracale Java SE + // which apparently treats password bytes as UTF-8 (16?) + // (the encoding is left unspecified in PKCS#5) + + // try { + // SecretKeyFactory keyFactory = SecretKeyFactory + // .getInstance("PBKDF2WithHmacSHA1"); + // KeySpec ks = new PBEKeySpec(pwArray, salt, rounds, PBKDF2_KEY_SIZE); + // return keyFactory.generateSecret(ks); + // } catch (InvalidKeySpecException e) { + // throw new RuntimeException(e); + // } catch (NoSuchAlgorithmException e) { + // throw new RuntimeException(e); + // } catch (NoSuchProviderException e) { + // throw new RuntimeException(e); + // } + // return null; + + return androidPBKDF2(pwArray, salt, rounds, useUtf8); + } + + public static SecretKey androidPBKDF2(char[] pwArray, byte[] salt, int rounds, boolean useUtf8) { + PBEParametersGenerator generator = new PKCS5S2ParametersGenerator(); + // Android treats password bytes as ASCII, which is obviously + // not the case when an AES key is used as a 'password'. + // Use the same method for compatibility. + + // Android 4.4 however uses all char bytes + // useUtf8 needs to be true for KitKat + byte[] pwBytes = useUtf8 ? PBEParametersGenerator.PKCS5PasswordToUTF8Bytes(pwArray) + : PBEParametersGenerator.PKCS5PasswordToBytes(pwArray); + generator.init(pwBytes, salt, rounds); + KeyParameter params = (KeyParameter) generator.generateDerivedParameters(PBKDF2_KEY_SIZE); + + return new SecretKeySpec(params.getKey(), "AES"); + } + + private static SecretKey buildPasswordKey(String pw, byte[] salt, int rounds, boolean useUtf8) { + return buildCharArrayKey(pw.toCharArray(), salt, rounds, useUtf8); + } +}