├── .gitignore
├── libs
├── gson-2.8.0.jar
├── jna-4.3.0.jar
├── xmpcore-5.1.3.jar
├── WMI4Java-1.5.1.jar
├── gson-2.8.0-javadoc.jar
├── jna-platform-4.3.0.jar
└── metadata-extractor-2.10.1.jar
├── docs
├── IO User Manual.pdf
└── IO Developer Guide.pdf
├── src
└── com
│ └── ciphertechsolutions
│ └── io
│ ├── ui
│ ├── icons
│ │ ├── ion.ico
│ │ ├── ion.jpg
│ │ ├── ion.png
│ │ ├── ctLogo.jpg
│ │ ├── splash.jpg
│ │ ├── infoIcon.png
│ │ ├── settings.png
│ │ ├── smallerSplash.jpg
│ │ ├── ionLogoTrimmed.jpg
│ │ ├── settings-white.png
│ │ └── ionInvertedCropped.jpg
│ ├── InfoScreenController.java
│ ├── css
│ │ └── style.css
│ ├── fxml
│ │ ├── IONDevice.fxml
│ │ ├── IONImaging.fxml
│ │ ├── IONMain.fxml
│ │ ├── IONInfo.fxml
│ │ └── IONOptions.fxml
│ ├── GUILauncher.java
│ ├── SelectDeviceController.java
│ ├── MainScreenController.java
│ ├── ImagingController.java
│ └── AdvancedOptionsController.java
│ ├── ewf
│ ├── Header2Section.java
│ ├── SessionSection.java
│ ├── DiskSection.java
│ ├── DataSection.java
│ ├── SectorsSection.java
│ ├── DoneSection.java
│ ├── Table2Section.java
│ ├── NextSection.java
│ ├── DataChunk.java
│ ├── HashSection.java
│ ├── DigestSection.java
│ ├── TableSection.java
│ ├── Error2Section.java
│ ├── VolumeSectionManager.java
│ ├── Section.java
│ ├── HeaderSection.java
│ ├── VolumeSection.java
│ └── AbstractHeaderSection.java
│ ├── applicationLogic
│ ├── IStoppable.java
│ ├── ApplicationState.java
│ ├── options
│ │ ├── CompressionTypesEnum.java
│ │ └── AdvancedOptions.java
│ ├── Utils.java
│ ├── IProcessController.java
│ └── ProcessController.java
│ ├── processing
│ ├── digests
│ │ ├── Md5Digest.java
│ │ ├── SHA1Digest.java
│ │ └── DigestBase.java
│ ├── IProcessor.java
│ ├── IMediaReader.java
│ ├── triage
│ │ ├── StringCarver.java
│ │ ├── TriageProcessorBase.java
│ │ └── ByteUtils.java
│ ├── ProcessorBase.java
│ ├── CompressionTask.java
│ ├── ChunkedCompressor.java
│ ├── DriveReader.java
│ └── ProcessorManager.java
│ ├── logging
│ ├── LogMessageType.java
│ ├── ObservableOutputStream.java
│ └── Logging.java
│ ├── device
│ ├── Device.java
│ ├── Volume.java
│ └── Disk.java
│ └── usb
│ ├── UsbWriteBlock.java
│ └── USBPoller.java
├── launch4j
├── IO.manifest
└── IOexe.cfg.xml
├── .project
├── .classpath
├── LICENSE
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /bin/
2 | /build/
3 | /build/build.xml
4 |
--------------------------------------------------------------------------------
/libs/gson-2.8.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/libs/gson-2.8.0.jar
--------------------------------------------------------------------------------
/libs/jna-4.3.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/libs/jna-4.3.0.jar
--------------------------------------------------------------------------------
/libs/xmpcore-5.1.3.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/libs/xmpcore-5.1.3.jar
--------------------------------------------------------------------------------
/docs/IO User Manual.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/docs/IO User Manual.pdf
--------------------------------------------------------------------------------
/libs/WMI4Java-1.5.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/libs/WMI4Java-1.5.1.jar
--------------------------------------------------------------------------------
/docs/IO Developer Guide.pdf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/docs/IO Developer Guide.pdf
--------------------------------------------------------------------------------
/libs/gson-2.8.0-javadoc.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/libs/gson-2.8.0-javadoc.jar
--------------------------------------------------------------------------------
/libs/jna-platform-4.3.0.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/libs/jna-platform-4.3.0.jar
--------------------------------------------------------------------------------
/libs/metadata-extractor-2.10.1.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/libs/metadata-extractor-2.10.1.jar
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/icons/ion.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ui/icons/ion.ico
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/icons/ion.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ui/icons/ion.jpg
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/icons/ion.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ui/icons/ion.png
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/icons/ctLogo.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ui/icons/ctLogo.jpg
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/icons/splash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ui/icons/splash.jpg
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/icons/infoIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ui/icons/infoIcon.png
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/icons/settings.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ui/icons/settings.png
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/Header2Section.java:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ewf/Header2Section.java
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/icons/smallerSplash.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ui/icons/smallerSplash.jpg
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/icons/ionLogoTrimmed.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ui/icons/ionLogoTrimmed.jpg
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/icons/settings-white.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ui/icons/settings-white.png
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/icons/ionInvertedCropped.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ciphertechsolutions/IO/HEAD/src/com/ciphertechsolutions/io/ui/icons/ionInvertedCropped.jpg
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/applicationLogic/IStoppable.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.applicationLogic;
2 |
3 | /**
4 | * A basic interface for processing that can be reliably halted if needed.
5 | */
6 | public interface IStoppable {
7 | /**
8 | * Stops any running threads that may have been spawned.
9 | */
10 | public void stop();
11 | }
12 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/SessionSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | /**
4 | * TODO: only used for images of optical disc CD/DVD/BD media.
5 | * Are we supporting this?
6 | *
7 | */
8 | public class SessionSection extends Section {
9 |
10 | protected SessionSection() {
11 | super("session");
12 | // TODO Auto-generated constructor stub
13 | }
14 |
15 | }
16 |
--------------------------------------------------------------------------------
/launch4j/IO.manifest:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/DiskSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | import com.ciphertechsolutions.io.device.Device;
4 |
5 | /**
6 | * TODO: Do not use?
7 | * Disk section is the same as the {@link com.ciphertechsolutions.io.ewf.VolumeSection}. Only found in FTK Imager 2.3.
8 | *
9 | */
10 | public class DiskSection extends VolumeSection {
11 |
12 | public DiskSection(long currentOffset, Device disk, byte[] guid) {
13 | super("disk", currentOffset, disk, guid);
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/digests/Md5Digest.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing.digests;
2 |
3 | import java.security.MessageDigest;
4 | import java.security.NoSuchAlgorithmException;
5 |
6 | /**
7 | * A class to compute the MD5 Digest of an input.
8 | */
9 | public class Md5Digest extends DigestBase {
10 |
11 | /**
12 | * Sole constructor.
13 | */
14 | public Md5Digest() {
15 | super("MD5Digest");
16 | }
17 |
18 | @Override
19 | protected MessageDigest getDigest() throws NoSuchAlgorithmException {
20 | return MessageDigest.getInstance("md5");
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/digests/SHA1Digest.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing.digests;
2 |
3 | import java.security.MessageDigest;
4 | import java.security.NoSuchAlgorithmException;
5 | /**
6 | * A class to compute the SHA1 Digest of an input.
7 | */
8 | public class SHA1Digest extends DigestBase {
9 |
10 | /**
11 | * Sole constructor.
12 | */
13 | public SHA1Digest() {
14 | super("SHA1Digest");
15 | }
16 |
17 | @Override
18 | protected MessageDigest getDigest() throws NoSuchAlgorithmException {
19 | return MessageDigest.getInstance("SHA1");
20 | }
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/.project:
--------------------------------------------------------------------------------
1 |
2 |
3 | IO
4 |
5 |
6 |
7 |
8 |
9 | org.eclipse.jdt.core.javabuilder
10 |
11 |
12 |
13 |
14 | org.eclipse.xtext.ui.shared.xtextBuilder
15 |
16 |
17 |
18 |
19 |
20 | org.eclipse.xtext.ui.shared.xtextNature
21 | org.eclipse.jdt.core.javanature
22 |
23 |
24 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/DataSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | import com.ciphertechsolutions.io.device.Device;
4 |
5 | /**
6 | * If the data section has data it should contain the same information
7 | * as the volume section. For multiple segment files it is not in the first segment.
8 | * Resides after {@link com.ciphertechsolutions.io.ewf.Table2Section} in a single segment file,
9 | * or at the start of the segment files, other than the first segment file.
10 | *
11 | *
12 | */
13 | public class DataSection extends VolumeSection {
14 |
15 | public DataSection(long currentOffset, Device disk, byte[] guid) {
16 | super("data", currentOffset, disk, guid);
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/logging/LogMessageType.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.logging;
2 |
3 | /**
4 | * An enum for the types of messages in the application.
5 | */
6 | public enum LogMessageType {
7 | /**
8 | * An error message.
9 | */
10 | ERROR,
11 | /**
12 | * A warning message.
13 | */
14 | WARNING,
15 | /**
16 | * An info message.
17 | */
18 | INFO,
19 | /**
20 | * A message for the user.
21 | */
22 | USER,
23 | /**
24 | * A message to help with debugging.
25 | */
26 | DEBUG,
27 | /**
28 | * A message to show in the report generated by ION during drive imaging.
29 | */
30 | REPORT,
31 | /**
32 | * A GPS message to write out to CSV
33 | */
34 | GPS;
35 |
36 | }
37 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/SectorsSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 |
4 | /**
5 | * Resides after the {@link com.ciphertechsolutions.io.ewf.VolumeSection} in the first segment file
6 | * or after the {@link com.ciphertechsolutions.io.ewf.DataSection} in other segment files.
7 | * Default size is 32k 64 sectors * 512 bytes. First chunk is located at offset 76
8 | * The most significant bit in the offset in the table section defines if a chunk is compressed or not.
9 | *
10 | * Stores chunk data.
11 | */
12 | public class SectorsSection extends Section {
13 |
14 | public SectorsSection(long currentOffset) {
15 | super(currentOffset, "sectors");
16 | }
17 |
18 | public void add(int length) {
19 | this.sectionSize += length;
20 | this.nextOffset += length;
21 | }
22 |
23 | }
24 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/DoneSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | /**
4 | * Last section in the last segment file.
5 | * The offset of the next section in the section start of the done section point to itself.
6 | * Resides after {@link com.ciphertechsolutions.io.ewf.DataSection} in single segment, or after {@link com.ciphertechsolutions.io.ewf.Table2Section} in multiple segment files.
7 | * Size in section start is 0 instead of 76 for EnCase.
8 | */
9 | public class DoneSection extends Section {
10 |
11 | /**
12 | * Create a new "Done" Section with the given starting offset.
13 | * @param currentOffset The offset, in bytes, from the start of the file that this section resides at.
14 | */
15 | public DoneSection(long currentOffset) {
16 | super(currentOffset, "done");
17 | this.nextOffset = currentOffset; // This seems to just point to itself.
18 | }
19 |
20 | }
21 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/Table2Section.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | /**
4 | * It has the same specification as the table section.
5 | * Every segment file contains its own table2 section. It resides directly after the {@link com.ciphertechsolutions.io.ewf.TableSection}.
6 | * For EnCase6 the table2 section contains a mirror copy of the {@link com.ciphertechsolutions.io.ewf.TableSection}.
7 | * Probably intended for recovery purposes.
8 | *
9 | */
10 | public class Table2Section extends TableSection {
11 |
12 | public Table2Section(long baseOffset) {
13 | super("table2", baseOffset);
14 | }
15 |
16 | public Table2Section(long currentOffset, TableSection toClone) {
17 | super("table2", currentOffset, toClone.getBaseOffset(), toClone.getSectionSize(), toClone.getArray(),
18 | toClone.getSecondaryAdler32(), toClone.getTableAdler32());
19 | }
20 |
21 | }
22 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/NextSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | /**
4 | * Next section has a section data type field of 'next'.
5 | * The next section is the last section within a segment other than the last segment file. The offset to the next section
6 | * in the start of the nextion section point to itself (the start of the next section). It should be the last section
7 | * in a segment file, other than the last segment file.
8 | * Resides after {@link com.ciphertechsolutions.io.ewf.DataSection} in single segment, or after {@link com.ciphertechsolutions.io.ewf.Table2Section} in multiple segment files.
9 | * Size in section start is 0 instead of 76 for EnCase.
10 | */
11 | public class NextSection extends Section {
12 |
13 | public NextSection(long currentOffset) {
14 | super(currentOffset, "next");
15 | this.nextOffset = currentOffset; // This seems to just point to itself.
16 | }
17 |
18 | }
19 |
--------------------------------------------------------------------------------
/.classpath:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/DataChunk.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | /**
4 | * Chunks of data for converting into sectors for ewf output.
5 | */
6 | public class DataChunk {
7 | /**
8 | * The raw data.
9 | */
10 | public final byte[] data;
11 | /**
12 | * The size of the data, in bytes.
13 | */
14 | public final int size;
15 | /**
16 | * The size of the uncompressed data, may be equal to size if {@link #compressed} is false.
17 | */
18 | public final int originalSize;
19 | /**
20 | * Whether this data is compressed.
21 | */
22 | public final boolean compressed;
23 |
24 | /**
25 | * Makes a {@link DataChunk} with the given data.
26 | * @param originalSize The uncompressed size of the data.
27 | * @param data The (possibly compressed) data.
28 | * @param compressed Whether or not the data given is compressed.
29 | */
30 | public DataChunk(int originalSize, byte[] data, boolean compressed) {
31 | this.data = data;
32 | this.originalSize = originalSize;
33 | this.size = this.data.length;
34 | this.compressed = compressed;
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/HashSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.nio.ByteOrder;
5 | import java.util.zip.Adler32;
6 |
7 | /**
8 | * Hash section is optional, it does not need to be present in an EWF file. It resides in the last segment file before the done section.
9 | *
10 | */
11 | public class HashSection extends Section {
12 | private static final int ADDITIONAL_SECTION_SIZE = 36;
13 | private final byte[] md5Hash;
14 | private final static byte[] padding = new byte[16]; // Possibly put stuff in here?
15 |
16 | public HashSection(long currentOffset, byte[] md5Hash) {
17 | super(currentOffset, "hash");
18 | this.md5Hash = md5Hash;
19 | this.sectionSize += ADDITIONAL_SECTION_SIZE;
20 | this.nextOffset += ADDITIONAL_SECTION_SIZE;
21 | }
22 |
23 | public byte[] getFullBytes() {
24 | ByteBuffer buffer = ByteBuffer.allocate(ADDITIONAL_SECTION_SIZE);
25 | buffer.order(ByteOrder.LITTLE_ENDIAN);
26 | buffer.put(md5Hash);
27 | buffer.put(padding);
28 | buffer.putInt(getSecondaryAdler32());
29 | return buffer.array();
30 | }
31 |
32 | private int getSecondaryAdler32() {
33 | Adler32 adlerCalc = new Adler32();
34 | adlerCalc.update(md5Hash);
35 | adlerCalc.update(padding);
36 | return (int) adlerCalc.getValue();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/applicationLogic/ApplicationState.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.applicationLogic;
2 |
3 | import com.ciphertechsolutions.io.applicationLogic.options.AdvancedOptions;
4 | import com.ciphertechsolutions.io.device.Device;
5 |
6 | /**
7 | * A class representing the current state of the application, namely the currently active
8 | * {@link AdvancedOptions options configuration} and the active {@link Device device}.
9 | */
10 | public class ApplicationState {
11 | private AdvancedOptions options;
12 | private Device selectedDevice;
13 |
14 | /**
15 | * The sole constructor.
16 | */
17 | public ApplicationState() {
18 | options = new AdvancedOptions();
19 | }
20 |
21 | /**
22 | * @return the options
23 | */
24 | public AdvancedOptions getOptions() {
25 | return options;
26 | }
27 |
28 | /**
29 | * @param options the options to set
30 | */
31 | public void setOptions(AdvancedOptions options) {
32 | this.options = options;
33 | }
34 |
35 | /**
36 | * @return the selectedDevice
37 | */
38 | public Device getSelectedDevice() {
39 | return selectedDevice;
40 | }
41 |
42 | /**
43 | * Sets the given {@link Device device} to be the active selected device.
44 | * @param device The {@link Device device} to select.
45 | */
46 | public void setSelectedDevice(Device device) {
47 | this.selectedDevice = device;
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/InfoScreenController.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ui;
2 |
3 | import javafx.event.ActionEvent;
4 | import javafx.fxml.FXML;
5 | import javafx.scene.control.Button;
6 | import javafx.scene.control.Label;
7 |
8 | /**
9 | * The controller for ION's info screen.
10 | */
11 | public class InfoScreenController extends BaseController {
12 |
13 | @FXML
14 | private Button closeButton;
15 | @FXML
16 | private Label versionLabel;
17 |
18 | private static final String ION_VERSION = "20170906.0"; // TODO: Make this update via build script?
19 |
20 | @Override
21 | protected void setTitle() {
22 | setTitle("IO - About");
23 | }
24 |
25 | @Override
26 | protected void performSetup() {
27 | versionLabel.setText("Version: " + ION_VERSION);
28 | }
29 |
30 | @FXML
31 | void onCloseClick(ActionEvent event) {
32 | changeScene(loadFXML(MainScreenController.class, MainScreenController.getFXMLLocation()).getScene());
33 | }
34 |
35 | @FXML
36 | void onMailToTeam(ActionEvent event) {
37 | hostServices.showDocument("mailto:io@ciphertechsolutions.com");
38 | }
39 |
40 | @FXML
41 | void onGetWebsite(ActionEvent event) {
42 | hostServices.showDocument("https://www.ciphertechsolutions.com/open-source/");
43 | }
44 |
45 | /**
46 | * Get the location of this controller's corresponding FXML file.
47 | *
48 | * @return the FXML file location.
49 | */
50 | public static String getFXMLLocation() {
51 | return "fxml/IONInfo.fxml";
52 | }
53 |
54 | }
55 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2017, Cipher Tech Solutions, Inc.
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | * Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | * Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | * Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/DigestSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.nio.ByteOrder;
5 | import java.util.zip.Adler32;
6 |
7 | /**
8 | * TODO: Replacement for {@link com.ciphertechsolutions.io.ewf.HashSection}?
9 | * Has section data type of 'hash' found in EnCase 6.12.
10 | * Additional digest section is 80 byte of size and consist of below fields.
11 | */
12 | public class DigestSection extends Section {
13 | private static final int ADDITIONAL_SECTION_SIZE = 80;
14 | private final byte[] md5Hash;
15 | private final byte[] sha1Hash;
16 | private final static byte[] padding = new byte[40];
17 |
18 | public DigestSection(long currentOffset, byte[] md5Hash, byte[] sha1Hash) {
19 | super(currentOffset, "digest");
20 | this.md5Hash = md5Hash;
21 | this.sha1Hash = sha1Hash;
22 | this.sectionSize += ADDITIONAL_SECTION_SIZE;
23 | this.nextOffset += ADDITIONAL_SECTION_SIZE;
24 | }
25 |
26 | public byte[] getFullBytes() {
27 | ByteBuffer buffer = ByteBuffer.allocate(ADDITIONAL_SECTION_SIZE);
28 | buffer.order(ByteOrder.LITTLE_ENDIAN);
29 | buffer.put(md5Hash);
30 | buffer.put(sha1Hash);
31 | buffer.put(padding);
32 | buffer.putInt(getSecondaryAdler32());
33 | return buffer.array();
34 | }
35 |
36 | private int getSecondaryAdler32() {
37 | Adler32 adlerCalc = new Adler32();
38 | adlerCalc.update(md5Hash);
39 | adlerCalc.update(sha1Hash);
40 | adlerCalc.update(padding);
41 | return (int) adlerCalc.getValue();
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # IO
2 | Simple Imaging. Tactical Triage. Zero Clicks.
3 |
4 | About IO:
5 | Imaging for Operations (IO) is a zero-click forensic imaging tool designed for use in high-stress environments. IO automatically enables a software write-block, detects changes to attached devices, and begins producing E01 images from connected target media without any user interaction. Furthermore, IO logs include critical device information that MEDEX (forensic) examiners require such as device type, model, name, size, geometry, MD5 and SHA1 hashes, the hardware serial number, the volume serial number for each partition, and the device VID/PID.
6 |
7 | Additionally, IO was designed from the ground up to enable concurrent data processing and analysis alongside imaging. Without impacting total imaging time, IO produces triage reports that include total file counts and extracted geo-location information from JPEGs. Developers can expand IO’s capabilities through the API.
8 |
9 | For instructions on using IO, please see [the user manual](https://github.com/ciphertechsolutions/IO/blob/master/docs/IO%20User%20Manual.pdf).
10 |
11 | For instructions on developing with IO, please see [the developer guide](https://github.com/ciphertechsolutions/IO/blob/master/docs/IO%20Developer%20Guide.pdf).
12 |
13 | Credit:
14 | IO was based upon a Cipher Tech internal summer intern project named KANT. KANT's authors included Noah (noahbkim), William (woodruffw), and AJ, all working under the oversight of Eric. The project was later rebranded, remastered, and brought to production by Cipher Tech's full-time engineering team. That team was primarily comprised of: Joe (ct-jzarrelli), Andrew (aziehl), and Mike.
15 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/IProcessor.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing;
2 |
3 | import com.ciphertechsolutions.io.applicationLogic.IStoppable;
4 |
5 | /**
6 | * An interface designed for interacting with {@link ProcessorManager} to process the contents of a media device.
7 | * Data will be given in discrete chunks via {@link #process(byte[])}.
8 | */
9 | public interface IProcessor extends IStoppable {
10 | /**
11 | * Process the given byte array. This method should return as quickly as possible as any delays in this method will delay all processors.
12 | * @param toProcess
13 | */
14 | public void process(byte[] toProcess);
15 |
16 | /**
17 | * This method will be called before {@link #process(byte[])} and can be used for any initialization needed.
18 | */
19 | public void initialize();
20 |
21 | /**
22 | * This method will be called at some point after the final call of {@link #process(byte[])} to indicate that
23 | * all data has been given. There is no need to have this method block until all processing is complete,
24 | * {@link #waitForExit()} should be used for that purpose instead.
25 | */
26 | public void finish();
27 |
28 | /**
29 | * This method will be called at some point after {@link #finish()} is called. Use this method to block
30 | * until all processing is complete.
31 | */
32 | public void waitForExit();
33 |
34 | /**
35 | * Called when processing should be terminated early. Any and all threads spawned should be terminated in an
36 | * orderly fashion when this method is called.
37 | */
38 | public void cancel();
39 | }
40 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/css/style.css:
--------------------------------------------------------------------------------
1 | .root
2 | {
3 | -fx-font-size: 11pt;
4 | -fx-font-family: "Univers";
5 | -fx-text-fill: white;
6 | -fx-font-weight: normal;
7 | -fx-background-color: #3f3f3f;
8 | }
9 |
10 | .progress-bar { -fx-box-border: goldenrod; }
11 | .orange-bar { -fx-accent: orange; }
12 |
13 | .listview-box
14 | {
15 | -fx-font-size: 11pt;
16 | -fx-box-border: goldenrod;
17 | -fx-background-color: #3f3f3f;
18 | }
19 |
20 | .choice-box
21 | {
22 | -fx-font-size: 10pt;
23 | -fx-text-fill: black;
24 | }
25 |
26 | /* Button Style */
27 | .button {
28 | -fx-background-color:
29 | #ebebeb; /*#707070, */
30 | /* linear-gradient(#fcfcfc, #f3f3f3), */
31 | /* linear-gradient(#f2f2f2 0%, #ebebeb 49%, #dddddd 50%, #cfcfcf 100%); */
32 | -fx-background-insets: 0,1,2;
33 | -fx-background-radius: 0; /* 3,2,1; */
34 | /* -fx-padding: 3 30 3 30; */
35 | -fx-text-fill: black;
36 | /* -fx-font-size: 14px; */
37 | }
38 | #devicesLoadingLabel
39 | {
40 | -fx-font-size: 18pt;
41 | -fx-font-family: "Univers";
42 | -fx-text-fill: white;
43 | -fx-font-weight: bold;
44 | -fx-text-alignment: center;
45 | -fx-background-color: #3f3f3f;
46 | }
47 |
48 | #advancedOptionsButton
49 | {
50 | -fx-min-height: 30px;
51 | -fx-min-width: 30px;
52 | -fx-max-height: 30px;
53 | -fx-max-width: 30px;
54 | -fx-graphic: url("/com/ciphertechsolutions/io/ui/icons/settings.png");
55 |
56 | }
57 |
58 | #infoButton
59 | {
60 | -fx-min-height: 30px;
61 | -fx-min-width: 30px;
62 | -fx-max-height: 30px;
63 | -fx-max-width:30px;
64 | -fx-graphic: url("/com/ciphertechsolutions/io/ui/icons/infoIcon.png");
65 |
66 |
67 | }
--------------------------------------------------------------------------------
/launch4j/IOexe.cfg.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | false
4 | gui
5 | ..\build\dist\IO.jar
6 | ..\build\dist\IO.exe
7 |
8 |
9 | .
10 | normal
11 | http://java.com/download
12 |
13 | false
14 | false
15 | .\IO.manifest
16 | ..\src\com\ciphertechsolutions\io\ui\icons\ion.ico
17 |
18 | CipherTechSolutions IO Mutex : {46dfbe27-5ed5-46cb-a90a-a82f53391e85}
19 |
20 |
21 |
22 |
23 | false
24 | false
25 | 1.8.0
26 |
27 | preferJdk
28 | 64
29 | 6000
30 |
31 |
32 | 0.9.0.0
33 | 0.9
34 | Open Source Disk Imaging and Triage
35 | 2017
36 | 0.9.0.0
37 | 0.9
38 | IO
39 | Cipher Tech Solutions, Inc.
40 | IO
41 | IO.exe
42 |
43 | ENGLISH_US
44 |
45 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/IMediaReader.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing;
2 |
3 | import java.io.IOException;
4 | import java.nio.ByteBuffer;
5 |
6 | /**
7 | * An interface specifying the basic operations needed to be a data source for ION's processing.
8 | */
9 | public interface IMediaReader extends AutoCloseable {
10 |
11 | /**
12 | * Read the next set of bytes from the data. -1 is returned to indicate the end of file has been reached.
13 | * @return The number of bytes read.
14 | * @throws IOException An error occurred while trying to read data.
15 | */
16 | public int read() throws IOException;
17 |
18 | /**
19 | * Get the ByteBuffer backing this {@link IMediaReader}.
20 | * @return The ByteBuffer.
21 | */
22 | public ByteBuffer getBuffer();
23 |
24 | /**
25 | * Get the bytes read by the last {@link #read} operation. This method must be thread-safe,
26 | * and the array returned must not be modified further by the {@link IMediaReader}. The caller of this
27 | * method may freely modify the array.
28 | * @return The bytes read.
29 | */
30 | public byte[] getBytes();
31 |
32 | /**
33 | * Get the bytes read by the last {@link #read} operation. The array returned by this method
34 | * may be modified further by the {@link IMediaReader}. Wherever possible, use of {@link #getBytes} is preferred.
35 | * @return The bytes read.
36 | */
37 | public byte[] getUnsafeBytes();
38 |
39 | /**
40 | * Gets the total number of bytes read by this {@link IMediaReader} across all calls to {@link #read()}.
41 | * @return The total bytes read.
42 | */
43 | public long getBytesRead();
44 |
45 | /**
46 | * Get the default read size of this {@link IMediaReader}. Not all calls to {@link #read()} will read this many bytes.
47 | * @return The default read size.
48 | */
49 | public int getReadSize();
50 | }
51 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/triage/StringCarver.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing.triage;
2 |
3 | import java.util.concurrent.TimeUnit;
4 |
5 | import com.ciphertechsolutions.io.logging.Logging;
6 |
7 | import javafx.util.Pair;
8 |
9 | /**
10 | * A processor that can find strings while imaging the drive.
11 | */
12 | public class StringCarver extends TriageProcessorBase {
13 |
14 | /**
15 | * The default minimum string length.
16 | */
17 | private final int DEFAULT_STRING_LENGTH = 10;
18 |
19 | private final int DEFAULT_RANDOM_THRESHOLD = 20;
20 |
21 | /**
22 | * Sole constructor.
23 | * @param readSize The default size of chunks that will be passed to this carver, used for memory management purposes.
24 | */
25 | public StringCarver(int readSize) {
26 | super("StringCarver", readSize);
27 | }
28 |
29 | @Override
30 | public void initialize() {
31 | startThreads();
32 | }
33 |
34 | @Override
35 | protected int getThreadCount() {
36 | return 1;
37 | }
38 |
39 | @Override
40 | protected void internalProcess() {
41 | try {
42 | while (isRunning && !Thread.currentThread().isInterrupted())
43 | {
44 | Pair toRead;
45 | toRead = byteQueue.poll(5, TimeUnit.SECONDS);
46 | if (toRead == null) {
47 | continue;
48 | }
49 | byte[] bytesToRead = toRead.getKey();
50 | if (bytesToRead.length == 0)
51 | {
52 | isRunning = false;
53 | return;
54 | }
55 | ByteUtils.printableSpansWithIndexes(bytesToRead, DEFAULT_STRING_LENGTH, false, DEFAULT_RANDOM_THRESHOLD);
56 | // TODO: What to do with this?
57 | }
58 | } catch (InterruptedException e) {
59 | Logging.log(e);
60 | return;
61 | }
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/device/Device.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.device;
2 |
3 | import java.nio.file.Path;
4 | import java.nio.file.Paths;
5 |
6 | /**
7 | * Represents storage devices.
8 | */
9 | public abstract class Device {
10 |
11 | /**
12 | * The simple name of a device.
13 | */
14 | protected String name;
15 |
16 | /// The path to read the device with.
17 | protected String path;
18 |
19 | /**
20 | * The size of the device in bytes.
21 | */
22 | protected long size;
23 |
24 | /**
25 | * Gives detailed information about this device as a printable string.
26 | * @return A printable string of device information.
27 | */
28 | public abstract String details();
29 |
30 | /**
31 | * Converts the device name to a format appropriate for outputting as a file name. Does not include file extension.
32 | * @return A valid file name containing device-relevant information.
33 | */
34 | public abstract String toFileNameString();
35 |
36 | /**
37 | *
38 | * @return The device path.
39 | */
40 | public Path getPath() {
41 | return Paths.get(path);
42 | }
43 |
44 | /**
45 | * @return the name
46 | */
47 | public String getName() {
48 | return name;
49 | }
50 |
51 | /**
52 | * @param name the name to set
53 | */
54 | public void setName(String name) {
55 | this.name = name;
56 | }
57 |
58 | /**
59 | * Get the device serial number
60 | * @return The serial number.
61 | */
62 | public abstract String getSerialNumber();
63 |
64 | protected static String trimPropertyName(String s) {
65 | if (s.contains(":")) {
66 | return s.substring(s.indexOf(":") + 1).trim();
67 | }
68 | return s;
69 | }
70 |
71 | /**
72 | * @return the size
73 | */
74 | public long getSize() {
75 | return size;
76 | }
77 |
78 | /**
79 | * @param size the size to set
80 | */
81 | public void setSize(long size) {
82 | this.size = size;
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/applicationLogic/options/CompressionTypesEnum.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.applicationLogic.options;
2 |
3 | /**
4 | * An enumeration of the compression levels ION offers. These are used internally by zlib.
5 | */
6 | public enum CompressionTypesEnum {
7 | /**
8 | * No compression
9 | */
10 | NONE("none", 0),
11 | /**
12 | * Fastest possible - zlib compression level 1.
13 | */
14 | FAST("fast", 1),
15 | /**
16 | * Balanced between speed and size - zlib compression level 4.
17 | */
18 | BALANCED("balanced", 4),
19 | /**
20 | * Maximal compression. zlib compression level 9.
21 | */
22 | BEST("best", 9);
23 |
24 |
25 | private final String displayName;
26 | private final int compressionLevel;
27 |
28 | CompressionTypesEnum(String value, int compressionLevel) {
29 | displayName = value;
30 | this.compressionLevel = compressionLevel;
31 | }
32 |
33 | /**
34 | * Get a user-friendly name for the compression level.
35 | * @return A user-friendly name.
36 | */
37 | public String getDisplayName() {
38 | return displayName;
39 | }
40 |
41 | /**
42 | * Get the zlib compression level, as an int.
43 | * @return The zlib compression level.
44 | */
45 | public int getLevel() {
46 | return compressionLevel;
47 | }
48 |
49 | /**
50 | * Gets ION's default compression level.
51 | * @return The default compression level.
52 | */
53 | public static CompressionTypesEnum getDefaultCompressionType() {
54 | return FAST;
55 | }
56 |
57 | /**
58 | * Get the zlib compression level by the user-friendly display name.
59 | * @param name The name to look up.
60 | * @return The compression level as an int.
61 | */
62 | public static int getLevelByName(String name) {
63 | for (CompressionTypesEnum value : CompressionTypesEnum.values()) {
64 | if (name.equals(value.getDisplayName())) {
65 | return value.getLevel();
66 | }
67 | }
68 | return 1;
69 | }
70 | }
71 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/ProcessorBase.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing;
2 |
3 | import com.ciphertechsolutions.io.logging.Logging;
4 |
5 | /**
6 | * Provides some of the basic functionality required to implement {@link IProcessor}. This implementation
7 | * assumes that the process will be spawning and managing child threads.
8 | */
9 | public abstract class ProcessorBase implements IProcessor {
10 | protected boolean isRunning = true;
11 | private final Thread[] threads;
12 | private final String threadName;
13 |
14 | protected ProcessorBase(String threadName) {
15 | threads = new Thread[getThreadCount()];
16 | this.threadName = threadName;
17 | }
18 |
19 | protected abstract int getThreadCount();
20 |
21 | protected abstract void internalProcess();
22 |
23 | protected void startThreads() {
24 | for (int i =0; i< getThreadCount(); i++)
25 | {
26 | threads[i] = new Thread(() -> {internalProcess();}, threadName + (i > 0 ? i : ""));
27 | threads[i].start();
28 | }
29 | }
30 |
31 | protected void waitForThreads() {
32 | for (Thread thread : threads)
33 | {
34 | try {
35 | thread.join();
36 | }
37 | catch (InterruptedException e) {
38 | Logging.log(e);
39 | return;
40 | }
41 | }
42 | }
43 |
44 | @Override
45 | public void waitForExit() {
46 | waitForThreads();
47 | }
48 |
49 | @Override
50 | public void cancel() {
51 | cancel(true);
52 | }
53 |
54 | /**
55 | * Interrupts all threads that have been spawned.
56 | * @param wait A boolean to determine if this method waits for the threads to halt before returning.
57 | */
58 | public void cancel(boolean wait) {
59 | isRunning = false;
60 | for (Thread thread : threads)
61 | {
62 | thread.interrupt();
63 | }
64 | if (wait) {
65 | waitForThreads();
66 | }
67 | }
68 |
69 |
70 | @Override
71 | public void stop() {
72 | cancel(false);
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/applicationLogic/Utils.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.applicationLogic;
2 |
3 | import java.time.Duration;
4 |
5 | /**
6 | * A class to contain some generic useful helper functions for ION.
7 | */
8 | public class Utils {
9 |
10 | /**
11 | * Converts the given duration to a string in the format of "x days hh:mm:ss", "hh:mm:ss", or "x seconds",
12 | * depending on the amount of time in the duration.
13 | * @param duration The duration to convert to a string.
14 | * @return The duration as a string.
15 | */
16 | public static String getPrettyTime(Duration duration) {
17 | StringBuilder timeString = new StringBuilder();
18 | long seconds = duration.getSeconds();
19 | addDays(duration, timeString);
20 | addHours(duration, timeString, seconds);
21 | addMinutesAndSeconds(duration, timeString, seconds);
22 | return timeString.toString();
23 | }
24 |
25 | private static void addMinutesAndSeconds(Duration duration, StringBuilder timeString, long seconds) {
26 | if (duration.toMinutes() >= 1) {
27 | int remainingMinutes = (int) ((seconds % 3600) / 60);
28 | padIfNeeded(timeString, remainingMinutes);
29 | timeString.append(":");
30 | int remainingSeconds = (int) (seconds % 60);
31 | padIfNeeded(timeString, remainingSeconds);
32 | }
33 | else {
34 | timeString.append(seconds % 60);
35 | timeString.append(" seconds.");
36 | }
37 | }
38 |
39 | private static void addHours(Duration duration, StringBuilder timeString, long seconds) {
40 | if (duration.toHours() >= 1) {
41 | timeString.append((seconds % 86400)/ 3600);
42 | timeString.append(":");
43 | }
44 | }
45 |
46 | private static void addDays(Duration duration, StringBuilder timeString) {
47 | if (duration.toDays() >= 1) {
48 | timeString.append(duration.toDays());
49 | timeString.append(" days ");
50 | }
51 | }
52 |
53 | private static void padIfNeeded(StringBuilder timeString, int remaining) {
54 | if (remaining < 10) {
55 | timeString.append("0");
56 | }
57 | timeString.append(remaining);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/fxml/IONDevice.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
18 |
22 |
23 |
24 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/CompressionTask.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing;
2 |
3 | import java.io.UnsupportedEncodingException;
4 | import java.util.Arrays;
5 | import java.util.concurrent.Callable;
6 | import java.util.zip.DataFormatException;
7 | import java.util.zip.Deflater;
8 | import java.util.zip.Inflater;
9 |
10 | import javax.xml.bind.DatatypeConverter;
11 |
12 | import com.ciphertechsolutions.io.ewf.DataChunk;
13 | import com.ciphertechsolutions.io.processing.triage.ByteUtils;
14 |
15 | class CompressionTask implements Callable {
16 |
17 | private final byte[] input;
18 | private final int compressionLevel;
19 |
20 | CompressionTask(byte[] toCompress, int compressionLevel) {
21 | input = toCompress;
22 | this.compressionLevel = compressionLevel;
23 | }
24 |
25 | @Override
26 | public DataChunk call() throws Exception {
27 | Deflater deflater = new Deflater(compressionLevel);
28 | deflater.setInput(input);
29 | deflater.finish();
30 | byte[] output = new byte[input.length + 4];
31 | int compressedSize = deflater.deflate(output);
32 | //Unlikely, but possible.
33 | if (compressedSize >= input.length) {
34 | System.arraycopy(input, 0, output, 0, input.length);
35 | System.arraycopy(ByteUtils.intToBytes(deflater.getAdler()), 0, output, input.length, 4);
36 | return new DataChunk(input.length, output, false);
37 | }
38 | return new DataChunk(input.length, Arrays.copyOf(output, compressedSize), true);
39 | }
40 |
41 | /**
42 | * Testing method for decompression of ewf file hex bytes
43 | * @param ewfHexStr any zlib compressed hex
44 | * @return decompressed string
45 | */
46 | protected static String decompress(String ewfHexStr) {
47 | Inflater inflater = new Inflater();
48 | byte[] input = DatatypeConverter.parseHexBinary(ewfHexStr);
49 | inflater.setInput(input, 0, input.length);
50 | String outputString = "empty";
51 |
52 | byte[] result = new byte[input.length];
53 | int resultLength;
54 | try {
55 | resultLength = inflater.inflate(result);
56 | outputString = new String(result, 0, resultLength, "UTF-8");
57 | } catch (DataFormatException | UnsupportedEncodingException e) {
58 | e.printStackTrace();
59 | }
60 | inflater.end();
61 |
62 | return outputString;
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/fxml/IONImaging.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
23 |
27 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/triage/TriageProcessorBase.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing.triage;
2 |
3 | import java.util.concurrent.BlockingQueue;
4 | import java.util.concurrent.LinkedBlockingQueue;
5 |
6 | import com.ciphertechsolutions.io.logging.LogMessageType;
7 | import com.ciphertechsolutions.io.logging.Logging;
8 | import com.ciphertechsolutions.io.processing.ProcessorBase;
9 |
10 | import javafx.util.Pair;
11 |
12 | /**
13 | * Provides basic processing functionality to make implementing triaging classes easier.
14 | *
15 | */
16 | public abstract class TriageProcessorBase extends ProcessorBase {
17 | /**
18 | * Set to {@link Integer#MAX_VALUE}.
19 | */
20 | private final int MAX_BACKLOG_SIZE_IN_BYTES = Integer.MAX_VALUE;
21 | protected final BlockingQueue> byteQueue;
22 | protected long currentLength = 0;
23 | private final String friendlyName;
24 | private boolean hasWarned = false;
25 |
26 | /**
27 | *
28 | * @param threadName
29 | * @param readSize
30 | */
31 | protected TriageProcessorBase(String threadName, int readSize) {
32 | super(threadName);
33 | friendlyName = threadName;
34 | byteQueue = new LinkedBlockingQueue<>(MAX_BACKLOG_SIZE_IN_BYTES/readSize);
35 | }
36 |
37 | /**
38 | * Attempts to add the given byte array to a queue with a max capacity of {@link #MAX_BACKLOG_SIZE_IN_BYTES} bytes.
39 | */
40 | @Override
41 | public void process(byte[] toProcess) {
42 | if (!byteQueue.offer(new Pair<>(toProcess, currentLength))) {
43 | if (!hasWarned) {
44 | hasWarned = true;
45 | Logging.log("Read speed is outpacing processor speed, " + friendlyName + " will not be able to process every byte.", LogMessageType.USER);
46 | }
47 | Logging.log("Read speed is outpacing processor speed, " + friendlyName + " was unable to process bytes "
48 | + currentLength +"through " + (currentLength + toProcess.length) +".", LogMessageType.DEBUG);
49 | // TODO: Log ranges and report at end rather than during? Unsure.
50 | }
51 | currentLength += toProcess.length;
52 | }
53 |
54 | /**
55 | * Adds an empty byte array to signal the end of input.
56 | */
57 | @Override
58 | public void finish() {
59 | byteQueue.add(new Pair<>(new byte[0], currentLength));
60 | }
61 |
62 | @Override
63 | public void waitForExit() {
64 | waitForThreads();
65 | }
66 |
67 | }
68 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/GUILauncher.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ui;
2 |
3 | import java.awt.SplashScreen;
4 |
5 | import com.ciphertechsolutions.io.applicationLogic.IProcessController;
6 | import com.ciphertechsolutions.io.applicationLogic.ProcessController;
7 | import com.ciphertechsolutions.io.usb.UsbWriteBlock;
8 |
9 | import javafx.application.Application;
10 | import javafx.scene.image.Image;
11 | import javafx.stage.Stage;
12 |
13 | /**
14 | * The primary class of the ION GUI, launches the application.
15 | */
16 | public class GUILauncher extends Application {
17 | private IProcessController controller;
18 |
19 | @Override
20 | public void start(Stage primaryStage) throws Exception {
21 | try {
22 | BaseController.setStage(primaryStage);
23 | BaseController.setHostServices(getHostServices());
24 | primaryStage.getIcons().add(new Image("/com/ciphertechsolutions/io/ui/icons/ion.png"));
25 |
26 | SplashScreen splash = SplashScreen.getSplashScreen();
27 | MainScreenController root = BaseController.loadFXML(MainScreenController.getFXMLLocation(),
28 | MainScreenController.class);
29 | controller = new ProcessController();
30 | root.setWorkflowController(controller);
31 | root.performSetup();
32 | primaryStage.setScene(root.getScene());
33 | if (splash != null) {
34 | splash.close();
35 | }
36 | primaryStage.show();
37 | BaseController.changeScene(root.getScene());
38 |
39 | }
40 | catch (Exception e) {
41 | BaseController.displayErrorPopup(e, "Failed to initialize! Details: ");
42 | System.exit(1);
43 | }
44 | }
45 |
46 | @Override
47 | public void stop() {
48 | if (controller != null) {
49 | controller.stop();
50 | }
51 | if (UsbWriteBlock.getInitialState()) {
52 | if (BaseController.displayYesNoPopup("The system-wide USB write block was enabled when ION began, do you want to turn it off? "
53 | + "WARNING: CLICKING YES WILL DISABLE THE SOFTWARE WRITE-BLOCK FOR ALL CURRENTLY RUNNING ION INSTANCES.")) {
54 | UsbWriteBlock.disable();
55 | }
56 | }
57 | else {
58 | UsbWriteBlock.disable();
59 | }
60 | }
61 |
62 | /**
63 | * Launches the application.
64 | * @param args No args taken.
65 | */
66 | public static void main(String[] args) {
67 | launch(args);
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/SelectDeviceController.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ui;
2 |
3 | import com.ciphertechsolutions.io.device.Device;
4 | import com.ciphertechsolutions.io.device.Disk;
5 |
6 | import javafx.application.Platform;
7 | import javafx.collections.FXCollections;
8 | import javafx.collections.ObservableList;
9 | import javafx.event.ActionEvent;
10 | import javafx.fxml.FXML;
11 | import javafx.scene.control.Button;
12 | import javafx.scene.control.Label;
13 | import javafx.scene.control.ListView;
14 | import javafx.scene.control.cell.TextFieldListCell;
15 |
16 | /**
17 | * The controller for the device selection screen.
18 | */
19 | public class SelectDeviceController extends BaseController {
20 |
21 | @FXML
22 | private Button selectButton;
23 | @FXML
24 | private Button cancelButton;
25 | @FXML
26 | private Label devicesLoadingLabel;
27 | @FXML
28 | private ListView diskListView;
29 |
30 | /**
31 | * Get the location of this controller's corresponding FXML file.
32 | * @return the FXML file locatiion.
33 | */
34 | public static String getFXMLLocation() {
35 | return "fxml/IONDevice.fxml";
36 | }
37 |
38 | @Override
39 | protected void performSetup() {
40 | setTitle();
41 | // TODO: Better loading.
42 | Platform.runLater(new Runnable() {
43 | @Override
44 | public void run() {
45 | devicesLoadingLabel.setVisible(true);
46 | diskListView.setCellFactory(lv -> {
47 | TextFieldListCell cell = new TextFieldListCell<>();
48 | cell.setConverter(workflowController.getStringConverterForDisks());
49 | return cell;
50 | });
51 | ObservableList disks = FXCollections.observableArrayList();
52 | disks.addAll(workflowController.getAvailableDisks());
53 | devicesLoadingLabel.setVisible(false);
54 | diskListView.setItems(disks);
55 | }});
56 | }
57 |
58 | @FXML
59 | void onSelectDevice(ActionEvent event) {
60 | Device device = diskListView.getSelectionModel().getSelectedItem();
61 | if (device != null) {
62 | workflowController.selectDevice(device);
63 | tryBeginImaging();
64 | }
65 | else {
66 | displayInformationPopup("No device selected.");
67 | }
68 | }
69 |
70 | @FXML
71 | void onCancel(ActionEvent event) {
72 | changeScene(loadFXML(MainScreenController.class, MainScreenController.getFXMLLocation()).getScene());
73 | }
74 |
75 | @Override
76 | protected void setTitle() {
77 | setTitle("IO - Select Device");
78 |
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/triage/ByteUtils.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing.triage;
2 |
3 | import java.nio.charset.StandardCharsets;
4 | import java.util.Arrays;
5 | import java.util.HashMap;
6 | import java.util.Map;
7 |
8 | /**
9 | * A collection of byte manipulation utility functions.
10 | */
11 | public class ByteUtils {
12 |
13 | /**
14 | * Converts a long to a little endian byte array.
15 | * @param l The long to convert
16 | * @return A little endian array.
17 | */
18 | public static byte[] longToBytes(long l) {
19 | byte[] result = new byte[8];
20 | for (int i = 0; i <= 7; i++) {
21 | result[i] = (byte)(l & 0xFF);
22 | l >>= 8;
23 | }
24 | return result;
25 | }
26 |
27 | /**
28 | * Converts an int to a little endian byte array.
29 | * @param _int The int to convert.
30 | * @return A little endian array.
31 | */
32 | public static byte[] intToBytes(int _int) {
33 | byte[] result = new byte[4];
34 | for (int i = 0; i <= 3; i++) {
35 | result[i] = (byte)(_int & 0xFF);
36 | _int >>= 8;
37 | }
38 | return result;
39 | }
40 |
41 | public static Map printableSpansWithIndexes(byte[] bytes, int minLength, boolean filterRandom, int randomThreshold) {
42 |
43 | Map spanPairs = new HashMap<>();
44 | int[] table = new int[bytes.length];
45 | int tableSize = 0;
46 | int strStart, strSize;
47 | byte[] byteSpan;
48 | String span;
49 |
50 |
51 | for (int i = 0; i < bytes.length; i++)
52 | {
53 | if (!isPrintable(bytes[i]))
54 | {
55 | table[tableSize++] = i;
56 | }
57 | }
58 |
59 | for (int j = 1; j < tableSize; j++)
60 | {
61 | if (table[j] == 0)
62 | {
63 | continue;
64 | }
65 |
66 | strStart = table[j - 1] + 1;
67 | strSize = table[j] - strStart;
68 |
69 | if (strSize < minLength)
70 | {
71 | continue;
72 | }
73 |
74 |
75 | byteSpan = Arrays.copyOfRange(bytes, strStart, strStart + strSize);
76 | span = new String(byteSpan, StandardCharsets.US_ASCII);
77 | spanPairs.put(strStart, span);
78 | }
79 |
80 | if (filterRandom)
81 | {
82 | //TODO: Bother with this?
83 | return spanPairs;
84 | }
85 | else
86 | {
87 | return spanPairs;
88 | }
89 | }
90 |
91 | private static boolean isPrintable(byte b)
92 | {
93 | return (b >= 0x20 && b < 0x7F) || b == 0x0a || b == 0x0d || b == 0x09;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/usb/UsbWriteBlock.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.usb;
2 |
3 | import com.ciphertechsolutions.io.logging.LogMessageType;
4 | import com.ciphertechsolutions.io.logging.Logging;
5 | import com.sun.jna.platform.win32.Advapi32Util;
6 | import com.sun.jna.platform.win32.WinReg;
7 |
8 | /**
9 | * A class for managing the USB Write Block registry key setting.
10 | * Specifically, the key is: HKLM\SYSTEM\CurrentControlSet\Control\StorageDevicePolicies\WriteProtect
11 | * Note: Write block status for a drive is determined at the time of drive insertion, not at the time of read/write.
12 | */
13 | public class UsbWriteBlock {
14 |
15 | private static final String WRITE_BLOCK_KEY = "SYSTEM\\CurrentControlSet\\Control\\StorageDevicePolicies";
16 |
17 | private static boolean initial_status = getWriteBlockStatus();
18 |
19 | ///The associated registry key.
20 | private static void writeToKey(int value) {
21 | try {
22 | if (!Advapi32Util.registryKeyExists(WinReg.HKEY_LOCAL_MACHINE, WRITE_BLOCK_KEY)) {
23 | Advapi32Util.registryCreateKey(WinReg.HKEY_LOCAL_MACHINE, WRITE_BLOCK_KEY);
24 | }
25 | Advapi32Util.registrySetIntValue(WinReg.HKEY_LOCAL_MACHINE, WRITE_BLOCK_KEY, "WriteProtect", value);
26 | }
27 | catch (IllegalArgumentException e) {
28 | Logging.log("Failed to " + (value == 0 ? "disable" : "enable") + " USB write block", LogMessageType.WARNING);
29 | Logging.log(e);
30 | }
31 |
32 | }
33 |
34 | /**
35 | * Gets the current setting of the write block key.
36 | * @return True if write block is turned on in the registry, false otherwise.
37 | */
38 | public static boolean getWriteBlockStatus() {
39 | if (!Advapi32Util.registryKeyExists(WinReg.HKEY_LOCAL_MACHINE, WRITE_BLOCK_KEY)) {
40 | return false;
41 | }
42 | return Advapi32Util.registryGetIntValue(WinReg.HKEY_LOCAL_MACHINE, WRITE_BLOCK_KEY, "WriteProtect") == 1;
43 |
44 | }
45 |
46 | /**
47 | * Get the state of USB write block as of when ION launched.
48 | * @return true if write block was enabled, false if it was not.
49 | */
50 | public static boolean getInitialState() {
51 | return initial_status;
52 | }
53 |
54 | /**
55 | * Sets the write block key back to what it was when ION launched.
56 | */
57 | public static void resetWriteBlockStatus() {
58 | if (!Advapi32Util.registryKeyExists(WinReg.HKEY_LOCAL_MACHINE, WRITE_BLOCK_KEY)) {
59 | return;
60 | }
61 | Advapi32Util.registrySetIntValue(WinReg.HKEY_LOCAL_MACHINE, WRITE_BLOCK_KEY, "WriteProtect", initial_status ? 1 : 0);
62 |
63 | }
64 |
65 | /**
66 | * Enable the system write block on newly connected devices.
67 | */
68 | public static void enable()
69 | {
70 | if (getWriteBlockStatus())
71 | {
72 | return;
73 | }
74 | writeToKey(1);
75 | }
76 |
77 | /**
78 | * Disable the system write block on any newly connected devices.
79 | */
80 | public static void disable()
81 | {
82 | if (!getWriteBlockStatus())
83 | {
84 | return;
85 | }
86 | writeToKey(0);
87 | }
88 |
89 |
90 | }
91 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/TableSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | import java.util.ArrayList;
4 | import java.util.Collections;
5 | import java.util.List;
6 | import java.util.zip.Adler32;
7 |
8 | import com.ciphertechsolutions.io.processing.triage.ByteUtils;
9 |
10 | /**
11 | * Every segment file contains its own table section. It resides after the {@link com.ciphertechsolutions.io.ewf.SectorsSection}.
12 | *
13 | */
14 | public class TableSection extends Section {
15 |
16 | private static final int MAX_ENTRIES = 16375; //TODO: Maybe 65534?
17 | // If needed, store as byte arrays instead to convert only once.
18 | private final List offsetArray;
19 | private final Adler32 offsetChecksumCalc = new Adler32();
20 | private int secondaryAdler32 = 0;
21 | private int tableAdler32 = 0;
22 |
23 | public TableSection(long baseOffset) {
24 | this("table", baseOffset);
25 | }
26 |
27 | protected TableSection(String sectionName, long baseOffset) {
28 | super(sectionName);
29 | this.baseOffset = baseOffset;
30 | sectionSize += 28;
31 | nextOffset += 28;
32 | offsetArray = Collections.synchronizedList(new ArrayList<>()); //TODO: Do we need to bother with something threadsafe here?
33 | }
34 |
35 | protected TableSection(String sectionName, long currentOffset, long baseOffset, long sectionSize,
36 | List offsetArray, int secondaryAdler32, int tableAdler32) {
37 | super(currentOffset, sectionName, currentOffset + sectionSize, sectionSize);
38 | this.baseOffset = baseOffset;
39 | this.offsetArray = offsetArray;
40 | this.secondaryAdler32 = secondaryAdler32;
41 | this.tableAdler32 = tableAdler32;
42 | }
43 | int tableEntries;
44 |
45 | // 4 bytes of padding.
46 | static final byte[] padding = {0x00, 0x00, 0x00, 0x00};
47 |
48 | private final long baseOffset;
49 |
50 | static final byte[] morePadding = padding;
51 |
52 | public boolean isFull() {
53 | return offsetArray.size() == MAX_ENTRIES;
54 | }
55 |
56 | public long getBaseOffset() {
57 | return baseOffset;
58 | }
59 |
60 | public List getArray() {
61 | return offsetArray;
62 | }
63 |
64 | public void add(long offset, boolean compressed) {
65 | //TODO: Do we need the actual chunk data to calculate a checksum?
66 | int relativeOffset = compressed ? (1 << 31) | (int) (offset - baseOffset) : (int) (offset - baseOffset);
67 | offsetArray.add(relativeOffset);
68 | offsetChecksumCalc.update(ByteUtils.intToBytes(relativeOffset));
69 | tableEntries++;
70 | sectionSize += 4;
71 | nextOffset += 4;
72 | }
73 |
74 | public int getSecondaryAdler32(){
75 | if (secondaryAdler32 == 0) {
76 | Adler32 adlerCalc = new Adler32();
77 | adlerCalc.update(ByteUtils.intToBytes(tableEntries));
78 | adlerCalc.update(padding);
79 | adlerCalc.update(ByteUtils.longToBytes(baseOffset));
80 | adlerCalc.update(morePadding);
81 | secondaryAdler32 = (int) adlerCalc.getValue();
82 | }
83 | return secondaryAdler32;
84 | }
85 |
86 | public int getTableAdler32(){
87 | if (tableAdler32 == 0) {
88 | tableAdler32 = (int) offsetChecksumCalc.getValue();
89 | }
90 | return tableAdler32;
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/Error2Section.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.nio.ByteOrder;
5 | import java.util.ArrayList;
6 | import java.util.zip.Adler32;
7 |
8 | /**
9 | * TODO: Optional? Since ION will specify errors as well.
10 | * Found in EnCase6 format.
11 | * It is only added to the last segment file when errors were encountered while reading the input.
12 | * It contains the sectors that have read errors. The sector where a read error
13 | * occurred are filled with zero's during EnCase acquiry.
14 | *
15 | */
16 | public class Error2Section extends Section {
17 |
18 | public Error2Section(long currentOffset) {
19 | super(currentOffset, "error2");
20 | numberOfEntires = 0;
21 | error2SectorEntries = new ArrayList<>();
22 | }
23 |
24 | private int numberOfEntires;
25 | private static final byte[] padding = new byte[512];
26 | private Adler32 checksumPrevious; //Checksum of previous data
27 |
28 | private final ArrayList error2SectorEntries;
29 |
30 | private Adler32 checksumSectors; //Checksum of remaining sectors data
31 |
32 | private static final int minimumError2SectionSize = padding.length + 12; // int + 2 alder;
33 |
34 | public void addEntry(int errorSector, int numSectors) {
35 | Error2SectorEntry errorEntry = new Error2SectorEntry(errorSector, numSectors);
36 | error2SectorEntries.add(errorEntry);
37 | numberOfEntires++;
38 | }
39 |
40 | public byte[] getFullBytes() {
41 | byte[] allEntries = getAllEntriesBytes();
42 | ByteBuffer buffer = ByteBuffer.allocate(allEntries.length + minimumError2SectionSize);
43 | buffer.order(ByteOrder.LITTLE_ENDIAN);
44 | buffer.putInt(numberOfEntires);
45 | buffer.put(padding);
46 | buffer.putInt(getPreviousAdler32());
47 | buffer.put(allEntries);
48 | buffer.putInt(getSectorsAdler32());
49 | return buffer.array();
50 | }
51 |
52 | private byte[] getAllEntriesBytes() {
53 | ByteBuffer buffer = ByteBuffer.allocate(Error2SectorEntry.entrySize * error2SectorEntries.size());
54 | for(Error2SectorEntry entry : error2SectorEntries) {
55 | buffer.put(entry.getFullBytes());
56 | }
57 | return buffer.array();
58 | }
59 |
60 |
61 |
62 | private int getPreviousAdler32() {
63 | Adler32 adlerCalc = new Adler32();
64 | adlerCalc.update(numberOfEntires);
65 | adlerCalc.update(padding);
66 | checksumPrevious = adlerCalc;
67 | return (int) adlerCalc.getValue();
68 | }
69 |
70 | private int getSectorsAdler32() {
71 | Adler32 adlerCalc = new Adler32();
72 | adlerCalc.update(getAllEntriesBytes());
73 | checksumSectors = adlerCalc;
74 | return (int) adlerCalc.getValue();
75 | }
76 |
77 | private class Error2SectorEntry {
78 | int firstErrorSector;
79 | int numberOfErrorSectors;
80 | static final int entrySize = 8;
81 | private Error2SectorEntry(int firstError, int numberofSectors) {
82 | firstErrorSector = firstError;
83 | numberOfErrorSectors = numberofSectors;
84 | }
85 |
86 | public byte[] getFullBytes() {
87 | ByteBuffer buffer = ByteBuffer.allocate(entrySize);
88 | buffer.order(ByteOrder.LITTLE_ENDIAN);
89 | buffer.putInt(firstErrorSector);
90 | buffer.putInt(numberOfErrorSectors);
91 | return buffer.array();
92 | }
93 | }
94 |
95 |
96 |
97 | }
98 |
99 |
100 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/VolumeSectionManager.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | import java.io.IOException;
4 | import java.io.RandomAccessFile;
5 | import java.nio.ByteBuffer;
6 | import java.nio.ByteOrder;
7 | import java.util.ArrayList;
8 | import java.util.Collections;
9 | import java.util.List;
10 | import java.util.Map;
11 | import java.util.UUID;
12 | import java.util.concurrent.ConcurrentHashMap;
13 |
14 | import com.ciphertechsolutions.io.device.Device;
15 |
16 | public class VolumeSectionManager {
17 | private final Map> volumeSections;
18 | private final byte[] EMPTY_VOLUME_SECTION = new byte[VolumeSection.ADDITIONAL_SECTION_SIZE + Section.SECTION_HEADER_SIZE];
19 | private final Device imagedDisk;
20 | private int chunks;
21 | private long sectors;
22 | private byte[] fileSetGUID = null;
23 |
24 | public VolumeSectionManager(Device imagedDisk) {
25 | this.volumeSections = new ConcurrentHashMap<>();
26 | this.imagedDisk = imagedDisk;
27 | }
28 |
29 | public void put(RandomAccessFile key, VolumeSection value) {
30 | if (volumeSections.containsKey(key)) {
31 | volumeSections.get(key).add(value);
32 | }
33 | else {
34 | List sections = Collections.synchronizedList(new ArrayList<>());
35 | sections.add(value);
36 | volumeSections.put(key, sections);
37 | }
38 | }
39 |
40 | public void writePreliminaryVolumeSection(RandomAccessFile file) throws IOException {
41 | put(file, new VolumeSection(file.getFilePointer(), imagedDisk, getFileSetGuid()));
42 | file.write(EMPTY_VOLUME_SECTION);
43 | }
44 |
45 | public void writePreliminaryDataSection(RandomAccessFile file) throws IOException {
46 | put(file, new DataSection(file.getFilePointer(), imagedDisk, getFileSetGuid()));
47 | file.write(EMPTY_VOLUME_SECTION);
48 | }
49 |
50 | public void writePreliminaryDiskSection(RandomAccessFile file) throws IOException {
51 | put(file, new DiskSection(file.getFilePointer(), imagedDisk, getFileSetGuid()));
52 | file.write(EMPTY_VOLUME_SECTION);
53 | }
54 |
55 | public void setVolumeSize(int chunks, long sectors) {
56 | this.chunks = chunks;
57 | this.sectors = sectors;
58 | }
59 |
60 | public void writeProperVolumeSections(boolean closeFiles) throws IOException{
61 | for (RandomAccessFile file : volumeSections.keySet()) {
62 | for (VolumeSection toWrite : volumeSections.get(file)) {
63 | toWrite.correctSizeInformation(chunks, sectors);
64 | file.seek(toWrite.getCurrentOffset());
65 | file.write(toWrite.getFullHeader());
66 | file.write(toWrite.getAdditionalBytes());
67 | }
68 | if (closeFiles) {
69 | file.close();
70 | }
71 | }
72 | }
73 |
74 | private byte[] getFileSetGuid() {
75 | if (fileSetGUID == null) {
76 | UUID acquiryID = UUID.randomUUID();
77 | ByteBuffer guidBuffer = ByteBuffer.allocate(16);
78 | guidBuffer.order(ByteOrder.LITTLE_ENDIAN);
79 | guidBuffer.putLong(acquiryID.getLeastSignificantBits());
80 | guidBuffer.putLong(acquiryID.getMostSignificantBits());
81 | fileSetGUID = guidBuffer.array();
82 | }
83 | return fileSetGUID;
84 | }
85 |
86 | }
87 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/digests/DigestBase.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing.digests;
2 |
3 | import java.security.MessageDigest;
4 | import java.security.NoSuchAlgorithmException;
5 | import java.util.concurrent.BlockingQueue;
6 | import java.util.concurrent.Callable;
7 | import java.util.concurrent.FutureTask;
8 | import java.util.concurrent.LinkedBlockingQueue;
9 | import java.util.concurrent.TimeUnit;
10 |
11 | import com.ciphertechsolutions.io.logging.Logging;
12 | import com.ciphertechsolutions.io.processing.ProcessorBase;
13 |
14 | /**
15 | * A class for computing digest hashes during imaging.
16 | */
17 | public abstract class DigestBase extends ProcessorBase implements Callable {
18 |
19 | protected DigestBase(String threadName) {
20 | super(threadName);
21 | }
22 |
23 | private MessageDigest digest;
24 | private final Object lock = new Object();
25 | private volatile byte[] md5 = null;
26 | private final BlockingQueue byteQueue = new LinkedBlockingQueue<>();
27 |
28 | @Override
29 | public void initialize() {
30 | try {
31 | digest = getDigest();
32 | startThreads();
33 | }
34 | catch (NoSuchAlgorithmException e) {
35 | // What to do here?
36 | Logging.log(e);
37 | }
38 | }
39 |
40 | protected abstract MessageDigest getDigest() throws NoSuchAlgorithmException;
41 |
42 | /**
43 | * Get the hash resulting from this digest. This method will return immediately, but accessing the contents of the
44 | * {@link FutureTask} will block until the digest is complete.
45 | * @return A {@link FutureTask} that will contain the hash upon completion.
46 | */
47 | public FutureTask getDigestResult() {
48 | return new FutureTask<>(this);
49 | }
50 |
51 | @Override
52 | public byte[] call() {
53 | synchronized(lock) {
54 | while (md5 == null) {
55 | try {
56 | lock.wait(5000);
57 | }
58 | catch (InterruptedException e) {
59 | return null;
60 | }
61 | }
62 | return md5;
63 | }
64 | }
65 |
66 | @Override
67 | public void finish() {
68 | byteQueue.add(new byte[0]);
69 | }
70 |
71 | @Override
72 | public void process(byte[] toProcess) {
73 | byteQueue.add(toProcess);
74 | }
75 |
76 | @Override
77 | protected int getThreadCount() {
78 | return 1;
79 | }
80 |
81 | @Override
82 | protected void internalProcess() {
83 | try {
84 | while (isRunning && !Thread.currentThread().isInterrupted())
85 | {
86 | byte[] toRead = byteQueue.poll(5, TimeUnit.SECONDS);
87 | if (toRead != null) {
88 | if (toRead.length == 0)
89 | {
90 | synchronized(lock) {
91 | md5 = digest.digest();
92 | lock.notifyAll();
93 | }
94 | return;
95 | }
96 | digest.update(toRead);
97 | }
98 | }
99 | }
100 | catch (InterruptedException e) {
101 | Logging.log(e);
102 | }
103 |
104 | }
105 |
106 | }
107 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/MainScreenController.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ui;
2 |
3 | import java.net.URL;
4 | import java.util.ResourceBundle;
5 |
6 | import javafx.application.Platform;
7 | import javafx.beans.value.ChangeListener;
8 | import javafx.beans.value.WeakChangeListener;
9 | import javafx.event.ActionEvent;
10 | import javafx.fxml.FXML;
11 | import javafx.scene.control.Button;
12 | import javafx.scene.control.Label;
13 | import javafx.scene.control.ProgressBar;
14 | import javafx.scene.control.TextArea;
15 | import javafx.scene.paint.Color;
16 |
17 | /**
18 | * The controller for ION's main screen.
19 | */
20 | public class MainScreenController extends BaseController {
21 |
22 | @FXML
23 | private Button advancedOptionsButton;
24 | @FXML
25 | private Button infoButton;
26 | @FXML
27 | private Button manuallySelectDeviceButton;
28 | @FXML
29 | private TextArea outputTextArea;
30 | @FXML
31 | private Label writeblockStatus;
32 | @FXML
33 | private ProgressBar progressBar;
34 | @FXML
35 | private Label readyLabel;
36 | private ChangeListener writeBlockListener;
37 |
38 | /**
39 | * Get the location of this controller's corresponding FXML file.
40 | * @return the FXML file location.
41 | */
42 | public static String getFXMLLocation() {
43 | return "fxml/IONMain.fxml";
44 | }
45 |
46 | @FXML
47 | void onManuallySelectDeviceClick(ActionEvent event) {
48 | changeScene(loadFXML(SelectDeviceController.class, SelectDeviceController.getFXMLLocation()).getScene());
49 | }
50 |
51 | @FXML
52 | void onAdvancedOptionsClick(ActionEvent event) {
53 | changeScene(loadFXML(AdvancedOptionsController.class, AdvancedOptionsController.getFXMLLocation()).getScene());
54 | }
55 |
56 | @FXML
57 | void onInfoClick(ActionEvent event) {
58 | changeScene(loadFXML(InfoScreenController.class, InfoScreenController.getFXMLLocation()).getScene());
59 | }
60 |
61 | private ChangeListener getWriteblockListener() {
62 | writeBlockListener = (observable, oldValue, newValue) -> Platform.runLater(() -> {
63 | if (newValue) {
64 | writeblockStatus.setText("ENABLED");
65 | writeblockStatus.setTextFill(Color.LIME);
66 | readyLabel.setVisible(true);
67 | writeblockStatus.autosize();
68 | }
69 | else {
70 | writeblockStatus.setText("DISABLED");
71 | writeblockStatus.setTextFill(Color.RED);
72 | readyLabel.setVisible(false);
73 | writeblockStatus.autosize();
74 | }
75 | });
76 | return new WeakChangeListener<>(writeBlockListener);
77 | }
78 |
79 | @Override
80 | public void initialize(URL arg0, ResourceBundle arg1) {
81 | setTitle();
82 | }
83 |
84 | @Override
85 | protected void setTitle() {
86 | setTitle("IO");
87 | }
88 |
89 | @Override
90 | protected void performSetup() {
91 | writeblockStatus.setText("ENABLED");
92 | writeblockStatus.setTextFill(Color.LIME);
93 | writeblockStatus.autosize();
94 | workflowController.registerWriteBlockListener(getWriteblockListener());
95 | if (!workflowController.checkValidSaveDirectory()) {
96 | displayErrorPopup("Your currently selected save location is invalid, please select a new save location.");
97 | Platform.runLater(() -> changeScene(loadFXML(AdvancedOptionsController.class, AdvancedOptionsController.getFXMLLocation()).getScene()));
98 | }
99 | }
100 |
101 | }
102 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/fxml/IONMain.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
18 |
22 |
26 |
30 |
35 |
40 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/fxml/IONInfo.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
17 |
22 |
27 |
32 |
37 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/Section.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.nio.ByteOrder;
5 | import java.util.Arrays;
6 | import java.util.zip.Adler32;
7 |
8 | import com.ciphertechsolutions.io.processing.triage.ByteUtils;
9 |
10 |
11 | /**
12 | * A base class for sections for EnCase6 Format.
13 | *
14 | */
15 | public abstract class Section {
16 |
17 | protected static final int SECTION_HEADER_SIZE = 76;
18 |
19 | private static final int MAX_TYPE_LENGTH = 16;
20 |
21 | // Max length of 16 bytes, should we just use a string?
22 | String typeString;
23 |
24 | protected long currentOffset;
25 |
26 | protected long nextOffset;
27 |
28 | protected long sectionSize;
29 |
30 | private static final byte[] padding = new byte[40];
31 |
32 |
33 | // This goes at the very end of the section header.
34 | int adler32;
35 |
36 | protected Section(String typeString) {
37 | this(0, typeString);
38 | }
39 |
40 | protected Section(long currentOffset, String typeString) {
41 | this(currentOffset, typeString, currentOffset + SECTION_HEADER_SIZE, SECTION_HEADER_SIZE);
42 | }
43 |
44 | protected Section(long currentOffset, String typeString, long nextOffset, long sectionSize) {
45 | this.currentOffset = currentOffset;
46 | if (typeString.length() > MAX_TYPE_LENGTH) {
47 | throw new IllegalArgumentException();
48 | }
49 | this.typeString = typeString;
50 | this.nextOffset = nextOffset;
51 | this.sectionSize = sectionSize;
52 | }
53 |
54 | /**
55 | * Get the section header bytes, although initialized with only the type string.
56 | * @return A mostly empty byte array that can serve as a placeholder for the section header.
57 | */
58 | public byte[] getInitialHeader() {
59 | return Arrays.copyOf(typeString.getBytes(), SECTION_HEADER_SIZE);
60 | }
61 |
62 | /**
63 | * Get the section header as a byte array.
64 | * @return A byte array containing the full section header.
65 | */
66 | public byte[] getFullHeader() {
67 | // TODO: Use a straight byte array instead if efficiency is an issue.
68 | ByteBuffer buffer = ByteBuffer.allocate(SECTION_HEADER_SIZE);
69 | buffer.order(ByteOrder.LITTLE_ENDIAN);
70 | buffer.put(Arrays.copyOf(typeString.getBytes(), MAX_TYPE_LENGTH));
71 | buffer.putLong(nextOffset);
72 | buffer.putLong(sectionSize);
73 | buffer.put(padding);
74 | buffer.putInt(getAdler32());
75 | return buffer.array();
76 | }
77 |
78 | /**
79 | * Get the offset of this section.
80 | * @return The offset.
81 | */
82 | public long getCurrentOffset() {
83 | return currentOffset;
84 | }
85 |
86 | public void setCurrentOffset(long currentOffset) {
87 | this.nextOffset = this.nextOffset - (this.currentOffset - currentOffset);
88 | this.currentOffset = currentOffset;
89 | }
90 |
91 | /**
92 | * Get the offset of the next section.
93 | * @return The offset of the next section.
94 | */
95 | public long getNextOffset() {
96 | return nextOffset;
97 | }
98 |
99 | /**
100 | * Get the adler32 checksum of the section header.
101 | * @return The section header checksum.
102 | */
103 | public int getAdler32() {
104 | if (adler32 == 0) {
105 | adler32 = calcAdler32();
106 | }
107 | return adler32;
108 | }
109 |
110 | private int calcAdler32() {
111 | Adler32 adlerCalc = new Adler32();
112 | adlerCalc.update(Arrays.copyOf(typeString.getBytes(), MAX_TYPE_LENGTH));
113 | adlerCalc.update(ByteUtils.longToBytes(nextOffset));
114 | adlerCalc.update(ByteUtils.longToBytes(sectionSize));
115 | adlerCalc.update(padding);
116 | return (int) adlerCalc.getValue();
117 | }
118 |
119 | /**
120 | * Get the size of this section.
121 | * @return The section size.
122 | */
123 | public long getSectionSize() {
124 | return sectionSize;
125 | }
126 | }
127 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/usb/USBPoller.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.usb;
2 |
3 | import com.ciphertechsolutions.io.applicationLogic.IProcessController;
4 | import com.ciphertechsolutions.io.applicationLogic.IStoppable;
5 | import com.ciphertechsolutions.io.device.Disk;
6 |
7 | import javafx.beans.property.SimpleBooleanProperty;
8 | import javafx.beans.value.ChangeListener;
9 |
10 | /**
11 | * A poller to monitor the connected devices as well as the USBWriteBlock registry key status.
12 | */
13 | public class USBPoller implements IStoppable {
14 |
15 | private final Thread pollingThread;
16 | private boolean running = false;
17 | private final IProcessController manager;
18 | private volatile Object hasPolled = new Object();
19 | private final SimpleBooleanProperty isWriteblockEnabled;
20 |
21 | /**
22 | * Sole constructor. Will notify the given {@link IProcessController} of any newly connected drives.
23 | * @param manager
24 | */
25 | public USBPoller(IProcessController manager) {
26 | this.manager = manager;
27 | this.isWriteblockEnabled = new SimpleBooleanProperty(UsbWriteBlock.getWriteBlockStatus());
28 | pollingThread = new Thread(new Poller(), "DevicePolling");
29 | }
30 |
31 | /**
32 | * Begins polling for connected devices and USBWriteBlock status.
33 | */
34 | public void startPolling() {
35 | running = true;
36 | pollingThread.start();
37 | waitForFirstPoll();
38 | }
39 |
40 | /**
41 | * Stops polling for connected devices and USBWriteBlock status.
42 | */
43 | public void stopPolling() {
44 | running = false;
45 | pollingThread.interrupt();
46 | }
47 |
48 | /**
49 | * Waits until the initial polling for devices and USBWriteBlock status is completed. Will wait a maximum of 6.5 seconds.
50 | */
51 | public void waitForFirstPoll() {
52 | synchronized (hasPolled) {
53 | try {
54 | hasPolled.wait(6500);
55 | }
56 | catch (InterruptedException e) {
57 | // We tried to wait.
58 | }
59 | }
60 | }
61 |
62 | /**
63 | * Register a listener for changes in USBWriteBlock status.
64 | * @param listener The listener to register.
65 | */
66 | public void addWriteBlockListener(ChangeListener listener) {
67 | isWriteblockEnabled.addListener(listener);
68 | }
69 |
70 | /**
71 | * Unregister a listener for changes in USBWriteBlock status.
72 | * @param listener The listener to unregister.
73 | */
74 | public void removeWriteBlockListener(ChangeListener listener) {
75 | isWriteblockEnabled.removeListener(listener);
76 | }
77 |
78 | @Override
79 | public void stop() {
80 | stopPolling();
81 | }
82 |
83 | private void notifyDisk(Disk insertedDisk) {
84 | manager.handleDiskInsertion(insertedDisk);
85 | }
86 |
87 | class Poller implements Runnable {
88 |
89 | @Override
90 | public void run() {
91 | synchronized (hasPolled) {
92 | manager.getAvailableDisks();
93 | hasPolled.notifyAll();
94 | }
95 | Disk insertedDisk = null;
96 | while (running) {
97 | while (running && insertedDisk == null) {
98 | try {
99 | Thread.sleep(2500);
100 | insertedDisk = manager.findNewDisk();
101 | boolean currentStatus = UsbWriteBlock.getWriteBlockStatus();
102 | if (currentStatus != isWriteblockEnabled.get()) {
103 | isWriteblockEnabled.set(currentStatus);
104 | }
105 | }
106 | catch (InterruptedException e) {
107 | // Don't care.
108 | }
109 | }
110 | if (insertedDisk != null) {
111 | notifyDisk(insertedDisk);
112 | }
113 | insertedDisk = null;
114 | }
115 | }
116 |
117 | }
118 | }
119 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/applicationLogic/IProcessController.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.applicationLogic;
2 |
3 | import java.util.Set;
4 |
5 | import com.ciphertechsolutions.io.applicationLogic.options.AdvancedOptions;
6 | import com.ciphertechsolutions.io.device.Device;
7 | import com.ciphertechsolutions.io.device.Disk;
8 | import com.ciphertechsolutions.io.ui.BaseController;
9 |
10 | import javafx.beans.value.ChangeListener;
11 | import javafx.collections.ListChangeListener;
12 | import javafx.util.StringConverter;
13 |
14 | /**
15 | * An interface specifying the responsibilities of the overall application controller.
16 | *
17 | */
18 | public interface IProcessController extends IStoppable {
19 |
20 | /**
21 | * Gets the appropriate controller to display given the current state.
22 | * @return The appropriate controller.
23 | */
24 | public BaseController getController();
25 |
26 | /**
27 | * Get a set containing all available disks on the system.
28 | * @return The set of disks.
29 | */
30 | public Set getAvailableDisks();
31 |
32 | /**
33 | * Gets a new disk that has been inserted to the system. May return null if none are present.
34 | * If multiple drives are new, only one new one will be returned.
35 | * @return The new disk. Null if there are no new disks.
36 | */
37 | public Disk findNewDisk();
38 |
39 | /**
40 | * Performs any necessary actions upon new disk insertion.
41 | */
42 | public void handleDiskInsertion(Disk insertedDisk);
43 |
44 | /**
45 | * Get a {@link StringConverter} for {@link com.ciphertechsolutions.io.device.Disk disks}.
46 | * @return A StringConverter for disks.
47 | */
48 | public StringConverter getStringConverterForDisks();
49 |
50 | /**
51 | * Sets the given device as the selected one.
52 | * @param device The device to select.
53 | */
54 | public void selectDevice(Device device);
55 |
56 | /**
57 | * Prepares to image the previously selected device.
58 | * @return True if a device was selected and imaging successfully began, false otherwise.
59 | */
60 | public boolean setupImaging();
61 |
62 | /**
63 | * Begins imaging the previously setup device. Setup must have been called first.
64 | */
65 | public void beginImaging();
66 |
67 | /**
68 | * Stops imaging the device, then calls the callback. {@link #beginImaging()} must have been called first.
69 | */
70 | public void stopImaging(Runnable callback);
71 |
72 | /**
73 | * Returns whether or not there is a {@link Device device} currently selected for imaging.
74 | * @return True if there is a device, false otherwise.
75 | */
76 | public boolean hasDevice();
77 |
78 | /**
79 | * Returns whether or not the currently selected save location is valid. If it does not exist it will try to create it.
80 | * @return True if the save location is valid, false otherwise.
81 | */
82 | public boolean checkValidSaveDirectory();
83 |
84 | /**
85 | * Registers a {@link ListChangeListener listener} to obtain all logging output from this process controller.
86 | * @param listener The listener to register
87 | */
88 | public void registerOutputListener(ListChangeListener listener);
89 |
90 | /**
91 | * Registers a {@link ChangeListener listener} to obtain progress information from this process controller.
92 | * @param listener The listener to register
93 | */
94 | public void registerProgressListener(ChangeListener listener);
95 |
96 | /**
97 | * Gets the maximum progress count of this process controller. A {@link Device device} must be set first.
98 | * Use {@link #hasDevice()} to check for a device first.
99 | * @return The maximum progress, as a long.
100 | */
101 | public long getMaxOutputCount();
102 |
103 | /**
104 | * Save the given {@link AdvancedOptions options} to disk.
105 | * @param options The {@link AdvancedOptions options} to save.
106 | */
107 | public void saveOptions(AdvancedOptions options);
108 |
109 | /**
110 | * Set the active {@link AdvancedOptions options} for this processing controller to the given {@link AdvancedOptions options}.
111 | * @param options The {@link AdvancedOptions options} to set.
112 | */
113 | public void setOptions(AdvancedOptions options);
114 |
115 | /**
116 | * Registers a listener for WriteBlock changes. If the Windows registry value of the WriteBlock value is changed,
117 | * any registered listeners will be notified.
118 | * @param listener The listener to notify.
119 | */
120 | public void registerWriteBlockListener(ChangeListener listener);
121 |
122 | }
123 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/HeaderSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | import java.nio.charset.StandardCharsets;
4 | import java.time.LocalDateTime;
5 | import java.util.Arrays;
6 | import java.util.List;
7 | import java.util.Map;
8 |
9 | /**
10 | * Header section is defined only once. It resides after the {@link com.ciphertechsolutions.io.ewf.Header2Section} of the first segment file. It is not found in other segment files.
11 | * The header data itself is ASCII text compressed using zlib.
12 | *
13 | * Offset 76 (0x4c), Size: variable, contains information about the acquire media, below.
14 | *
15 | * For Encase 6 the information consists of at least the four lines:
16 | * Line number Meaning Value
17 | * 1 The number of sections provided 1
18 | * 2 Probably the type of information provided main
19 | * 3 Identifiers for the values in the 4th line
20 | * 4 The data for the different identifiers in the 3rd line
21 | * 5 Empty Empty
22 | *
23 | * The 3rd and the 4th line consist of the following tab (0x09) separated values.
24 | * Identifier number|Character in 3rd line|Value in 4th line
25 | * 1 c Case number
26 | * 2 n Evidence number
27 | * 3 a Unique description
28 | * 4 e Examiner name
29 | * 5 t Notes
30 | * 6 av Version The EnCase version used to acquire the media EnCase limits this value to 12 characters
31 | * 7 ov Platform The platform/operating system used to acquire the media
32 | * 8 m Acquired date
33 | * 9 u System date
34 | * 10 p pwhas
35 | * Case number, evidence number, unique description, examiner name, notes, version and platform are freeform
36 | * EOL char is return (0x13) followed by newline (0x10)
37 | * Acquired date and System date are 2002 3 4 10 19 59 which represents March 4, 2002 10:19:59 DIFFERENT THAN {@link com.ciphertechsolutions.io.ewf.Header2Section}
38 | * pwhash is 0
39 | *
40 | */
41 | public class HeaderSection extends AbstractHeaderSection {
42 | private static final String[] IDENTIFIER3_CHARS = new String[] { "c", "n", "a", "e", "t", "sn", "av", "ov", "m", "u", "p"};
43 | private static final List IDENTIFIER3_CHARS_LIST = Arrays.asList(IDENTIFIER3_CHARS);
44 |
45 | public HeaderSection(long currentOffset) {
46 | super(currentOffset, "header");
47 | }
48 |
49 | String lineOne = "1";
50 | String lineTwo = "main";
51 | String[] lineThree = IDENTIFIER3_CHARS;
52 | String[] lineFour = new String[11];
53 |
54 | private void setLineFourValues(Map identifierValues) {
55 | setLineValues(IDENTIFIER3_CHARS_LIST, lineFour, identifierValues);
56 | }
57 |
58 | /**
59 | * Acquired date and System date are 2002 3 4 10 19 59 which represents March 4, 2002 10:19:59 DIFFERENT THAN {@link com.ciphertechsolutions.io.ewf.Header2Section}
60 | */
61 | @Override
62 | protected String generateDateTimeHeaderValue(LocalDateTime timestamp) {
63 | StringBuilder sb = new StringBuilder();
64 | sb.append(timestamp.getYear() + " ");
65 | sb.append(timestamp.getMonthValue() + " ");
66 | sb.append(timestamp.getDayOfMonth() + " ");
67 | sb.append(timestamp.getHour() + " ");
68 | sb.append(timestamp.getMinute() + " ");
69 | sb.append(timestamp.getSecond());
70 |
71 | return sb.toString();
72 |
73 | }
74 |
75 | /**
76 | * Takes in Map containing user input, and set the Header Section
77 | * @param identifierValues Map containing user input case num/evidence num/ description/examiner name/notes
78 | */
79 | @Override
80 | protected void setHeaderSection(Map identifierValues) {
81 | setupLinesThreeFour(identifierValues);
82 | setLineFourValues(identifierValues);
83 | }
84 |
85 | /**
86 | * Use after {@link com.ciphertechsolutions.io.ewf.HeaderSection#generateHeaderSection()}
87 | */
88 | @Override
89 | protected byte[] convertHeaderString() {
90 | // TODO: Use a straight byte array instead if efficiency is an issue.
91 | // All lines, separated by new line
92 | // All arrays within line separated by tab
93 | StringBuilder sb = new StringBuilder();
94 | sb.append(lineOne);
95 | sb.append(getEOLString());
96 | sb.append(lineTwo);
97 | sb.append(getEOLString());
98 | sb.append(tabifyStringArrayLine(lineThree));
99 | sb.append(getEOLString());
100 | sb.append(tabifyStringArrayLine(lineFour));
101 | sb.append(getEOLString());
102 |
103 | return deflate(getStringBytesByFormat(sb.toString()));
104 | }
105 |
106 | /**
107 | * Returns ASCII text in byte[] form.
108 | */
109 | @Override
110 | protected byte[] getStringBytesByFormat(String str) {
111 | return str.getBytes(StandardCharsets.US_ASCII);
112 | }
113 |
114 | @Override
115 | protected String getBytesAsString(byte[] bytes) {
116 | return new String(bytes, StandardCharsets.US_ASCII);
117 | }
118 |
119 |
120 | }
121 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/VolumeSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.nio.ByteOrder;
5 | import java.util.zip.Adler32;
6 |
7 | import com.ciphertechsolutions.io.device.Device;
8 |
9 | /**
10 | * A class to represent the Volume section in the Encase6 format.
11 | */
12 | public class VolumeSection extends Section {
13 |
14 | static final int ADDITIONAL_SECTION_SIZE = 1052;
15 |
16 | //TODO: Enum?
17 | //0x00 => removable disk
18 | // 0x01 => fixed disk
19 | // 0x03 => optical disk
20 | // 0x0e => Logical evidence file (LEV or L01)
21 | // 0x10 => memory (RAM/process)
22 | MediaType mediaType;
23 |
24 | byte[] unknown = { 0x00, 0x00, 0x00 };
25 |
26 | int chunkCount;
27 |
28 | // TODO: Have the chunker set these, or be set from these.
29 | int sectorsPerChunk;
30 |
31 | int bytesPerSector;
32 |
33 | //TODO: Confirm long vs int
34 | long sectorCount;
35 |
36 | int cylinders;
37 | // Per cylinder, I think
38 | int heads;
39 | // Per head, I think
40 | int sectors;
41 |
42 | int mediaFlag;
43 |
44 | int IS_IMAGE_FILE_FLAG = 0x01;
45 | int IS_PHYSICAL_FLAG = 0x02;
46 | int FASTBLOC_USED_FLAG = 0x04;
47 | int TABLEAU_BLOCK_USED_FLAG = 0x08;
48 |
49 |
50 | // ???
51 | int palmVolumeStartSector;
52 |
53 | byte[] unknown2 = {0x00, 0x00, 0x00, 0x00};
54 |
55 | int smartLogStartSector;
56 |
57 | // I don't think this corresponds exactly to zlib levels, unsure though.
58 | int compressionLevel;
59 |
60 |
61 | //TODO: Tie this to our error handling in drive reader?
62 | int errorGranularity;
63 |
64 | byte[] unknown3 = {0x00, 0x00, 0x00, 0x00};
65 |
66 | byte[] fileSetGUID = new byte[16];
67 |
68 | // I am not typing this one out.
69 | byte[] unknown4 = new byte[963];
70 |
71 | //???
72 | byte[] signature = new byte[5];
73 |
74 | int secondaryAdler32 = 0;
75 |
76 | protected VolumeSection(long currentOffset, Device disk, byte[] guid) {
77 | this("volume", currentOffset, disk, guid);
78 | }
79 |
80 | protected VolumeSection(String typeString, long currentOffset, Device disk, byte[] guid) {
81 | super(currentOffset, typeString);
82 | this.sectionSize += ADDITIONAL_SECTION_SIZE;
83 | this.nextOffset += ADDITIONAL_SECTION_SIZE;
84 |
85 | // Maybe leave these as 0?
86 | this.cylinders = 0;
87 | this.heads = 0;
88 | this.sectors = 0;
89 |
90 | this.fileSetGUID = guid;
91 | // TODO: Set these more appropriately.
92 | this.mediaType = MediaType.FIXED_STORAGE_MEDIA; // Should match actual media type.
93 | this.compressionLevel = 0x01; //Make configurable maybe?
94 | this.errorGranularity = 1; // Should be tied to our reading granularity.
95 | this.mediaFlag = IS_IMAGE_FILE_FLAG | IS_PHYSICAL_FLAG; //Should match actual.
96 | this.sectorsPerChunk = 1024; //Must match compressed chunker
97 | this.smartLogStartSector = 0; // Measured from the end of media.
98 | this.palmVolumeStartSector = 0; //I have no idea what this is.
99 | this.bytesPerSector = 512; // Compressed chunker needs this too. Almost always 512.
100 |
101 | // These will get updated manually later.
102 | this.chunkCount = 0;
103 | this.sectorCount = 0;
104 | }
105 |
106 | void correctSizeInformation(int chunkCount, long sectorCount) {
107 | this.chunkCount = chunkCount;
108 | this.sectorCount = sectorCount;
109 | secondaryAdler32 = 0;
110 | }
111 |
112 | int getSecondaryAdler32(){
113 | if (secondaryAdler32 == 0) {
114 | Adler32 adlerCalc = new Adler32();
115 | ByteBuffer bytes = getPartialBytes();
116 | bytes.flip();
117 | adlerCalc.update(bytes);
118 | secondaryAdler32 = (int) adlerCalc.getValue();
119 | }
120 | return secondaryAdler32;
121 | }
122 |
123 | byte[] getAdditionalBytes() {
124 | ByteBuffer bytes = getPartialBytes();
125 | bytes.order(ByteOrder.LITTLE_ENDIAN);
126 | bytes.putInt(getSecondaryAdler32());
127 | return bytes.array();
128 |
129 | }
130 |
131 | private ByteBuffer getPartialBytes() {
132 | ByteBuffer bytes = ByteBuffer.allocate(ADDITIONAL_SECTION_SIZE);
133 | bytes.order(ByteOrder.LITTLE_ENDIAN);
134 | bytes.put(mediaType.value);
135 | bytes.put(unknown);
136 | bytes.putInt(chunkCount);
137 | bytes.putInt(sectorsPerChunk);
138 | bytes.putInt(bytesPerSector);
139 | bytes.putLong(sectorCount);
140 | bytes.putInt(cylinders);
141 | bytes.putInt(heads);
142 | bytes.putInt(sectors);
143 | bytes.putInt(mediaFlag);
144 | bytes.putInt(palmVolumeStartSector);
145 | bytes.put(unknown2);
146 | bytes.putInt(smartLogStartSector);
147 | bytes.putInt(compressionLevel);
148 | bytes.putInt(errorGranularity);
149 | bytes.put(unknown3);
150 | bytes.put(fileSetGUID);
151 | bytes.put(unknown4);
152 | bytes.put(signature);
153 | return bytes;
154 | }
155 |
156 | enum MediaType {
157 | REMOVABLE_MEDIA(0x00), FIXED_STORAGE_MEDIA(0x01), OPTICAL_DISK(0x03), LOGICAL_EVIDENCE_FILE(0x0E), PHYSICAL_MEMORY(0x10);
158 |
159 | final byte value;
160 |
161 | MediaType(int value) {
162 | this.value = (byte) value;
163 | }
164 | }
165 |
166 | }
167 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/ChunkedCompressor.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing;
2 |
3 | import java.nio.ByteBuffer;
4 | import java.util.Arrays;
5 | import java.util.concurrent.BlockingQueue;
6 | import java.util.concurrent.ExecutorService;
7 | import java.util.concurrent.Executors;
8 | import java.util.concurrent.Future;
9 | import java.util.concurrent.LinkedBlockingQueue;
10 | import java.util.concurrent.TimeUnit;
11 |
12 | import com.ciphertechsolutions.io.ewf.DataChunk;
13 | import com.ciphertechsolutions.io.logging.Logging;
14 |
15 | /**
16 | * Groups input into a consistently sized chunks, then compresses those chunks. The compression is done in a
17 | * multi-threaded manner. The output is guaranteed to be in the same order as the input, and is accessible via
18 | * {@link #getOutputQueue()}.
19 | */
20 | public class ChunkedCompressor extends ProcessorBase {
21 |
22 | private final BlockingQueue byteQueue = new LinkedBlockingQueue<>();
23 | private final ExecutorService executor = Executors.newFixedThreadPool(8);
24 | private final BlockingQueue> compressedOutputQueue = new LinkedBlockingQueue<>();
25 | private final int CHUNK_SIZE;
26 | private final static int DEFAULT_CHUNK_SIZE = 1024 * 512;
27 | private final ByteBuffer localBuffer;
28 | private final int compressionLevel;
29 |
30 | /**
31 | * Creates a ChunkedCompressor with the given chunk size and compression level.
32 | * @param chunkSize The size, in bytes, to group data into.
33 | * @param compressionLevel The level of compression to use (uses z-lib for compression).
34 | */
35 | public ChunkedCompressor(int chunkSize, int compressionLevel) {
36 | super("ChunkCompressor");
37 | CHUNK_SIZE = chunkSize;
38 | localBuffer = ByteBuffer.allocate(CHUNK_SIZE);
39 | this.compressionLevel = compressionLevel;
40 | }
41 |
42 | /**
43 | * Convenience constructor, equivalent to {@link #ChunkedCompressor(int, int)} called with compressionLevel, {@link #DEFAULT_CHUNK_SIZE}}.
44 | * @param compressionLevel The compression level to use.
45 | */
46 | public ChunkedCompressor(int compressionLevel) {
47 | this(DEFAULT_CHUNK_SIZE, compressionLevel);
48 | }
49 |
50 | /**
51 | * Default constructor, equivalent to {@link #ChunkedCompressor(int) ChunkedCompressor(1)}.
52 | */
53 | public ChunkedCompressor() {
54 | this(1);
55 | }
56 |
57 | /**
58 | * Returns the output queue. The contents of this queue are the actual output, not copies of it.
59 | * @return The output queue.
60 | */
61 | public BlockingQueue> getOutputQueue() {
62 | return compressedOutputQueue;
63 | }
64 |
65 | @Override
66 | public void internalProcess() {
67 | try {
68 | while (isRunning && !Thread.currentThread().isInterrupted())
69 | {
70 | byte[] toRead = byteQueue.poll(5, TimeUnit.SECONDS);
71 | if (toRead != null) {
72 | if (toRead.length == 0)
73 | {
74 | finalizeChunkStream();
75 | return;
76 | }
77 | int offset = 0;
78 | int remainingCapacity = localBuffer.remaining();
79 | boolean isBufferEmpty = localBuffer.position() == 0;
80 | boolean toReadOverCapacity = toRead.length >= remainingCapacity;
81 | if (!isBufferEmpty && toReadOverCapacity) {
82 | offset = fillAndFlushLocalBuffer(toRead, remainingCapacity);
83 | }
84 | int remainingLength = toRead.length - offset;
85 | while (remainingLength != 0) {
86 | if (remainingLength >= CHUNK_SIZE) {
87 | compressedOutputQueue.add(executor.submit(new CompressionTask(Arrays.copyOfRange(toRead, offset, offset + CHUNK_SIZE),
88 | compressionLevel)));
89 | offset += CHUNK_SIZE;
90 | remainingLength = toRead.length - offset;
91 | }
92 | else {
93 | localBuffer.put(toRead, offset, remainingLength);
94 | remainingLength = 0;
95 | }
96 | }
97 | }
98 | }
99 | }
100 | catch (InterruptedException e) {
101 | Logging.log(e);
102 | }
103 | }
104 |
105 | protected int fillAndFlushLocalBuffer(byte[] toRead, int remainingCapacity) {
106 | localBuffer.put(toRead, 0, remainingCapacity);
107 | compressedOutputQueue.add(executor.submit(new CompressionTask(Arrays.copyOf(localBuffer.array(), localBuffer.position()), compressionLevel)));
108 | localBuffer.clear();
109 | return remainingCapacity;
110 | }
111 |
112 | protected void finalizeChunkStream() {
113 | if (localBuffer.position() != 0) {
114 | compressedOutputQueue.add(executor.submit(new CompressionTask(Arrays.copyOf(localBuffer.array(), localBuffer.position()), compressionLevel)));
115 | localBuffer.clear();
116 | }
117 | isRunning = false;
118 | compressedOutputQueue.add(executor.submit(new CompressionTask(new byte[0], compressionLevel)));
119 | executor.shutdown();
120 | }
121 |
122 | @Override
123 | public void initialize() {
124 | startThreads();
125 | }
126 |
127 | @Override
128 | public void finish() {
129 | byteQueue.add(new byte[0]);
130 | }
131 |
132 | @Override
133 | public void process(byte[] toProcess) {
134 | byteQueue.add(toProcess);
135 | }
136 |
137 | @Override
138 | protected int getThreadCount() {
139 | return 1;
140 | }
141 |
142 | }
143 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/logging/ObservableOutputStream.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.logging;
2 |
3 | import java.io.IOException;
4 | import java.io.OutputStream;
5 | import java.util.Collection;
6 | import java.util.Iterator;
7 | import java.util.List;
8 | import java.util.ListIterator;
9 |
10 | import javafx.beans.InvalidationListener;
11 | import javafx.collections.FXCollections;
12 | import javafx.collections.ListChangeListener;
13 | import javafx.collections.ObservableList;
14 |
15 | /**
16 | * A class to easily turn an OutputStream into an ObservableList.
17 | */
18 | public class ObservableOutputStream extends OutputStream implements ObservableList {
19 |
20 | private StringBuilder currentString = new StringBuilder();
21 | private final ObservableList wrappedObservable = FXCollections.synchronizedObservableList(FXCollections.observableArrayList());
22 |
23 | @Override
24 | public int size() {
25 | return wrappedObservable.size();
26 | }
27 |
28 | @Override
29 | public boolean isEmpty() {
30 | return wrappedObservable.isEmpty();
31 | }
32 |
33 | @Override
34 | public boolean contains(Object o) {
35 | return wrappedObservable.contains(o);
36 | }
37 |
38 | @Override
39 | public Iterator iterator() {
40 | return wrappedObservable.iterator();
41 | }
42 |
43 | @Override
44 | public Object[] toArray() {
45 | return wrappedObservable.toArray();
46 | }
47 |
48 | @Override
49 | public T[] toArray(T[] a) {
50 | return wrappedObservable.toArray(a);
51 | }
52 |
53 | @Override
54 | public boolean add(String e) {
55 | return wrappedObservable.add(e);
56 | }
57 |
58 | @Override
59 | public boolean remove(Object o) {
60 | return wrappedObservable.remove(o);
61 | }
62 |
63 | @Override
64 | public boolean containsAll(Collection> c) {
65 | return wrappedObservable.containsAll(c);
66 | }
67 |
68 | @Override
69 | public boolean addAll(Collection extends String> c) {
70 | return wrappedObservable.addAll(c);
71 | }
72 |
73 | @Override
74 | public boolean addAll(int index, Collection extends String> c) {
75 | return wrappedObservable.addAll(index, c);
76 | }
77 |
78 | @Override
79 | public boolean removeAll(Collection> c) {
80 | return wrappedObservable.removeAll(c);
81 | }
82 |
83 | @Override
84 | public boolean retainAll(Collection> c) {
85 | return wrappedObservable.retainAll(c);
86 | }
87 |
88 | @Override
89 | public void clear() {
90 | wrappedObservable.clear();
91 | }
92 |
93 | @Override
94 | public String get(int index) {
95 | return wrappedObservable.get(index);
96 | }
97 |
98 | @Override
99 | public String set(int index, String element) {
100 | return wrappedObservable.set(index, element);
101 | }
102 |
103 | @Override
104 | public void add(int index, String element) {
105 | wrappedObservable.add(index, element);
106 | }
107 |
108 | @Override
109 | public String remove(int index) {
110 | return wrappedObservable.remove(index);
111 | }
112 |
113 | @Override
114 | public int indexOf(Object o) {
115 | return wrappedObservable.indexOf(o);
116 | }
117 |
118 | @Override
119 | public int lastIndexOf(Object o) {
120 | return wrappedObservable.lastIndexOf(o);
121 | }
122 |
123 | @Override
124 | public ListIterator listIterator() {
125 | return wrappedObservable.listIterator();
126 | }
127 |
128 | @Override
129 | public ListIterator listIterator(int index) {
130 | return wrappedObservable.listIterator(index);
131 | }
132 |
133 | @Override
134 | public List subList(int fromIndex, int toIndex) {
135 | return wrappedObservable.subList(fromIndex, toIndex);
136 | }
137 |
138 | @Override
139 | public void addListener(InvalidationListener listener) {
140 | wrappedObservable.addListener(listener);
141 |
142 | }
143 |
144 | @Override
145 | public void removeListener(InvalidationListener listener) {
146 | wrappedObservable.removeListener(listener);
147 | }
148 |
149 | @Override
150 | public boolean addAll(String... elements) {
151 | return wrappedObservable.addAll(elements);
152 | }
153 |
154 | @Override
155 | public void addListener(ListChangeListener super String> listener) {
156 | wrappedObservable.addListener(listener);
157 | }
158 |
159 | @Override
160 | public void remove(int from, int to) {
161 | wrappedObservable.remove(from, to);
162 | }
163 |
164 | @Override
165 | public boolean removeAll(String... elements) {
166 | return wrappedObservable.removeAll(elements);
167 | }
168 |
169 | @Override
170 | public void removeListener(ListChangeListener super String> listener) {
171 | removeListener(listener);
172 | }
173 |
174 | @Override
175 | public boolean retainAll(String... elements) {
176 | return wrappedObservable.retainAll(elements);
177 | }
178 |
179 | @Override
180 | public boolean setAll(String... elements) {
181 | return wrappedObservable.setAll(elements);
182 | }
183 |
184 | @Override
185 | public boolean setAll(Collection extends String> col) {
186 | return setAll(col);
187 | }
188 |
189 | @Override
190 | public void write(int b) throws IOException {
191 | if (b < 0) {
192 | currentString.append((char) (b & 0x00FF)); // TODO: Not entirely sure why this is needed or if this works in all cases.
193 | }
194 | else {
195 | currentString.append((char) b);
196 | }
197 | }
198 |
199 | @Override
200 | public void flush() {
201 | synchronized (currentString) {
202 | if (currentString.length() > 0) {
203 | wrappedObservable.add(currentString.toString());
204 | currentString = new StringBuilder();
205 | }
206 | }
207 | }
208 | }
209 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/device/Volume.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.device;
2 |
3 | import java.io.IOException;
4 | import java.nio.file.FileStore;
5 |
6 | /**
7 | * A class representing a volume on a disk.
8 | */
9 | public class Volume extends Device {
10 |
11 | private static final String BLANK_SERIAL = "00000000";
12 |
13 | /**
14 | * The volume's label, or "NO NAME" if none can be found.
15 | */
16 | public String label;
17 |
18 | /**
19 | * Volume serial number.
20 | */
21 | public String volumeSerial;
22 |
23 | /**
24 | * The type of volume.
25 | */
26 | public String type;
27 |
28 | /**
29 | * The file system format of the volume.
30 | */
31 | public String format;
32 |
33 | /**
34 | * The free space in the volume.
35 | */
36 | public long free;
37 |
38 | /**
39 | * The {@link Disk disk} this volume is on.
40 | */
41 | public Disk underlyingDisk;
42 |
43 | /**
44 | * A constructor to create a Volume object from a FileStore object.
45 | *
46 | * @param root
47 | * @param fileStore
48 | */
49 | public Volume(String root, FileStore fileStore)
50 | {
51 | label = fileStore.name(); //TODO VolumeLabel;
52 | name = fileStore.name();
53 | type = fileStore.type();
54 | format = fileStore.type(); // TODO DriveFormat
55 | path = root; // ex. C:\
56 |
57 | try {
58 | size = fileStore.getTotalSpace();
59 | free = fileStore.getUsableSpace();
60 | }
61 | catch (IOException e) {
62 | e.printStackTrace();
63 | }
64 | }
65 |
66 | /**
67 | * The primary constructor for a Volume object
68 | * @param info The wmi query result to instantiate from
69 | */
70 | public Volume(String info)
71 | {
72 | String[] lines = info.split(System.lineSeparator());
73 | if (lines.length >= 8) {
74 | path = trimPropertyName(lines[0]);
75 | name = trimPropertyName(lines[1]);
76 | format = trimPropertyName(lines[2]);
77 | label = trimPropertyName(lines[3]);
78 | free = safeParseLong(trimPropertyName(lines[4]));
79 | size = safeParseLong(trimPropertyName(lines[5]));
80 | type = getDriveTypeByNumber(trimPropertyName(lines[6]));
81 | volumeSerial = formatVolumeSerial(lines[7]); // logical disk query
82 | }
83 | else {
84 | throw new IllegalArgumentException("Invalid volume info");
85 | }
86 |
87 | }
88 |
89 | private static String getDriveTypeByNumber(String driveType) {
90 | switch (driveType) {
91 | case "1":
92 | return "Invalid";
93 | case "2":
94 | return "Removable";
95 | case "3":
96 | return "Fixed";
97 | case "4":
98 | return "Remote";
99 | case "5":
100 | return "CD-ROM";
101 | case "6":
102 | return "RAM disk";
103 | case "0":
104 | default:
105 | return "Unknown";
106 | }
107 | }
108 |
109 | protected String formatVolumeSerial(String line) {
110 | String volSerial = String.format("%08X", safeParseInt(trimPropertyName(line)));
111 | return volSerial.substring(0,4) + "-" + volSerial.substring(4);
112 | }
113 |
114 | private static int safeParseInt(String toParse) {
115 | if (toParse.length() == 0) {
116 | return 0;
117 | }
118 | else {
119 | try {
120 | return Integer.parseInt(toParse);
121 | }
122 | catch (NumberFormatException e) {
123 | return 0;
124 | }
125 | }
126 | }
127 |
128 | private static long safeParseLong(String toParse) {
129 | if (toParse.length() == 0) {
130 | return 0;
131 | }
132 | else {
133 | try {
134 | return Long.parseLong(toParse);
135 | }
136 | catch (NumberFormatException e) {
137 | return 0;
138 | }
139 | }
140 | }
141 |
142 | @Override
143 | public String details() {
144 | StringBuilder sb = new StringBuilder();
145 | sb.append("Volume: " + (label.equals("") ? getName() : label) + System.lineSeparator());
146 | sb.append("Root: " + path + System.lineSeparator());
147 | sb.append("Volume Serial: " + volumeSerial + System.lineSeparator());
148 | sb.append("Format: " + format + System.lineSeparator());
149 | sb.append(" Size: " + getSize() + System.lineSeparator());
150 | sb.append(" Free: " + free + System.lineSeparator());
151 | return sb.toString();
152 | }
153 |
154 | @Override
155 | public String toFileNameString() {
156 | return label.replaceAll("\\W+", "_");
157 | }
158 |
159 | @Override
160 | public int hashCode() {
161 | if (!BLANK_SERIAL.equals(volumeSerial)) {
162 | return this.volumeSerial.hashCode();
163 | }
164 | return this.path.hashCode();
165 | }
166 |
167 | @Override
168 | public boolean equals(Object o) {
169 | if (o instanceof Volume) {
170 | return equals((Volume) o);
171 | }
172 | return false;
173 | }
174 |
175 | /**
176 | * Determines if this volume is equal to another volume using the serial number. If the serial is lacking compares instead via path.
177 | * @param o The volume to compare to.
178 | * @return True if they are equal, false otherwise.
179 | */
180 | public boolean equals(Volume o) {
181 | if (o == null) {
182 | return false;
183 | }
184 | if (BLANK_SERIAL.equals(this.volumeSerial) && BLANK_SERIAL.equalsIgnoreCase(o.volumeSerial)) {
185 | return o.path.equals(this.path);
186 | }
187 | return o.volumeSerial.equals(this.volumeSerial);
188 | }
189 |
190 | @Override
191 | public String getSerialNumber() {
192 | return volumeSerial;
193 | }
194 | }
195 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ewf/AbstractHeaderSection.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ewf;
2 |
3 | import java.time.LocalDateTime;
4 | import java.util.Arrays;
5 | import java.util.HashMap;
6 | import java.util.List;
7 | import java.util.Map;
8 | import java.util.zip.Deflater;
9 |
10 | abstract class AbstractHeaderSection extends Section {
11 |
12 | protected AbstractHeaderSection(long currentOffset, String typeString) {
13 | super(currentOffset, typeString);
14 | }
15 |
16 | protected final String TAB = "\t";
17 | protected final byte RETURN = 0x0D;
18 | protected final byte NEWLINE = 0x0A;
19 | protected byte[] convertedHeaderString = null;
20 | protected byte[] headerAsBytes = null;
21 | protected boolean isSet = false;
22 |
23 | protected abstract String generateDateTimeHeaderValue(LocalDateTime timestamp);
24 |
25 | protected abstract byte[] convertHeaderString();
26 |
27 | protected abstract byte[] getStringBytesByFormat(String str);
28 |
29 | protected abstract String getBytesAsString(byte[] bytes);
30 |
31 |
32 | /**
33 | * Takes in Map containing user input, and sets those values in the header.
34 | * @param identifierValues Map containing user input case num/evidence num/ description/examiner name/notes
35 | */
36 | protected abstract void setHeaderSection(Map identifierValues);
37 |
38 | /**
39 | * Use this method for inputting user configured strings and setting the header values.
40 | * @param uniqueDescription
41 | * @param caseNumber
42 | * @param evidenceNumber
43 | * @param examinerName
44 | * @param notes
45 | */
46 | public void setHeaderSection(String uniqueDescription, String caseNumber, String evidenceNumber,
47 | String examinerName, String notes, String serialNumber) {
48 | if (isSet) {
49 | throw new IllegalStateException("Header info already set! This information cannot be set again.");
50 | }
51 | Map identifierValues = new HashMap<>();
52 | identifierValues.put("a", uniqueDescription);
53 | identifierValues.put("c", caseNumber);
54 | identifierValues.put("n", evidenceNumber);
55 | identifierValues.put("e", examinerName);
56 | identifierValues.put("t", notes);
57 | identifierValues.put("sn", serialNumber);
58 | //TODO: ALL: md (model # of media), sn (serial number of media) from jWMI disk info, not user input.
59 | //TODO: HEADER2: gu (GUID), ah (Acquire Hash), lo (logical offset), po (physical offset), tb (total bytes)
60 | setHeaderSection(identifierValues);
61 | isSet = true;
62 | }
63 |
64 | public byte[] getHeaderAsBytes() {
65 | if (headerAsBytes == null) {
66 | if (convertedHeaderString == null) {
67 | convertedHeaderString = convertHeaderString();
68 | }
69 | nextOffset += convertedHeaderString.length;
70 | sectionSize += convertedHeaderString.length;
71 | headerAsBytes = convertedHeaderString;
72 | //TODO: if this is all we need, the extra member variable is redundant.
73 | }
74 | return headerAsBytes;
75 | }
76 |
77 | /**
78 | * Takes in Map containing user input, and generates the resulting Header Section
79 | */
80 | //public abstract HeaderSection generateHeaderSection(Map identifierValues);
81 |
82 |
83 | public byte[] getEOLBytes() {
84 | return new byte[]{ RETURN, NEWLINE };
85 | }
86 |
87 | public String getEOLString() {
88 | return "\r\n";
89 | }
90 |
91 | public String tabifyStringArrayLine(String[] line) {
92 | StringBuilder sb = new StringBuilder();
93 | int i = 0;
94 | for(String value : line) {
95 | if(i == (line.length -1)) {
96 | sb.append(value); // last value doesn't need to be tabified, will instead have EOL
97 | } else {
98 | sb.append(value + TAB);
99 | }
100 | i++;
101 | }
102 |
103 | return sb.toString();
104 | }
105 | /**
106 | * Returns header value for current date time.
107 | */
108 | protected String generateCurrentDateTimeString() {
109 | return generateDateTimeHeaderValue(LocalDateTime.now());
110 | }
111 |
112 | protected static byte[] deflate(byte[] toCompress) {
113 | Deflater deflater = new Deflater();
114 | deflater.setInput(toCompress);
115 | deflater.finish();
116 | byte[] output = new byte[toCompress.length];
117 | int compressedSize = deflater.deflate(output);
118 | //TODO: Determine if we need to append the adler-32 hash.
119 | return Arrays.copyOf(output, compressedSize);
120 | }
121 |
122 | /**
123 | * Gets the correct index of the identifier and sets the key and value at the index for the given lines.
124 | * @param identifiers List of identifier keys
125 | * @param identifier Identifier to be added
126 | * @param identifierLine to set identifier at index
127 | * @param valueLine to set value at index
128 | * @param value to be set
129 | */
130 | private static void setHeaderValueByIdentifier(List identifiers, String identifier, String[] valueLine, String value) {
131 | int index = identifiers.indexOf(identifier);
132 | if (index >= 0) { // is a correct identifier
133 | valueLine[index] = value;
134 | }
135 | }
136 |
137 |
138 | /**
139 | * Takes a map of identifiers and values and assigns them to proper lines to include in header.
140 | * @param identifiersForLine List of identifier keys
141 | * @param identifierLine Line to set identifiers of
142 | * @param valueLine Line to set values of
143 | * @param identifierValues Identifier/Value map
144 | */
145 | protected void setLineValues(List identifiersForLine, String[] valueLine, Map identifierValues) {
146 | for (String key : identifierValues.keySet()) {
147 | setHeaderValueByIdentifier(identifiersForLine, key, valueLine, identifierValues.get(key));
148 | }
149 | }
150 |
151 | /**
152 | * IdentifierValues contains larger set for Header2, but both Headers share core identifiers.
153 | * @param identifierValues map of user/system configured identifier/value
154 | */
155 | protected void setupLinesThreeFour(Map identifierValues) {
156 | String acquireDate = generateCurrentDateTimeString();
157 | identifierValues.put("av", "ION");
158 | identifierValues.put("ov", System.getProperty("os.name"));
159 | identifierValues.put("m", acquireDate);
160 | identifierValues.put("u", acquireDate);
161 | identifierValues.put("p", "0");
162 |
163 | }
164 |
165 |
166 |
167 |
168 | }
169 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/ImagingController.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ui;
2 |
3 | import java.net.URL;
4 | import java.time.Duration;
5 | import java.util.List;
6 | import java.util.ResourceBundle;
7 |
8 | import com.ciphertechsolutions.io.applicationLogic.Utils;
9 |
10 | import javafx.application.Platform;
11 | import javafx.beans.value.ChangeListener;
12 | import javafx.beans.value.WeakChangeListener;
13 | import javafx.collections.ListChangeListener;
14 | import javafx.collections.WeakListChangeListener;
15 | import javafx.event.ActionEvent;
16 | import javafx.fxml.FXML;
17 | import javafx.scene.control.Button;
18 | import javafx.scene.control.Label;
19 | import javafx.scene.control.ProgressBar;
20 | import javafx.scene.control.TextArea;
21 |
22 | /**
23 | * The controller for the imaging screen in ION.
24 | */
25 | public class ImagingController extends BaseController {
26 |
27 | @FXML
28 | private Button returnButton;
29 | @FXML
30 | private Button abortImagingButton;
31 | @FXML
32 | private TextArea outputTextArea;
33 | @FXML
34 | private Label timeEstimateLabel;
35 | @FXML
36 | private ProgressBar progressBar;
37 |
38 | private ListChangeListener outputListener;
39 | private ChangeListener progressListener;
40 |
41 | /**
42 | * Get the location of this controller's corresponding FXML file.
43 | *
44 | * @return the FXML file location.
45 | */
46 | public static String getFXMLLocation() {
47 | return "fxml/IONImaging.fxml";
48 | }
49 |
50 | @FXML
51 | void onAbortImagingClick(ActionEvent event) {
52 | workflowController.stopImaging(() -> abortImaging());
53 | }
54 |
55 | /**
56 | * Sets the screen to its aborted state.
57 | */
58 | public void abortImaging() {
59 | Platform.runLater(() -> {
60 | setReturnVisible();
61 | timeEstimateLabel.setText("Aborted");
62 | });
63 | }
64 |
65 | private void setReturnVisible() {
66 | abortImagingButton.setVisible(false);
67 | abortImagingButton.setDisable(true);
68 | returnButton.setVisible(true);
69 | returnButton.setDisable(false);
70 | }
71 |
72 | @FXML
73 | void onReturnClick(ActionEvent event) {
74 | changeScene(loadFXML(MainScreenController.class, MainScreenController.getFXMLLocation()).getScene());
75 | }
76 |
77 | private void displayImagingProgress() {
78 | workflowController.registerOutputListener(getOutputToScreenLogger());
79 | workflowController.registerProgressListener(getProgressUpdater());
80 | }
81 |
82 | @Override
83 | public void initialize(URL arg0, ResourceBundle arg1) {
84 | setTitle();
85 | }
86 |
87 | @Override
88 | protected void performSetup() {
89 | timeEstimateLabel.setVisible(false);
90 | displayImagingProgress();
91 | }
92 |
93 | @Override
94 | protected void setTitle() {
95 | setTitle("IO - Imaging");
96 | }
97 |
98 | private ListChangeListener getOutputToScreenLogger() {
99 | outputListener = new ListChangeListener() {
100 | boolean first = true;
101 |
102 | @Override
103 | public void onChanged(ListChangeListener.Change extends String> change) {
104 | change.next();
105 | Platform.runLater(() -> {
106 | List extends String> changedList = change.getList();
107 | for (int index = first ? 0 : change.getFrom(); index < change.getTo(); index++) {
108 | String added = changedList.get(index);
109 | if (added != null) {
110 | outputTextArea.appendText(added);
111 | }
112 | }
113 | first = false;
114 | });
115 | }
116 | };
117 | return new WeakListChangeListener<>(outputListener);
118 | }
119 |
120 | private ChangeListener getProgressUpdater() {
121 | ProgressStatus currentStatus = new ProgressStatus();
122 | progressListener = (arg0, oldVal, newVal) -> Platform.runLater(() -> {
123 | if (!progressBar.isDisabled()) {
124 | long newLong = newVal.longValue();
125 | if (newLong > 0) {
126 | currentStatus.onChange(newLong);
127 | }
128 | else if (newVal.intValue() == -1) {
129 | // failureLabel.setVisible(true);
130 | progressBar.setDisable(true);
131 | displayErrorPopup("Error: Failed to complete imaging.\n");
132 | }
133 | }
134 | });
135 | return new WeakChangeListener<>(progressListener);
136 | }
137 |
138 | private class ProgressStatus {
139 | private int calls;
140 | private double lastAverage;
141 | private final long max;
142 | private final long startTime;
143 |
144 | protected ProgressStatus() {
145 | calls = 0;
146 | lastAverage = 0;
147 | max = workflowController.getMaxOutputCount();
148 | startTime = System.nanoTime();
149 | }
150 |
151 | protected void onChange(long newCount) {
152 | double percentComplete = newCount / (double) max;
153 | calls++;
154 | if (calls % 250 == 0) {
155 | estimateRemainingTime(percentComplete);
156 | }
157 | if (newCount == max) {
158 | timeEstimateLabel.setText("Complete");
159 | setReturnVisible();
160 | }
161 | else if (lastAverage > 0) {
162 | timeEstimateLabel.setText(Math.round((1 - percentComplete) * 100) + "% remaining: " + getTime());
163 | timeEstimateLabel.setVisible(true);
164 | }
165 | progressBar.setProgress(percentComplete);
166 | }
167 |
168 | protected String getTime() {
169 | return Utils.getPrettyTime(Duration.ofSeconds(Math.max(Math.round(lastAverage - (System.nanoTime() - startTime) / 1000000000), 0)));
170 | }
171 |
172 | protected void estimateRemainingTime(double percentComplete) {
173 | double newEstimate = (System.nanoTime() - startTime) / percentComplete / 1000000000;
174 | double estimatedTimeRemaining;
175 | if (lastAverage > 0) {
176 | estimatedTimeRemaining = (newEstimate + lastAverage) / 2;
177 | }
178 | else {
179 | estimatedTimeRemaining = newEstimate;
180 | }
181 | lastAverage = estimatedTimeRemaining;
182 | }
183 | }
184 |
185 | }
186 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/AdvancedOptionsController.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.ui;
2 |
3 | import java.io.File;
4 | import java.net.URL;
5 | import java.util.ArrayList;
6 | import java.util.Collection;
7 | import java.util.List;
8 | import java.util.ResourceBundle;
9 |
10 | import com.ciphertechsolutions.io.applicationLogic.options.AdvancedOptions;
11 |
12 | import javafx.beans.value.ChangeListener;
13 | import javafx.beans.value.ObservableValue;
14 | import javafx.collections.FXCollections;
15 | import javafx.collections.ObservableList;
16 | import javafx.event.ActionEvent;
17 | import javafx.event.Event;
18 | import javafx.fxml.FXML;
19 | import javafx.scene.control.Button;
20 | import javafx.scene.control.ChoiceBox;
21 | import javafx.scene.control.TextArea;
22 | import javafx.scene.control.TextField;
23 | import javafx.stage.DirectoryChooser;
24 |
25 | /**
26 | * The options screen controller.
27 | */
28 | public class AdvancedOptionsController extends BaseController {
29 |
30 | @FXML
31 | private Button saveButton;
32 | @FXML
33 | private Button cancelButton;
34 | @FXML
35 | private Button deleteButton;
36 | @FXML
37 | private ChoiceBox selectConfigDDL;
38 | @FXML
39 | private TextField saveLocationField;
40 | @FXML
41 | private ChoiceBox compressionTypeDDL;
42 | @FXML
43 | private TextField descriptionField;
44 | @FXML
45 | private TextField examinerNameField;
46 | @FXML
47 | private TextField caseNumberField;
48 | @FXML
49 | private TextField evidenceNumberField;
50 | @FXML
51 | private TextArea notesField;
52 | @FXML
53 | private TextField configNameField;
54 |
55 | private boolean wasSaveSet = false;
56 |
57 | private AdvancedOptions selectedConfig;
58 |
59 | /**
60 | * Sole constructor. Initializes with most recently saved configuration.
61 | */
62 | public AdvancedOptionsController() {
63 | super();
64 | getSelectedConfig();
65 | }
66 |
67 | private AdvancedOptions getSelectedConfig() {
68 | if (selectedConfig == null) {
69 | selectedConfig = AdvancedOptions.loadMostRecentConfig();
70 | }
71 | return selectedConfig;
72 | }
73 |
74 | @Override
75 | public void initialize(URL location, ResourceBundle resources) {
76 | setTitle();
77 | populateDDL(compressionTypeDDL, AdvancedOptions.getCompressionTypes());
78 | populateDDL(selectConfigDDL, AdvancedOptions.getConfigNames());
79 | setDisplayToSelectedConfig();
80 | selectConfigDDL.getSelectionModel().selectedItemProperty().addListener(
81 | (ObservableValue extends String> observable, String oldValue, String newValue) -> {
82 | if (newValue != null && !newValue.isEmpty()) {
83 | selectedConfig = AdvancedOptions.loadConfigByName(newValue);
84 | workflowController.setOptions(selectedConfig);
85 | setDisplayToSelectedConfig();
86 | }
87 | });
88 | }
89 |
90 | private void setDisplayToSelectedConfig() {
91 | compressionTypeDDL.getSelectionModel().select(selectedConfig.getCompressionLevelName());
92 | selectConfigDDL.getSelectionModel().select(selectedConfig.getName());
93 | saveLocationField.setText(selectedConfig.getOutputFolder());
94 | descriptionField.setText(selectedConfig.getCaseDescription());
95 | examinerNameField.setText(selectedConfig.getExaminerName());
96 | notesField.setText(selectedConfig.getCaseNotes());
97 | evidenceNumberField.setText(selectedConfig.getEvidenceNumber());
98 | caseNumberField.setText(selectedConfig.getCaseNumber());
99 | configNameField.setText(selectedConfig.getName());
100 | }
101 |
102 | private static void populateDDL(ChoiceBox dropDownList, Collection items) {
103 | List list = new ArrayList<>(items);
104 | ObservableList dropDownItems = FXCollections.observableList(list);
105 | dropDownList.setItems(dropDownItems);
106 | }
107 |
108 | @FXML
109 | void handleOnDeleteAction(ActionEvent event) {
110 | String toDelete = selectedConfig.getName();
111 | AdvancedOptions.deleteConfigByName(toDelete);
112 | selectConfigDDL.getItems().remove(toDelete);
113 | }
114 |
115 | @FXML
116 | void handleOnSaveAction(ActionEvent event) {
117 | String compression = compressionTypeDDL.getSelectionModel().getSelectedItem();
118 | if (compression == null || configNameField.getText().equals("")) {
119 | displayErrorPopup("Unable to save. Compression, and name must be filled out.");
120 |
121 | }
122 | else {
123 | String saveLocation = saveLocationField.getText().endsWith("\\") ? saveLocationField.getText() : saveLocationField.getText() + "\\";
124 | AdvancedOptions options = new AdvancedOptions(configNameField.getText(), saveLocation, compression,
125 | descriptionField.getText(), examinerNameField.getText(), caseNumberField.getText(),
126 | evidenceNumberField.getText(), notesField.getText());
127 | workflowController.saveOptions(options);
128 | selectedConfig = options;
129 | changeScene(loadFXML(MainScreenController.class, MainScreenController.getFXMLLocation()).getScene());
130 | }
131 | }
132 |
133 | @FXML
134 | void openDirectoryChooser(Event event) {
135 | getDirectoryFromChooser();
136 | }
137 |
138 | protected void getDirectoryFromChooser() {
139 | wasSaveSet = true;
140 | DirectoryChooser dirChooser = new DirectoryChooser();
141 | File saveDirectory = dirChooser.showDialog(primaryStage);
142 | if (saveDirectory != null) {
143 | saveLocationField.setText(saveDirectory.getAbsolutePath());
144 | }
145 | }
146 |
147 | @FXML
148 | void handleOnCancelAction(ActionEvent event) {
149 | changeScene(loadFXML(MainScreenController.class, MainScreenController.getFXMLLocation()).getScene());
150 | }
151 |
152 | /**
153 | * Get the location of this controller's corresponding FXML file.
154 | * @return the FXML file locatiion.
155 | */
156 | public static String getFXMLLocation() {
157 | return "fxml/IONOptions.fxml";
158 | }
159 |
160 | @Override
161 | protected void performSetup() {
162 | saveLocationField.focusedProperty().addListener((ChangeListener) (observable, oldValue, newValue) -> {
163 | if (newValue) {
164 | if (!wasSaveSet) {
165 | getDirectoryFromChooser();
166 | }
167 | else {
168 | wasSaveSet = false;
169 | }
170 | }
171 | });
172 | }
173 |
174 | @Override
175 | protected void setTitle() {
176 | setTitle("IO - Advanced Options");
177 | }
178 |
179 | }
180 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/DriveReader.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing;
2 |
3 | import java.io.IOException;
4 | import java.io.RandomAccessFile;
5 | import java.nio.ByteBuffer;
6 | import java.nio.channels.FileChannel;
7 | import java.nio.file.Path;
8 | import java.util.ArrayList;
9 |
10 | import com.ciphertechsolutions.io.device.DeviceManager;
11 | import com.ciphertechsolutions.io.device.Disk;
12 | import com.ciphertechsolutions.io.logging.LogMessageType;
13 | import com.ciphertechsolutions.io.logging.Logging;
14 |
15 | /**
16 | * An implementation of {@link IMediaReader} for reading entire drives. Capable of reading physical drives only.
17 | */
18 | public class DriveReader implements IMediaReader {
19 |
20 | private static final int SECTORS_PER_CHUNK = 256;
21 | private static int SECTOR_SIZE = 512; // TODO: does this need to be variable based on the device?
22 | private static byte[] NULL_SECTOR = new byte[SECTOR_SIZE];
23 | private static final int READ_CHUNK_SIZE = SECTOR_SIZE * SECTORS_PER_CHUNK;
24 | private int currentReadSize = READ_CHUNK_SIZE;
25 | private final FileChannel channel;
26 | private ByteBuffer buffer;
27 | private long bytesRead;
28 | private final long lastSector;
29 | private final long lastSectorBytes; // For faster comparisons
30 | private long resumeNormalRead = 0l;
31 | private static ArrayList badSectorList;
32 | private static final int MAX_PRINTED_WARNINGS = 100;
33 | private boolean hasWarned = false;
34 |
35 | /**
36 | * Constructs a DriveReader to read the drive specified by the given path. The path should be to a physical
37 | * drive (e.g. \\\\.\\PhysicalDrive3).
38 | * @param toRead The path to the drive to read.
39 | * @throws IOException An irrecoverable exception occurred during IO.
40 | */
41 | @SuppressWarnings("resource")
42 | public DriveReader(Path toRead) throws IOException {
43 | channel = new RandomAccessFile(toRead.toFile(), "r").getChannel();
44 | channel.position(0);
45 | buffer = ByteBuffer.allocate(READ_CHUNK_SIZE);
46 | DeviceManager dManager = new DeviceManager();
47 | String diskName = toRead.toString().substring(0, toRead.toString().length() - 1);
48 | Disk disk = dManager.getDeviceByName(diskName);
49 | lastSector = (disk.getSize() / 512);
50 | lastSectorBytes = disk.getSize();
51 | badSectorList = new ArrayList<>();
52 | }
53 |
54 | @Override
55 | public int read() throws IOException {
56 | buffer.clear();
57 | int read = 0;
58 | try {
59 | if (currentReadSize < READ_CHUNK_SIZE && bytesRead >= resumeNormalRead) { // once past bad sector(s), ramp back up
60 | currentReadSize = READ_CHUNK_SIZE; // don't slowly ramp up.
61 | changeReadSize();
62 | }
63 | //Never read past last expected sector, TODO check this doesn't impact speeds
64 | if (channel.position() >= lastSectorBytes) {
65 | return -1;
66 | }
67 | read = channel.read(buffer);
68 | }
69 | catch (IOException e) {
70 | read = buffer.position();
71 | if (read == 0) {
72 | long currentPosition = channel.position();
73 | long currentSector = currentPosition / SECTOR_SIZE;
74 | // Ensure never past last sector
75 | if (currentSector >= lastSector) {
76 | return -1;
77 | }
78 | else if (currentReadSize > SECTOR_SIZE) {
79 | resumeNormalRead = currentPosition + READ_CHUNK_SIZE;
80 | currentReadSize = SECTOR_SIZE;
81 | changeReadSize();
82 | return 0;
83 | }
84 | else if (currentReadSize == SECTOR_SIZE) {
85 | if (currentSector < lastSector && e.getMessage().contains("The drive cannot find the sector requested")) {
86 | logIfNeeded("The drive cannot find the sector #" + currentSector + ". Nulling chunk and advancing to next sector.");
87 | Logging.log("Current Read size: " + currentReadSize, LogMessageType.DEBUG);
88 | read = markBadSectorAndAdvance(currentSector);
89 | }
90 | else if (e.getMessage().contains("Data error")) {
91 | logIfNeeded("Bad sector at sector #" + currentSector + ". Nulling chunk and advancing to next sector.");
92 | read = markBadSectorAndAdvance(currentSector);
93 | }
94 | else if (currentSector < lastSector) {
95 | logIfNeeded("Unknown error at sector #" + currentSector + ". Nulling chunk and advancing to next sector.");
96 | read = markBadSectorAndAdvance(currentSector);
97 | }
98 | else {
99 | close();
100 | return -1;
101 | }
102 | }
103 | else {
104 | // We should never get here.
105 | close();
106 | return -1;
107 | }
108 | }
109 | }
110 | bytesRead += read;
111 | buffer.flip();
112 | return read;
113 | }
114 |
115 | private void logIfNeeded(String warning) {
116 | if (badSectorList.size() > MAX_PRINTED_WARNINGS) {
117 | if (!hasWarned) {
118 | hasWarned = true;
119 | Logging.log("A high number of bad sectors has been detected, individual sector errors will no longer be printed.", LogMessageType.WARNING);
120 | }
121 | }
122 | else {
123 | Logging.log(warning, LogMessageType.WARNING);
124 | }
125 |
126 | }
127 |
128 | protected int markBadSectorAndAdvance(long currentSector) throws IOException {
129 | badSectorList.add(currentSector);
130 | channel.position(channel.position() + SECTOR_SIZE);
131 | buffer.put(NULL_SECTOR);
132 | return SECTOR_SIZE;
133 | }
134 |
135 | @Override
136 | public void close() {
137 | try {
138 | channel.close();
139 | }
140 | catch (IOException e) {
141 | // Well, we tried
142 | Logging.log(e);
143 | }
144 | }
145 |
146 | private void changeReadSize() {
147 | buffer = ByteBuffer.allocate(currentReadSize);
148 | buffer.limit(0);
149 | }
150 |
151 | @Override
152 | public ByteBuffer getBuffer() {
153 | return this.buffer;
154 | }
155 |
156 | @Override
157 | public byte[] getBytes() {
158 | int length = buffer.remaining();
159 | byte[] dest = new byte[length];
160 | System.arraycopy(buffer.array(), 0, dest, 0, length);
161 | return dest;
162 | }
163 |
164 | @Override
165 | public byte[] getUnsafeBytes() {
166 | return buffer.array();
167 | }
168 |
169 | @Override
170 | public long getBytesRead() {
171 | return bytesRead;
172 | }
173 |
174 | // TODO: This is bad code and should not be static.
175 | /**
176 | * Get a list of bad sectors read. This method should be replaced with one that is not static.
177 | * @return The bad sectors.
178 | */
179 | public static ArrayList getBadSectorsList() {
180 | return badSectorList;
181 | }
182 |
183 | @Override
184 | public int getReadSize() {
185 | return READ_CHUNK_SIZE;
186 | }
187 | }
188 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/logging/Logging.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.logging;
2 |
3 | import java.io.PrintStream;
4 | import java.sql.Timestamp;
5 | import java.util.ArrayList;
6 | import java.util.Arrays;
7 | import java.util.Collections;
8 | import java.util.Date;
9 | import java.util.HashSet;
10 | import java.util.List;
11 | import java.util.Set;
12 |
13 | /**
14 | * A class for performing some basic logging functions.
15 | */
16 | public class Logging {
17 |
18 | private static final List LOGGING_PRINT_STREAMS = Collections.synchronizedList(new ArrayList<>());
19 |
20 | static {
21 | LOGGING_PRINT_STREAMS.add(new LoggingStream(System.out, true));
22 | }
23 |
24 | /**
25 | * Adds the given PrintStream and sets it to accept all levels of logging output.
26 | * @param outputStream The stream to print the logging output to.
27 | */
28 | public static void addOutput(PrintStream outputStream) {
29 | LOGGING_PRINT_STREAMS.add(new LoggingStream(outputStream, true));
30 | }
31 |
32 | /**
33 | * Adds the given PrintStream and sets it to accept the given {@link LogMessageType message types}
34 | * @param outputStream The stream to print the logging output to.
35 | * @param types The {@link LogMessageType message types} to print.
36 | */
37 | public static void addOutput(PrintStream outputStream, LogMessageType... types) {
38 | LOGGING_PRINT_STREAMS.add(new LoggingStream(outputStream, types));
39 | }
40 |
41 | /**
42 | * Stops outputting logging to the given PrintStream.
43 | * @param outputStream The PrintStream to cease logging to.
44 | */
45 | public static void removeOutput(PrintStream outputStream) {
46 | LOGGING_PRINT_STREAMS.remove(outputStream);
47 | }
48 |
49 | /**
50 | * Logs the given object by calling toString() on it. {@code null} will not cause a crash,
51 | * but will also not be logged. Log message will include a timestamp.
52 | * @param message
53 | */
54 | public static void log(Object message) {
55 | if (message != null) {
56 | log(message.toString());
57 | }
58 | }
59 |
60 | /**
61 | * Logs the given object by calling toString() on it. {@code null} will not cause a crash,
62 | * but will also not be logged. Log message will include a timestamp. The message will be of type messageType.
63 | * @param message
64 | * @param messageType
65 | */
66 | public static void log(Object message, LogMessageType messageType) {
67 | if (message != null) {
68 | log(message.toString(), messageType);
69 | }
70 | }
71 |
72 | /**
73 | * Logs an exception with a timestamp and relevant details.
74 | * @param exception The exception to log.
75 | */
76 | public static void log(Exception exception) {
77 | for (LoggingStream loggingStream : LOGGING_PRINT_STREAMS) {
78 | if (loggingStream.shouldPrint(LogMessageType.ERROR)) {
79 | @SuppressWarnings("resource")
80 | PrintStream printStream = loggingStream.getStream();
81 | printStream.println(new Timestamp(new Date().getTime()).toString());
82 | printStream.println(exception);
83 | exception.printStackTrace(printStream);
84 | }
85 | }
86 | }
87 |
88 | /**
89 | * Logs an exception with a timestamp and relevant details.
90 | * @param exception The exception to log.
91 | */
92 | public static void log(Throwable throwable) {
93 | for (LoggingStream loggingStream : LOGGING_PRINT_STREAMS) {
94 | if (loggingStream.shouldPrint(LogMessageType.ERROR)) {
95 | @SuppressWarnings("resource")
96 | PrintStream printStream = loggingStream.getStream();
97 | printStream.println(new Timestamp(new Date().getTime()).toString());
98 | printStream.println(throwable);
99 | throwable.printStackTrace(printStream);
100 | }
101 | }
102 | }
103 |
104 | /**
105 | * Logs the given message, with a timestamp.
106 | * @param message
107 | */
108 | public static void log(String message) {
109 | for (LoggingStream loggingStream : LOGGING_PRINT_STREAMS) {
110 | if (loggingStream.shouldPrint(LogMessageType.INFO)) {
111 | loggingStream.getStream().println("[" + new Date() + "]\t" + message);
112 | }
113 | }
114 | }
115 |
116 | /**
117 | * Logs the given message. The message will be of type messageType.
118 | * @param message
119 | * @param messageType the type of the message.
120 | */
121 | public static void logSimple(String message, LogMessageType messageType) {
122 | for (LoggingStream loggingStream : LOGGING_PRINT_STREAMS) {
123 | if (loggingStream.shouldPrint(messageType)) {
124 | loggingStream.getStream().println(message);
125 | }
126 | }
127 | }
128 |
129 | /**
130 | * Logs the given message, with a timestamp. The message will be of type messageType.
131 | * @param message
132 | * @param messageType the type of the message.
133 | */
134 | public static void log(String message, LogMessageType messageType) {
135 | for (LoggingStream loggingStream : LOGGING_PRINT_STREAMS) {
136 | if (loggingStream.shouldPrint(messageType)) {
137 | loggingStream.getStream().println("[" + new Date() + "]\t" + message);
138 | }
139 | }
140 | }
141 |
142 | /**
143 | * Logs the given message, with a timestamp. The message will be of type messageType.
144 | * @param message
145 | * @param messageType the type of the message.
146 | */
147 | public static void log(String message, LogMessageType... messageType) {
148 | for (LoggingStream loggingStream : LOGGING_PRINT_STREAMS) {
149 | if (loggingStream.shouldPrint(messageType)) {
150 | loggingStream.getStream().println("[" + new Date() + "]\t" + message);
151 | }
152 | }
153 | }
154 | }
155 |
156 | class LoggingStream {
157 | private final PrintStream stream;
158 | private final Set types;
159 | private final static Set allTypesSet = new HashSet<>(Arrays.asList(LogMessageType.values()));
160 |
161 | LoggingStream(PrintStream stream, boolean allTypes) {
162 | this.stream = stream;
163 | if (allTypes) {
164 | this.types = allTypesSet;
165 | }
166 | else {
167 | this.types = Collections.emptySet();
168 | }
169 | }
170 |
171 | LoggingStream(PrintStream stream, LogMessageType... types) {
172 | this.stream = stream;
173 | this.types = new HashSet<>(Arrays.asList(types));
174 | }
175 |
176 | boolean shouldPrint(LogMessageType type) {
177 | return this.types.contains(type);
178 | }
179 |
180 | boolean shouldPrint(LogMessageType[] types) {
181 | for (LogMessageType type : types) {
182 | if (this.types.contains(type)) {
183 | return true;
184 | }
185 | }
186 | return false;
187 | }
188 |
189 | PrintStream getStream() {
190 | return this.stream;
191 | }
192 |
193 | @Override
194 | public boolean equals(Object other) {
195 | if (other instanceof LoggingStream) {
196 | return this.stream.equals(((LoggingStream) other).stream);
197 | }
198 | if (other instanceof PrintStream) {
199 | return this.stream.equals(other);
200 | }
201 | return false;
202 | }
203 |
204 | @Override
205 | public int hashCode() {
206 | return this.stream.hashCode();
207 | }
208 | }
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/ui/fxml/IONOptions.fxml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
19 |
23 |
27 |
31 |
35 |
39 |
43 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
74 |
75 |
76 |
77 |
78 |
82 |
87 |
91 |
92 |
93 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/device/Disk.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.device;
2 |
3 | import java.util.HashSet;
4 | import java.util.Set;
5 |
6 | /**
7 | * A class representing Disks.
8 | *
9 | */
10 | public class Disk extends Device implements Comparable {
11 |
12 | /**
13 | * Hardware serial number.
14 | */
15 | public String serial;
16 |
17 | /**
18 | * The model of the physical disk.
19 | */
20 | public String model;
21 |
22 | /**
23 | * The number of heads on the disk.
24 | */
25 | public int heads;
26 |
27 | /**
28 | * The number of cylinders on the disk.
29 | */
30 | public long cylinders;
31 |
32 | /**
33 | * The number of tracks on the disk.
34 | */
35 | public long tracks;
36 |
37 | /**
38 | * The number of sectors on the disk.
39 | */
40 | public long sectors;
41 |
42 | /**
43 | * The number of tracks per disk cylinder.
44 | */
45 | public int tracksPerCylinder;
46 |
47 | /**
48 | * The number of sectors per disk track.
49 | */
50 | public int sectorsPerTrack;
51 |
52 | /**
53 | * The number of bytes per disk sector.
54 | */
55 | public int bytesPerSector;
56 |
57 | /**
58 | * PNP device ID for correlating to USB connectors.
59 | */
60 | public String pnpDeviceID;
61 |
62 | /**
63 | * Physical Device ID
64 | */
65 | public String physicalSerial;
66 |
67 | /**
68 | * VID From USB Device
69 | */
70 | public String VID;
71 |
72 | /**
73 | * Vendor name
74 | */
75 | public String vendorName;
76 |
77 | /**
78 | * PID From USB Device
79 | */
80 | public String PID;
81 |
82 | /**
83 | * The system name for the drive
84 | */
85 | public String systemName;
86 |
87 | /**
88 | * The disk type
89 | */
90 | public String mediaType;
91 |
92 | private final Set volumes = new HashSet<>();
93 |
94 | /**
95 | * Create a new disk object from a management object.
96 | *
97 | * @param info
98 | * The DiskDrive management object.
99 | */
100 | public Disk(String info) {
101 | String[] lines = info.split(System.lineSeparator());
102 | if (lines.length >= 14) {
103 | try {
104 | name = trimPropertyName(lines[0]);
105 | systemName = trimPropertyName(lines[1]);
106 | serial = trimPropertyName(lines[2]).replace("\\x1b", "").trim();
107 | model = trimPropertyName(lines[3]);
108 | path = trimPropertyName(lines[0]); // DeviceID
109 | size = Long.parseLong(trimPropertyName(lines[4]));
110 | heads = Integer.parseInt(trimPropertyName(lines[5]));
111 | cylinders = Long.parseLong(trimPropertyName(lines[6]));
112 | tracks = Long.parseLong(trimPropertyName(lines[7])); // TotalTracks
113 | sectors = Long.parseLong(trimPropertyName(lines[8])); // TotalSectors
114 | tracksPerCylinder = Integer.parseInt(trimPropertyName(lines[9]));
115 | sectorsPerTrack = Integer.parseInt(trimPropertyName(lines[10]));
116 | bytesPerSector = Integer.parseInt(trimPropertyName(lines[11]));
117 | pnpDeviceID = trimPropertyName(lines[12]);
118 | initializePhysicalSerialNumber();
119 | mediaType = trimPropertyName(lines[13]);
120 | }
121 | catch (NumberFormatException e) {
122 | throw new IllegalArgumentException("Invalid disk info");
123 | }
124 | }
125 | else {
126 | throw new IllegalArgumentException("Invalid disk info");
127 | }
128 | }
129 |
130 | private void initializePhysicalSerialNumber() {
131 | String[] pnpPathArray = pnpDeviceID.split("\\\\");
132 | String physSerial = pnpPathArray[pnpPathArray.length - 1];
133 | if (physSerial.startsWith("0&")) {
134 | physicalSerial = physSerial.substring(2);
135 | }
136 | else if (physSerial.endsWith("&0")) {
137 | physicalSerial = physSerial.substring(0, physSerial.length() - 2);
138 | }
139 | else {
140 | physicalSerial = physSerial;
141 | }
142 | }
143 |
144 | /**
145 | * Provides a detailed report on the disk.
146 | */
147 | @Override
148 | public String details() {
149 | StringBuilder sb = new StringBuilder();
150 | sb.append("Disk: " + getName() + System.lineSeparator());
151 | sb.append("Type: " + mediaType + System.lineSeparator());
152 | sb.append("Serial: " + serial + System.lineSeparator());
153 | sb.append("Model: " + model + System.lineSeparator());
154 | sb.append("Size: " + getSize() + System.lineSeparator());
155 | sb.append("PNPDeviceID: " + pnpDeviceID + System.lineSeparator());
156 | sb.append("Physical Serial: " + physicalSerial + System.lineSeparator());
157 | if (VID != null) {
158 | sb.append("Vendor ID: " + VID + System.lineSeparator());
159 | String vendor = VIDLookup.getVendorByVID(VID);
160 | if (vendor == null) {
161 | vendor = vendorName;
162 | }
163 | sb.append("Vendor: " + vendor + System.lineSeparator());
164 | }
165 | if (PID != null) {
166 | sb.append("Product ID: " + PID + System.lineSeparator());
167 | }
168 | sb.append("Geometry:" + System.lineSeparator());
169 | sb.append(" Heads: " + heads + System.lineSeparator());
170 | sb.append(" Cylinders: " + cylinders + System.lineSeparator());
171 | sb.append(" Tracks: " + tracks + System.lineSeparator());
172 | sb.append(" Sectors: " + getSize() / bytesPerSector + System.lineSeparator());
173 | if (this.volumes.size() > 0) {
174 | sb.append("Volumes: " + System.lineSeparator());
175 | for (Volume vol : volumes) {
176 | sb.append(vol.details());
177 | }
178 | }
179 | return sb.toString();
180 | }
181 |
182 | /**
183 | * Associate a volume with this disk.
184 | *
185 | * @param toAdd
186 | * The volume to add.
187 | */
188 | public void addVolume(Volume toAdd) {
189 | this.volumes.add(toAdd);
190 | }
191 |
192 | /**
193 | * Get the volumes associated with this disk.
194 | *
195 | * @return The set of volumes associated with this disk.
196 | */
197 | public Set getVolumes() {
198 | return this.volumes;
199 | }
200 |
201 | @Override
202 | public String toString() {
203 | return model;
204 | }
205 |
206 | @Override
207 | public String toFileNameString() {
208 | String cleanedSerial = serial.replace("\\W+", "");
209 | return model.replaceAll("\\W+", "_") + "_" + cleanedSerial.substring(Math.max(0, cleanedSerial.length() - 5));
210 | }
211 |
212 | /**
213 | * Note: this class has a natural ordering that is inconsistent with equals.
214 | */
215 | @Override
216 | public int compareTo(Device o) {
217 | return this.getName().compareTo(o.getName());
218 | }
219 |
220 | @Override
221 | public int hashCode() {
222 | return this.pnpDeviceID.hashCode();
223 | }
224 |
225 | @Override
226 | public boolean equals(Object o) {
227 | if (o instanceof Disk) {
228 | return equals((Disk) o);
229 | }
230 | return false;
231 | }
232 |
233 | /**
234 | * Determines if this disk is equal to the given disk. Performs a simple
235 | * name check.
236 | *
237 | * @param d
238 | * The disk to compare to
239 | * @return true if the disks are equal, false otherwise.
240 | */
241 | public boolean equals(Disk d) {
242 | return d.pnpDeviceID.equals(this.pnpDeviceID);
243 | }
244 |
245 | @Override
246 | public String getSerialNumber() {
247 | return serial;
248 | }
249 | }
250 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/processing/ProcessorManager.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.processing;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.util.ArrayList;
6 | import java.util.Arrays;
7 | import java.util.List;
8 | import java.util.concurrent.BlockingQueue;
9 | import java.util.concurrent.LinkedBlockingQueue;
10 | import java.util.concurrent.TimeUnit;
11 |
12 | import com.ciphertechsolutions.io.applicationLogic.IStoppable;
13 | import com.ciphertechsolutions.io.applicationLogic.options.AdvancedOptions;
14 | import com.ciphertechsolutions.io.device.Device;
15 | import com.ciphertechsolutions.io.logging.LogMessageType;
16 | import com.ciphertechsolutions.io.logging.Logging;
17 | import com.ciphertechsolutions.io.processing.digests.Md5Digest;
18 | import com.ciphertechsolutions.io.processing.digests.SHA1Digest;
19 | import com.ciphertechsolutions.io.processing.triage.MagicCarver;
20 |
21 | import javafx.beans.property.LongProperty;
22 | import javafx.beans.property.SimpleLongProperty;
23 | import javafx.beans.value.ChangeListener;
24 |
25 | /**
26 | * This class manages ION's imaging process.
27 | */
28 | public class ProcessorManager implements IStoppable {
29 |
30 | private final IMediaReader toProcess;
31 | private final String baseFileName;
32 | private final List processors;
33 | private final BlockingQueue queue;
34 | private boolean isRunning = true;
35 | private Device device;
36 | /**
37 | * How much of {@link #toProcess} has been read.
38 | */
39 | private final LongProperty status;
40 | private final AdvancedOptions options;
41 |
42 | /**
43 | * Creates a new ProcessorManager to process the given device using the given IMediaReader.
44 | * @param toProcess The IMediaReader to read from.
45 | * @param device The device that is being read.
46 | * @param options The application options - may or may not be used depending on the processors added.
47 | * @param baseFileName A unique file name base to use - may or may not be used depending on the processors added.
48 | */
49 | public ProcessorManager(IMediaReader toProcess, Device device, AdvancedOptions options, String baseFileName) {
50 | this(toProcess, options, baseFileName);
51 | this.device = device;
52 | }
53 |
54 | private ProcessorManager(IMediaReader toProcess, AdvancedOptions options, String baseFileName, IProcessor... processors) {
55 | this.toProcess = toProcess;
56 | this.processors = new ArrayList<>(); // TODO: Thread safe list here instead?
57 | this.processors.addAll(Arrays.asList(processors));
58 | this.queue = new LinkedBlockingQueue<>();
59 | this.status = new SimpleLongProperty(0l);
60 | this.options = options;
61 | this.baseFileName = baseFileName;
62 | }
63 |
64 | /**
65 | * Adds ION's default processing suite: {@link ChunkedCompressor compression}, {@link Md5Digest MD5 digest},
66 | * {@link SHA1Digest SHA1 digest}, {@link MagicCarver magic carving}, and {@link EWFOutput outputting to Encase6}.
67 | */
68 | public void addDefaultProcessors() {
69 | ChunkedCompressor chunker = new ChunkedCompressor(options.getCompressionLevel());
70 | addProcessor(chunker);
71 | Md5Digest md5Digest = new Md5Digest();
72 | addProcessor(md5Digest);
73 | SHA1Digest shaDigest = new SHA1Digest();
74 | addProcessor(shaDigest);
75 | try {
76 | addProcessor(new MagicCarver(toProcess.getReadSize()));
77 | addProcessor(new EWFOutput(device, chunker.getOutputQueue(),
78 | new File(baseFileName + ".E01"), md5Digest.getDigestResult(), shaDigest.getDigestResult(), options));
79 | }
80 | catch (IOException e) {
81 | Logging.log(e);
82 | }
83 | }
84 |
85 | /**
86 | * Adds the given {@link IProcessor processor} so that it will be run during {@link #process()}.
87 | * To have any impact this method must be called before {@link #process()} is called.
88 | * @param toAdd The {@link IProcessor processor} to add.
89 | */
90 | public void addProcessor(IProcessor toAdd) {
91 | processors.add(toAdd);
92 | }
93 |
94 | /**
95 | * Removes the given {@link IProcessor processor} so that it will not be run during {@link #process()}.
96 | * If kill is true, it will attempt to end the process.
97 | * @param toRemove The processor to remove.
98 | * @param kill If true, {@link IProcessor#cancel()} will be called on the given processor if it was associated with this ProcessManager.
99 | * @return true if the processor was present, false otherwise.
100 | */
101 | public boolean removeProcessor(IProcessor toRemove, boolean kill) {
102 | int index = processors.indexOf(toRemove);
103 | if (kill && index != -1) {
104 | processors.get(index).cancel();
105 | processors.remove(index);
106 | return true;
107 | }
108 | return index != -1;
109 | }
110 |
111 | /**
112 | * Adds the given ChangeListener to listen on {@link #status} which records the progress in processing the IMediaReader.
113 | * @param listener The listener to add.
114 | */
115 | public void addProgressMonitor(ChangeListener listener) {
116 | status.addListener(listener);
117 | }
118 |
119 | /**
120 | * Processes the {@link IMediaReader} associated with this ProcessorManager using the associated {@link IProcessor IProcessors}.
121 | * {@link IProcessor#initialize()} will be called on all IProcessors, then reading from the IMediaReader will begin. Once
122 | * all data has been read, {@link IProcessor#finish()} will be called on all IProcessors, then {@link IProcessor#waitForExit()}
123 | * will be called on all IProcessors.
124 | */
125 | public void process() {
126 | initializeProcessors();
127 | Logging.log("Starting.", LogMessageType.USER);
128 | logCaseInfo();
129 | logDeviceInfo();
130 | beginReading();
131 | long reads = 0l;
132 | try {
133 | while (isRunning && !Thread.currentThread().isInterrupted()) {
134 | byte[] toRead = queue.poll(5, TimeUnit.SECONDS);
135 | if (toRead != null) {
136 | if (toRead.length > 0) {
137 | reads++;
138 | status.set(status.get() + toRead.length);
139 | if (reads % 5000 == 0) {
140 | Logging.log("Read " + status.get() + " bytes.", LogMessageType.INFO);
141 | }
142 | for (IProcessor processor : processors) {
143 | processor.process(toRead);
144 | }
145 | }
146 | else {
147 | isRunning = false;
148 | }
149 | }
150 | }
151 | }
152 | catch (InterruptedException e) {
153 | Logging.log(e);
154 | }
155 | for (IProcessor processor : processors) {
156 | processor.finish();
157 | }
158 | for (IProcessor processor : processors) {
159 | processor.waitForExit();
160 | }
161 | status.set(status.get() + 1);
162 | }
163 |
164 | private void logCaseInfo() {
165 | Logging.log("Imaging Options:", LogMessageType.REPORT, LogMessageType.USER);
166 | String toLog = options.getCaseNumber();
167 | logIfAvailable("Case Number: ", toLog);
168 | toLog = options.getExaminerName();
169 | logIfAvailable("Examiner: ", toLog);
170 | toLog = options.getEvidenceNumber();
171 | logIfAvailable("Evidence Number: ", toLog);
172 | toLog = options.getCaseDescription();
173 | logIfAvailable("Case Description: ", toLog);
174 | toLog = options.getCaseNotes();
175 | logIfAvailable("Case Notes: ", toLog);
176 | Logging.log("Compression algorithm: zlib DEFLATE", LogMessageType.REPORT, LogMessageType.USER);
177 | toLog = options.getCompressionLevelName();
178 | logIfAvailable("Compression Level: ", toLog);
179 | toLog = "" + options.getCompressionLevel();
180 | logIfAvailable("Compression Level (numeric): ", toLog);
181 | }
182 |
183 | private void logIfAvailable(String label, String toLog) {
184 | if (toLog != null && !toLog.isEmpty()) {
185 | Logging.log(label + toLog, LogMessageType.REPORT, LogMessageType.USER);
186 | }
187 | }
188 |
189 | private void logDeviceInfo() {
190 | Logging.log("Device Info:", LogMessageType.REPORT, LogMessageType.USER);
191 | Logging.log(device.details(), LogMessageType.REPORT, LogMessageType.USER);
192 | }
193 |
194 | private void beginReading() {
195 | Thread readingThread = new Thread((Runnable) () -> {
196 | int read = 0;
197 | while (isRunning && read >= 0) {
198 | try {
199 | read = toProcess.read();
200 | if (read > 0) {
201 | byte[] bytes = toProcess.getBytes();
202 | queue.add(bytes);
203 | }
204 | }
205 | catch (IOException e1) {
206 | read = -1;
207 | Logging.log(e1);
208 | }
209 | }
210 | queue.add(new byte[0]);
211 | try {
212 | toProcess.close();
213 | }
214 | catch (Exception e2) {
215 | Logging.log(e2);
216 | }
217 | } , "DeviceReading");
218 | readingThread.start();
219 | }
220 |
221 | private void initializeProcessors() {
222 | for (IProcessor processor : processors) {
223 | processor.initialize();
224 | }
225 | }
226 |
227 | @Override
228 | public void stop() {
229 | isRunning = false;
230 | }
231 | }
232 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/applicationLogic/options/AdvancedOptions.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.applicationLogic.options;
2 |
3 | import java.io.File;
4 | import java.io.FileOutputStream;
5 | import java.io.FileReader;
6 | import java.io.IOException;
7 | import java.nio.file.Files;
8 | import java.nio.file.Path;
9 | import java.nio.file.Paths;
10 | import java.util.ArrayList;
11 | import java.util.Collections;
12 | import java.util.List;
13 | import java.util.Optional;
14 | import java.util.Properties;
15 | import java.util.Set;
16 | import java.util.stream.Collectors;
17 |
18 | import com.ciphertechsolutions.io.logging.LogMessageType;
19 | import com.ciphertechsolutions.io.logging.Logging;
20 |
21 | /**
22 | * Contains the configurable options for ION.
23 | */
24 | public class AdvancedOptions extends Properties {
25 |
26 | private static final long serialVersionUID = -4595476431254975311L;
27 |
28 | private final static List COMPRESSION_TYPES = initializeCompressionTypes();
29 |
30 | private final static String BASE_CONFIG_STRING = ".\\Data\\Configs\\";
31 | private final static String ERROR_LOADING_MSG = "Error loading config, loading default config instead.";
32 | private final static String ERROR_SAVING_MSG = "Error saving config, config was not saved.";
33 | /**
34 | * An AdvancedOptions containing the default configuration.
35 | */
36 | public static final AdvancedOptions DEFAULT_OPTIONS = getDefaultConfig();
37 | private String name;
38 |
39 | /**
40 | * Creates a new {@link AdvancedOptions} by loading the most recent config from disk.
41 | */
42 | public AdvancedOptions() {
43 | this(true);
44 | }
45 |
46 | private AdvancedOptions(boolean loadMostRecent) {
47 | if (loadMostRecent){
48 | this.putAll(loadMostRecentConfig());
49 | }
50 | }
51 | /**
52 | * Requires constructor and ConfigOptionsEnum to be in sync.
53 | * @param string
54 | */
55 | public AdvancedOptions(String name, String saveLocation, String compressionType, String description, String examiner, String caseNum, String evidence, String note) {
56 | this.name = name;
57 | List initOptions = new ArrayList<>();
58 | initOptions.add(compressionType);
59 | initOptions.add(saveLocation);
60 | initOptions.add(description);
61 | initOptions.add(examiner);
62 | initOptions.add(caseNum);
63 | initOptions.add(evidence);
64 | initOptions.add(note);
65 | int index = 0;
66 | for (ConfigOptionsEnum option : ConfigOptionsEnum.values()) {
67 | setProperty(option.getDisplayName(), initOptions.get(index));
68 | index++;
69 | }
70 | }
71 |
72 | /**
73 | * Creates a new {@link AdvancedOptions} using the defaults from {@link ConfigOptionsEnum}.
74 | * @return A new AdvancedOptions.
75 | */
76 | public static AdvancedOptions getDefaultConfig() {
77 | AdvancedOptions defaultConfig = new AdvancedOptions(false);
78 | defaultConfig.initToDefaults();
79 | defaultConfig.name = "Default";
80 | return defaultConfig;
81 |
82 | }
83 |
84 | private void initToDefaults() {
85 | for (ConfigOptionsEnum option : ConfigOptionsEnum.values()) {
86 | setProperty(option.getDisplayName(), option.getDefaultValue());
87 | }
88 | }
89 |
90 | /**
91 | * Saves these options to disk, with the given file name.
92 | * @param name The name to save as.
93 | * @return True if the save was successful, false if there was an error.
94 | */
95 | public boolean saveConfig(String name) {
96 | File saveFile = Paths.get(BASE_CONFIG_STRING + name).toFile();
97 | saveFile.getParentFile().mkdirs();
98 | try {
99 | saveFile.createNewFile();
100 | FileOutputStream writeTo = new FileOutputStream(saveFile);
101 | this.store(writeTo, "");
102 | writeTo.close();
103 | return true;
104 | } catch (IOException e) {
105 | Logging.log(ERROR_SAVING_MSG, LogMessageType.ERROR);
106 | Logging.log(e);
107 | }
108 | return false;
109 |
110 | }
111 |
112 | /**
113 | * Loads the most recently saved configuration from disk. On error will return the default config instead.
114 | * @return An AdvancedOptions containing the most recently saved configuration.
115 | */
116 | public static AdvancedOptions loadMostRecentConfig() {
117 | AdvancedOptions config = null;
118 | Optional lastFilePath;
119 | try {
120 | Path configDir = Paths.get(BASE_CONFIG_STRING);
121 | if (configDir.toFile().isDirectory()) {
122 | lastFilePath = Files.list(configDir) // get the stream with full directory listing
123 | .filter(f -> Files.isDirectory(f) == false) // exclude subdirectories from listing
124 | .max((f1, f2) -> (int)(f1.toFile().lastModified() - f2.toFile().lastModified()));
125 |
126 | if ( lastFilePath.isPresent() ) // folder may be empty
127 | {
128 | config = new AdvancedOptions(false);
129 | config.initConfigByPath(lastFilePath.get());
130 | }
131 | }
132 | } catch (IOException e) {
133 | Logging.log(ERROR_LOADING_MSG, LogMessageType.ERROR);
134 | Logging.log(e);
135 | }
136 | //Otherwise get default
137 | if(config == null) {
138 | config = AdvancedOptions.getDefaultConfig();
139 | }
140 | return config;
141 | }
142 |
143 | /**
144 | * Loads a configuration by name from disk. On error returns the default config instead.
145 | * @param fileName The name of the configuration to load.
146 | * @return The loaded configuration.
147 | */
148 | public static AdvancedOptions loadConfigByName(String fileName) {
149 | AdvancedOptions config = null;
150 | try {
151 | Path configDir = Paths.get(BASE_CONFIG_STRING);
152 | if (configDir.toFile().isDirectory()) {
153 | Optional toLoad = Files.list(configDir).filter(path -> path.getFileName().toString().equals(fileName)).findAny();
154 | if ( toLoad.isPresent() ) // folder may be empty
155 | {
156 | config = new AdvancedOptions(false);
157 | config.initConfigByPath(toLoad.get());
158 | }
159 | }
160 | } catch (IOException e) {
161 | Logging.log(ERROR_LOADING_MSG, LogMessageType.ERROR);
162 | Logging.log(e);
163 | }
164 | //Otherwise get default
165 | if(config == null) {
166 | config = AdvancedOptions.getDefaultConfig();
167 | }
168 | return config;
169 | }
170 |
171 | /**
172 | * Deletes a configuration with the given name from disk.
173 | * @param fileName The name of the configuration to delete.
174 | */
175 | public static void deleteConfigByName(String fileName) {
176 | try {
177 | Path configDir = Paths.get(BASE_CONFIG_STRING);
178 | if (configDir.toFile().isDirectory()) {
179 | Optional toLoad = Files.list(configDir).filter(path -> path.getFileName().toString().equals(fileName)).findAny();
180 | if ( toLoad.isPresent() ) // folder may be empty
181 | {
182 | toLoad.get().toFile().delete();
183 | }
184 | }
185 | } catch (IOException e) {
186 | Logging.log("Failed to delete configuration " + fileName, LogMessageType.ERROR);
187 | Logging.log(e);
188 | }
189 | }
190 |
191 | /**
192 | * Get a set of all configuration names present on disk.
193 | * @return A set containing all available configurations.
194 | */
195 | public static Set getConfigNames() {
196 | Path configDir = Paths.get(BASE_CONFIG_STRING);
197 | if (configDir.toFile().isDirectory()) {
198 | try {
199 | return Files.list(configDir).filter(f -> Files.isDirectory(f) == false).
200 | map(A -> A.getFileName().toString()).collect(Collectors.toSet());
201 | }
202 | catch (IOException e) {
203 | Logging.log(e);
204 | }
205 | }
206 | return Collections.emptySet();
207 | }
208 |
209 | private void initConfigByPath(Path configPath) {
210 | try (FileReader configReader = new FileReader(configPath.toFile())){
211 | load(configReader);
212 | name = configPath.getFileName().toString();
213 | } catch (IOException e) {
214 | initToDefaults();
215 | Logging.log(ERROR_LOADING_MSG, LogMessageType.ERROR);
216 | Logging.log(e);
217 | }
218 | }
219 |
220 | private static List initializeCompressionTypes() {
221 | List compressionTypes = new ArrayList<>();
222 | for (CompressionTypesEnum compression : CompressionTypesEnum.values()) {
223 | compressionTypes.add(compression.getDisplayName());
224 | }
225 |
226 | return compressionTypes;
227 | }
228 |
229 | /**
230 | * Gets the name of this AdvancedOptions.
231 | * @return The name.
232 | */
233 | public String getName() {
234 | return this.name;
235 | }
236 |
237 | /**
238 | * Get the output folder location.
239 | * @return The output folder.
240 | */
241 | public String getOutputFolder() {
242 | return this.getProperty(ConfigOptionsEnum.OutputFolder.getDisplayName(), "./");
243 | }
244 |
245 | /**
246 | * Get the desired compression level.
247 | * @return The compression level.
248 | */
249 | public int getCompressionLevel() {
250 | return CompressionTypesEnum.getLevelByName(this.getProperty(ConfigOptionsEnum.CompressionType.getDisplayName()));
251 | }
252 |
253 | /**
254 | * Get the display-friendly name of the compression level.
255 | * @return The display-friendly name of compression level.
256 | */
257 | public String getCompressionLevelName() {
258 | return this.getProperty(ConfigOptionsEnum.CompressionType.getDisplayName());
259 | }
260 |
261 | /**
262 | * Get the case number.
263 | * @return The case number.
264 | */
265 | public String getCaseNumber() {
266 | return getProperty(ConfigOptionsEnum.CaseNumber.getDisplayName());
267 | }
268 |
269 | /**
270 | * Get the case description.
271 | * @return The case description.
272 | */
273 | public String getCaseDescription() {
274 | return getProperty(ConfigOptionsEnum.Description.getDisplayName());
275 | }
276 |
277 | /**
278 | * Get the examiner name.
279 | * @return The examiner name.
280 | */
281 | public String getExaminerName() {
282 | return getProperty(ConfigOptionsEnum.ExaminerName.getDisplayName());
283 | }
284 |
285 | /**
286 | * Get the case notes.
287 | * @return The case notes.
288 | */
289 | public String getCaseNotes() {
290 | return getProperty(ConfigOptionsEnum.Notes.getDisplayName());
291 | }
292 |
293 | /**
294 | * Get the evidence number
295 | * @return The evidence number.
296 | */
297 | public String getEvidenceNumber() {
298 | return getProperty(ConfigOptionsEnum.EvidenceNumber.getDisplayName());
299 | }
300 |
301 | /**
302 | * Get a list containing the possible compression type settings, in display-friendly string form.
303 | * @return The compression types.
304 | */
305 | public static List getCompressionTypes() {
306 | return COMPRESSION_TYPES;
307 | }
308 |
309 | private enum ConfigOptionsEnum {
310 |
311 | CompressionType("compressionType", CompressionTypesEnum.getDefaultCompressionType().getDisplayName()),
312 | OutputFolder("outputFolder", "./_ForensicImages/"),
313 | Description("description", ""),
314 | ExaminerName("examinerName", ""),
315 | CaseNumber("caseNumber", ""),
316 | EvidenceNumber("evidenceNumber", ""),
317 | Notes("notes", "");
318 |
319 | String displayName;
320 | String defaultValue;
321 |
322 | ConfigOptionsEnum(String name, String value) {
323 | displayName = name;
324 | defaultValue = value;
325 | }
326 |
327 | public String getDisplayName() {
328 | return displayName;
329 | }
330 |
331 | public String getDefaultValue() {
332 | return defaultValue;
333 | }
334 | }
335 |
336 |
337 | }
338 |
--------------------------------------------------------------------------------
/src/com/ciphertechsolutions/io/applicationLogic/ProcessController.java:
--------------------------------------------------------------------------------
1 | package com.ciphertechsolutions.io.applicationLogic;
2 |
3 | import java.io.File;
4 | import java.io.IOException;
5 | import java.io.PrintStream;
6 | import java.math.RoundingMode;
7 | import java.text.DecimalFormat;
8 | import java.text.SimpleDateFormat;
9 | import java.util.ArrayList;
10 | import java.util.Collection;
11 | import java.util.Date;
12 | import java.util.Set;
13 |
14 | import com.ciphertechsolutions.io.applicationLogic.options.AdvancedOptions;
15 | import com.ciphertechsolutions.io.device.Device;
16 | import com.ciphertechsolutions.io.device.DeviceManager;
17 | import com.ciphertechsolutions.io.device.Disk;
18 | import com.ciphertechsolutions.io.logging.LogMessageType;
19 | import com.ciphertechsolutions.io.logging.Logging;
20 | import com.ciphertechsolutions.io.logging.ObservableOutputStream;
21 | import com.ciphertechsolutions.io.processing.DriveReader;
22 | import com.ciphertechsolutions.io.processing.ProcessorManager;
23 | import com.ciphertechsolutions.io.ui.BaseController;
24 | import com.ciphertechsolutions.io.ui.ImagingController;
25 | import com.ciphertechsolutions.io.ui.MainScreenController;
26 | import com.ciphertechsolutions.io.usb.USBPoller;
27 | import com.ciphertechsolutions.io.usb.UsbWriteBlock;
28 |
29 | import javafx.application.Platform;
30 | import javafx.beans.value.ChangeListener;
31 | import javafx.collections.ListChangeListener;
32 | import javafx.util.StringConverter;
33 |
34 | /**
35 | * The overall application logic controller for ION. This provides ION's default implementation of
36 | * {@link IProcessController}.
37 | */
38 | public class ProcessController implements IProcessController {
39 |
40 | private final ApplicationState state;
41 | private final DeviceManager deviceManager;
42 | private Thread processingThread;
43 | private final Collection toTerminate;
44 | private ProcessorManager activeImaging;
45 | private final ObservableOutputStream outputStream;
46 | private final USBPoller poller;
47 | private boolean aborted = false;
48 |
49 | /**
50 | * Sole constructor. Calling this will create an instance of {@link USBPoller} and call
51 | * {@link USBPoller#startPolling()} on it.
52 | * {@link UsbWriteBlock#enable()} will also be triggered.
53 | */
54 | public ProcessController() {
55 | outputStream = new ObservableOutputStream();
56 | state = new ApplicationState();
57 | deviceManager = new DeviceManager();
58 | toTerminate = new ArrayList<>();
59 | poller = new USBPoller(this);
60 | toTerminate.add(poller);
61 | UsbWriteBlock.enable();
62 | poller.startPolling();
63 | Logging.addOutput(new PrintStream(outputStream, true), LogMessageType.ERROR, LogMessageType.WARNING, LogMessageType.USER);
64 | }
65 |
66 | @Override
67 | public boolean checkValidSaveDirectory() {
68 | File baseDir = new File(getBaseDirectory());
69 | return (baseDir.exists() && baseDir.isDirectory() && baseDir.canWrite()) || baseDir.mkdirs();
70 | }
71 |
72 | @Override
73 | public Set getAvailableDisks() {
74 | return deviceManager.getCurrentDisks();
75 | }
76 |
77 | @Override
78 | public Disk findNewDisk() {
79 | Disk disk = deviceManager.scanForInsertedDisk();
80 | if (state.getSelectedDevice() != null) {
81 | if (deviceManager.getDeviceByName(state.getSelectedDevice().getName()) == null) {
82 | // TODO: Not this way. This code is terrible.
83 | ImagingController imagingController = (ImagingController) BaseController.getCurrentController();
84 | stopImaging(() -> imagingController.abortImaging());
85 | }
86 | }
87 | return disk;
88 | }
89 |
90 | @Override
91 | public void selectDevice(Device device) {
92 | state.setSelectedDevice(device);
93 | }
94 |
95 | @Override
96 | public boolean hasDevice() {
97 | return state.getSelectedDevice() != null;
98 | }
99 |
100 | @Override
101 | public boolean setupImaging() {
102 | aborted = false;
103 | Device device = state.getSelectedDevice();
104 | if (device == null) {
105 | return false;
106 | }
107 | final String baseName = setupDirectory();
108 | DriveReader reader;
109 | try {
110 | reader = new DriveReader(device.getPath());
111 | activeImaging = new ProcessorManager(reader, device, state.getOptions(), baseName);
112 | }
113 | catch (IOException e) {
114 | Logging.log(e);
115 | return false;
116 | }
117 | processingThread = new Thread(new Runnable() {
118 |
119 | @Override
120 | public void run() {
121 | try {
122 | PrintStream reportStream = setupReportStream(baseName);
123 | PrintStream csvStream = setupCSVStream(baseName);
124 | toTerminate.add(activeImaging);
125 | activeImaging.addDefaultProcessors();
126 | activeImaging.process();
127 | if (aborted) {
128 | logBadSectors(DriveReader.getBadSectorsList());
129 | Logging.log("Process aborted.", LogMessageType.REPORT, LogMessageType.USER);
130 | }
131 | else {
132 | logBadSectors(DriveReader.getBadSectorsList());
133 | Logging.log("Imaging Completed Successfully.", LogMessageType.REPORT, LogMessageType.USER);
134 | }
135 | Logging.removeOutput(csvStream);
136 | csvStream.close();
137 | Logging.removeOutput(reportStream);
138 | reportStream.close();
139 | state.setSelectedDevice(null);
140 | }
141 | catch (IOException e) {
142 | Logging.log(e);
143 | }
144 | }
145 |
146 | private void logBadSectors(ArrayList badSectorsList) {
147 | StringBuilder sb = new StringBuilder("Bad sectors found at the following sectors: \n");
148 | for (Long sector : badSectorsList) {
149 | sb.append(sector + "\n");
150 | }
151 | if (badSectorsList.size() > 0) {
152 | Logging.log(sb.toString(), LogMessageType.REPORT);
153 | }
154 |
155 | }
156 |
157 | protected PrintStream setupReportStream(final String baseName) throws IOException {
158 | File reportFile = new File(baseName + "_report.txt");
159 | reportFile.createNewFile();
160 | PrintStream reportStream = new PrintStream(reportFile);
161 | Logging.addOutput(reportStream, LogMessageType.REPORT);
162 | return reportStream;
163 | }
164 |
165 | protected PrintStream setupCSVStream(final String baseName) throws IOException {
166 | File csvFile = new File(baseName + "_gps.csv");
167 | csvFile.createNewFile();
168 | PrintStream csvStream = new PrintStream(csvFile);
169 | Logging.addOutput(csvStream, LogMessageType.GPS);
170 | Logging.logSimple("Camera Make, Camera Model, Latitude, Longitude, Altitude, Altitude Reference, Time Stamp", LogMessageType.GPS);
171 | return csvStream;
172 | }
173 | }, "ProcessingMain");
174 | outputStream.clear();
175 | return true;
176 | }
177 |
178 | @Override
179 | public void beginImaging() {
180 | processingThread.start();
181 | }
182 |
183 | protected String setupDirectory() {
184 | String baseName = getBaseDirectory() + state.getSelectedDevice().toFileNameString() + "_" +
185 | new SimpleDateFormat("yyyyMMddHHmm").format(new Date()) + "/";
186 | new File(baseName).mkdirs();
187 | baseName += state.getSelectedDevice().toFileNameString();
188 | return baseName;
189 | }
190 |
191 | protected String getBaseDirectory() {
192 | String outputFolder = state.getOptions().getOutputFolder();
193 | if (outputFolder == null || outputFolder.equals("")) {
194 | outputFolder = "./";
195 | }
196 | return outputFolder;
197 | }
198 |
199 | @Override
200 | public void registerOutputListener(ListChangeListener listener) {
201 | outputStream.addListener(listener);
202 | }
203 |
204 | @Override
205 | public void registerProgressListener(ChangeListener listener) {
206 | activeImaging.addProgressMonitor(listener);
207 | }
208 |
209 | @Override
210 | public void registerWriteBlockListener(ChangeListener listener) {
211 | poller.addWriteBlockListener(listener);
212 | }
213 |
214 | @Override
215 | public long getMaxOutputCount() {
216 | return state.getSelectedDevice().getSize() + 1;
217 | }
218 |
219 | @Override
220 | public BaseController getController() {
221 | return BaseController.loadFXML(MainScreenController.getFXMLLocation(), MainScreenController.class);
222 | }
223 |
224 | @Override
225 | public StringConverter getStringConverterForDisks() {
226 | return new NameStringConverter(deviceManager);
227 | }
228 |
229 | private static class NameStringConverter extends StringConverter {
230 | private final DeviceManager manager;
231 |
232 | public NameStringConverter(DeviceManager manager) {
233 | this.manager = manager;
234 | }
235 |
236 | @Override
237 | public Disk fromString(String name) {
238 | if (name == null || name == "" || !name.contains(":")) {
239 | return null;
240 | }
241 | return manager.getDeviceByName(name.split(":", 1)[0]);
242 | }
243 |
244 | @Override
245 | public String toString(Disk disk) {
246 | if (disk == null) {
247 | return "";
248 | }
249 | double bytesInGB = disk.getSize() / 1073741824.0;
250 | DecimalFormat df = new DecimalFormat("#.##");
251 | df.setRoundingMode(RoundingMode.CEILING);
252 | return disk.getName() + ": " + disk.model + " [" + df.format(bytesInGB) + "GB]";
253 |
254 | }
255 | }
256 |
257 | @Override
258 | public void handleDiskInsertion(Disk insertedDisk) {
259 | if (state.getSelectedDevice() != null) {
260 | return;
261 | }
262 | else {
263 | Platform.runLater(() -> {
264 | state.setSelectedDevice(insertedDisk);
265 | setupImaging();
266 | // TODO: Not this way. This code is terrible.
267 | ImagingController imagingController = BaseController.getCurrentController().loadFXML(ImagingController.class,
268 | ImagingController.getFXMLLocation());
269 | BaseController.changeScene(imagingController.getScene());
270 | beginImaging();
271 | });
272 | }
273 | }
274 |
275 | @Override
276 | public void stop() {
277 | aborted = true;
278 | for (IStoppable toStop : toTerminate) {
279 | toStop.stop();
280 | }
281 | }
282 |
283 | @Override
284 | public void stopImaging(Runnable callback) {
285 | aborted = true;
286 | activeImaging.stop();
287 | try {
288 | processingThread.join();
289 | }
290 | catch (InterruptedException e) {
291 | // Nothing to do here.
292 | }
293 | if (callback != null) {
294 | callback.run();
295 | }
296 | }
297 |
298 | @Override
299 | public void saveOptions(AdvancedOptions options) {
300 | state.setOptions(options);
301 | options.saveConfig(options.getName());
302 | Logging.log("Save successful, config selected.", LogMessageType.INFO);
303 | }
304 |
305 | @Override
306 | public void setOptions(AdvancedOptions options) {
307 | state.setOptions(options);
308 | }
309 | }
310 |
--------------------------------------------------------------------------------