From b38aa6636f4825f92a25a13057603ee601623029 Mon Sep 17 00:00:00 2001 From: Ganesh <65601315+ganeshbs17@users.noreply.github.com> Date: Fri, 14 Aug 2026 01:46:50 +0530 Subject: [PATCH 1/5] Pin jna 5.19.1 in CoreLibs to match the artifact Ivy retrieves Ivy conflict resolution retrieves only jna-5.19.1.jar into CoreLibs/release/modules/ext, but project.xml and project.properties still referenced jna-5.18.1.jar, so a clean build failed with com.sun.jna.Pointer not found. Co-Authored-By: Claude Fable 5 --- CoreLibs/nbproject/project.properties | 2 +- CoreLibs/nbproject/project.xml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CoreLibs/nbproject/project.properties b/CoreLibs/nbproject/project.properties index 822438de683..b0815cf0b1d 100644 --- a/CoreLibs/nbproject/project.properties +++ b/CoreLibs/nbproject/project.properties @@ -120,7 +120,7 @@ file.reference.jericho-html-3.4.jar=release/modules/ext/jericho-html-3.4.jar file.reference.jfxtras-common-17-r1.jar=release/modules/ext/jfxtras-common-17-r1.jar file.reference.jfxtras-controls-17-r1.jar=release/modules/ext/jfxtras-controls-17-r1.jar file.reference.jfxtras-fxml-17-r1.jar=release/modules/ext/jfxtras-fxml-17-r1.jar -file.reference.jna-5.18.1.jar=release/modules/ext/jna-5.18.1.jar +file.reference.jna-5.19.1.jar=release/modules/ext/jna-5.19.1.jar file.reference.jna-platform-5.18.1.jar=release/modules/ext/jna-platform-5.18.1.jar file.reference.joda-time-2.13.0.jar=release/modules/ext/joda-time-2.13.0.jar file.reference.jsr305-3.0.2.jar=release/modules/ext/jsr305-3.0.2.jar diff --git a/CoreLibs/nbproject/project.xml b/CoreLibs/nbproject/project.xml index fd76fc53338..12218dfe193 100755 --- a/CoreLibs/nbproject/project.xml +++ b/CoreLibs/nbproject/project.xml @@ -1049,8 +1049,8 @@ release/modules/ext/jfxtras-fxml-17-r1.jar - ext/jna-5.18.1.jar - release/modules/ext/jna-5.18.1.jar + ext/jna-5.19.1.jar + release/modules/ext/jna-5.19.1.jar ext/jna-platform-5.18.1.jar From b3920c20d27b8a347adbf9daa1c02dfdada0a777 Mon Sep 17 00:00:00 2001 From: Ganesh <65601315+ganeshbs17@users.noreply.github.com> Date: Fri, 14 Aug 2026 01:47:01 +0530 Subject: [PATCH 2/5] Per-volume BitLocker password fields in the Add Data Source wizard When validation of a disk image fails because BitLocker volumes are locked, the image file panel now shows one labeled password field per locked volume (recovery key GUID and volume byte offset parsed from the TestOpenImageResult message) in addition to the existing single password field. All entered passwords are pooled into a candidate list and passed through the new List overloads of SleuthkitJNI.testOpenImage, SleuthkitJNI.addImageToDatabase and SleuthkitCase.makeAddImageProcess, so images whose volumes use different keys (e.g. two BitLocker partitions with different recovery keys) can be added in one pass. Details: - Rows are keyed by recovery key GUID (falling back to volume offset) so they stay stable when TSK's message drops the offset suffix once only one volume remains locked. - Rows persist across re-validations of the same image (a volume that unlocks disappears from the message but its password must remain a candidate) and clear when the image path changes or the panel is reset; stale results from in-flight validations of a previously selected path are discarded. - ImageDSProcessor merges the panel's password fields into one candidate list for both addImageToDatabase (ingest stream) and AddImageTask/makeAddImageProcess. All existing single-password entry points (process(), LocalDiskDSProcessor, auto-ingest) are unchanged. Requires the sleuthkit multi-bitlocker-passwords branch (candidate password list APIs). Co-Authored-By: Claude Fable 5 --- .../autopsy/casemodule/AddImageTask.java | 28 +- .../casemodule/Bundle.properties-MERGED | 10 +- .../autopsy/casemodule/ImageDSProcessor.java | 63 +++- .../autopsy/casemodule/ImageFilePanel.form | 17 +- .../autopsy/casemodule/ImageFilePanel.java | 341 ++++++++++++++++-- 5 files changed, 408 insertions(+), 51 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java index 793d90bae31..ae867466e6b 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/AddImageTask.java @@ -107,7 +107,11 @@ public void run() { try { synchronized (tskAddImageProcessLock) { if (!tskAddImageProcessStopped) { - tskAddImageProcess = currentCase.getSleuthkitCase().makeAddImageProcess(imageDetails.timeZone, true, imageDetails.ignoreFatOrphanFiles, imageWriterPath, imageDetails.password); + if (imageDetails.passwords != null && !imageDetails.passwords.isEmpty()) { + tskAddImageProcess = currentCase.getSleuthkitCase().makeAddImageProcess(imageDetails.timeZone, true, imageDetails.ignoreFatOrphanFiles, imageWriterPath, imageDetails.passwords); + } else { + tskAddImageProcess = currentCase.getSleuthkitCase().makeAddImageProcess(imageDetails.timeZone, true, imageDetails.ignoreFatOrphanFiles, imageWriterPath, imageDetails.password); + } } else { return; } @@ -316,24 +320,34 @@ static class ImageDetails { String timeZone; boolean ignoreFatOrphanFiles; String md5; - String sha1; + String sha1; String sha256; ImageWriterSettings imageWriterSettings; String password; - + List passwords; + ImageDetails(String deviceId, Image image, int sectorSize, String timeZone, boolean ignoreFatOrphanFiles, String md5, String sha1, String sha256, ImageWriterSettings imageWriterSettings, String password) { + this(deviceId, image, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, imageWriterSettings, (List) null); + // Store the single password directly (not as a one-element + // candidate list) so existing single-password callers keep going + // through the legacy makeAddImageProcess(String) path. + this.password = password; + } + + ImageDetails(String deviceId, Image image, int sectorSize, String timeZone, boolean ignoreFatOrphanFiles, String md5, String sha1, String sha256, ImageWriterSettings imageWriterSettings, List passwords) { this.deviceId = deviceId; this.image = image; this.sectorSize = sectorSize; this.timeZone = timeZone; this.ignoreFatOrphanFiles = ignoreFatOrphanFiles; this.md5 = md5; - this.sha1 = sha1; - this.sha256 = sha256; + this.sha1 = sha1; + this.sha256 = sha256; this.imageWriterSettings = imageWriterSettings; - this.password = password; + this.password = (passwords != null && !passwords.isEmpty()) ? passwords.get(0) : null; + this.passwords = passwords; } - + String getImagePath() { if (image.getPaths().length > 0) { return image.getPaths()[0]; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED index a68d4ba9889..acbdf386c57 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED @@ -166,8 +166,16 @@ ImageFilePanel.validatePanel.dataSourceOnCDriveError=Warning: Path to multi-user ImageFilePanel.validatePanel.invalidMD5=Invalid MD5 hash ImageFilePanel.validatePanel.invalidSHA1=Invalid SHA1 hash ImageFilePanel.validatePanel.invalidSHA256=Invalid SHA256 hash +# {0} - recoveryKeyId +ImageFilePanel_bitlockerVolume_labelIdOnly=BitLocker volume (Recovery key ID: {0}): +# {0} - volumeOffset +ImageFilePanel_bitlockerVolume_labelNoId=BitLocker volume at offset {0} (user password): +ImageFilePanel_bitlockerVolume_labelPlain=BitLocker volume password: +# {0} - volumeOffset +# {1} - recoveryKeyId +ImageFilePanel_bitlockerVolume_labelWithId=BitLocker volume at offset {0} (Recovery key ID: {1}): # {0} - imageOpenError -ImageFilePanel_validatePanel_imageOpenError=

An error occurred while opening the image:{0}

+ImageFilePanel_validatePanel_imageOpenError=

An error occurred while opening the image:{0}

ImageFilePanel_validatePanel_unknownError=

An unknown error occurred while attempting to validate the image

ImageFilePanel_validatePanel_unknownErrorMsg= IngestJobInfoPanel.IngestJobTableModel.EndTime.header=End Time diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java index 5c3e88c5d18..15eba56bf69 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java @@ -23,6 +23,7 @@ import javax.swing.JPanel; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.List; import java.util.logging.Level; import java.util.UUID; @@ -84,6 +85,7 @@ public class ImageDSProcessor implements DataSourceProcessor, AutoIngestDataSour private String sha256; private Host host = null; private String password; + private List passwords; static { filtersList.add(allFilter); @@ -213,9 +215,10 @@ public void run(String password, Host host, DataSourceProcessorProgressMonitor p readConfigSettings(); this.host = host; this.password = Objects.toString(password, this.password); + List candidatePasswords = buildCandidatePasswords(); try { image = SleuthkitJNI.addImageToDatabase(Case.getCurrentCase().getSleuthkitCase(), - new String[]{imagePath}, sectorSize, timeZone, md5, sha1, sha256, deviceId, this.password, this.host); + new String[]{imagePath}, sectorSize, timeZone, md5, sha1, sha256, deviceId, candidatePasswords, this.host); } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Error adding data source with path " + imagePath + " to database", ex); final List errors = new ArrayList<>(); @@ -224,7 +227,7 @@ public void run(String password, Host host, DataSourceProcessorProgressMonitor p return; } - doAddImageProcess(deviceId, imagePath, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, this.password, progressMonitor, callback); + doAddImageProcess(deviceId, imagePath, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, candidatePasswords, progressMonitor, callback); } @@ -253,7 +256,7 @@ public void run(String deviceId, String imagePath, String timeZone, boolean igno ingestStream = new DefaultIngestStream(); try { image = SleuthkitJNI.addImageToDatabase(Case.getCurrentCase().getSleuthkitCase(), - new String[]{imagePath}, sectorSize, timeZone, "", "", "", deviceId, null, null); + new String[]{imagePath}, sectorSize, timeZone, "", "", "", deviceId, (String) null, null); } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Error adding data source with path " + imagePath + " to database", ex); final List errors = new ArrayList<>(); @@ -262,7 +265,10 @@ public void run(String deviceId, String imagePath, String timeZone, boolean igno return; } - doAddImageProcess(deviceId, imagePath, 0, timeZone, ignoreFatOrphanFiles, null, null, null, this.password, progressMonitor, callback); + // This overload uses only the settings given by the caller, never + // the candidate passwords collected from the configuration panel. + doAddImageProcess(deviceId, imagePath, 0, timeZone, ignoreFatOrphanFiles, null, null, null, + (password != null) ? Collections.singletonList(password) : null, progressMonitor, callback); } @@ -316,18 +322,19 @@ public void runWithIngestStream(Host host, IngestJobSettings settings, DataSourc @Override - public void runWithIngestStream(String password, Host host, IngestJobSettings settings, + public void runWithIngestStream(String password, Host host, IngestJobSettings settings, DataSourceProcessorProgressMonitor progress, DataSourceProcessorCallback callBack) { - // Read the settings from the wizard + // Read the settings from the wizard readConfigSettings(); this.host = host; this.password = Objects.toString(password, this.password); + List candidatePasswords = buildCandidatePasswords(); // Set up the data source before creating the ingest stream try { image = SleuthkitJNI.addImageToDatabase(Case.getCurrentCase().getSleuthkitCase(), - new String[]{imagePath}, sectorSize, timeZone, md5, sha1, sha256, deviceId, this.password, this.host); + new String[]{imagePath}, sectorSize, timeZone, md5, sha1, sha256, deviceId, candidatePasswords, this.host); } catch (TskCoreException ex) { logger.log(Level.SEVERE, "Error adding data source with path " + imagePath + " to database", ex); final List errors = new ArrayList<>(); @@ -347,7 +354,7 @@ public void runWithIngestStream(String password, Host host, IngestJobSettings se ingestStream = new DefaultIngestStream(); } - doAddImageProcess(deviceId, imagePath, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, this.password, progress, callBack); + doAddImageProcess(deviceId, imagePath, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, candidatePasswords, progress, callBack); } @@ -377,6 +384,32 @@ private void readConfigSettings() { if (this.password.isEmpty()) { password = null; } + this.passwords = configPanel.getPasswords(); + if (this.passwords.isEmpty()) { + this.passwords = null; + } + } + + /** + * Combines the single password (possibly supplied by a caller) with the + * candidate passwords collected from the configuration panel. Each + * candidate will be tried when opening encrypted volumes in the image. + * + * @return The combined candidate password list, or null if there are none. + */ + private List buildCandidatePasswords() { + List candidates = new ArrayList<>(); + if (password != null) { + candidates.add(password); + } + if (passwords != null) { + for (String candidate : passwords) { + if (!candidates.contains(candidate)) { + candidates.add(candidate); + } + } + } + return candidates.isEmpty() ? null : candidates; } /** @@ -415,12 +448,13 @@ public boolean supportsIngestStream() { * @param md5 The MD5 hash of the image, may be null. * @param sha1 The SHA-1 hash of the image, may be null. * @param sha256 The SHA-256 hash of the image, may be null. - * @param password Password for image decryption. May be null. + * @param passwords Candidate passwords for image decryption. + * May be null or empty. * @param progressMonitor Progress monitor for reporting progress * during processing. * @param callback Callback to call when processing is done. */ - private void doAddImageProcess(String deviceId, String imagePath, int sectorSize, String timeZone, boolean ignoreFatOrphanFiles, String md5, String sha1, String sha256, String password, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { + private void doAddImageProcess(String deviceId, String imagePath, int sectorSize, String timeZone, boolean ignoreFatOrphanFiles, String md5, String sha1, String sha256, List passwords, DataSourceProcessorProgressMonitor progressMonitor, DataSourceProcessorCallback callback) { // If the data source or ingest stream haven't been initialized, stop processing if (ingestStream == null) { @@ -440,7 +474,7 @@ private void doAddImageProcess(String deviceId, String imagePath, int sectorSize return; } - AddImageTask.ImageDetails imageDetails = new AddImageTask.ImageDetails(deviceId, image, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, null, password); + AddImageTask.ImageDetails imageDetails = new AddImageTask.ImageDetails(deviceId, image, sectorSize, timeZone, ignoreFatOrphanFiles, md5, sha1, sha256, null, passwords); addImageTask = new AddImageTask(imageDetails, progressMonitor, new StreamingAddDataSourceCallbacks(ingestStream), @@ -477,6 +511,7 @@ public void reset() { ignoreFatOrphanFiles = false; host = null; password = null; + passwords = null; configPanel.reset(); } @@ -561,7 +596,8 @@ public void process(String deviceId, Path dataSourcePath, String password, Host return; } - doAddImageProcess(deviceId, dataSourcePath.toString(), sectorSize, timeZone, ignoreFatOrphanFiles, null, null, null, this.password, progressMonitor, callBack); + doAddImageProcess(deviceId, dataSourcePath.toString(), sectorSize, timeZone, ignoreFatOrphanFiles, null, null, null, + (this.password != null) ? Collections.singletonList(this.password) : null, progressMonitor, callBack); } @@ -610,7 +646,8 @@ public IngestStream processWithIngestStream(String deviceId, Path dataSourcePath return null; } - doAddImageProcess(deviceId, dataSourcePath.toString(), sectorSize, timeZone, ignoreFatOrphanFiles, null, null, null, password, progressMonitor, callBack); + doAddImageProcess(deviceId, dataSourcePath.toString(), sectorSize, timeZone, ignoreFatOrphanFiles, null, null, null, + (password != null) ? Collections.singletonList(password) : null, progressMonitor, callBack); return ingestStream; } diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form index 6e3633641d5..e70c509f9f8 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form @@ -115,15 +115,9 @@ - - - - - - @@ -286,6 +280,15 @@ + + + + + + + + + @@ -293,7 +296,7 @@ - + diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java index bb4170b7fff..b1a998dbbdd 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java @@ -20,18 +20,26 @@ import com.google.common.util.concurrent.ThreadFactoryBuilder; import java.io.File; +import java.util.ArrayList; import java.util.Calendar; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.concurrent.Future; import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; import java.util.logging.Level; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.swing.JFileChooser; +import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JTextField; +import javax.swing.SwingUtilities; import javax.swing.event.DocumentEvent; import javax.swing.event.DocumentListener; import javax.swing.filechooser.FileFilter; @@ -75,6 +83,19 @@ public class ImageFilePanel extends JPanel { private Runnable validateAction = null; private Future validateFuture = null; + private static final String BITLOCKER_LINE_MARKER = "BitLocker status - "; //NON-NLS + private static final Pattern BITLOCKER_GUID_PATTERN = Pattern.compile("Recovery key identifier: ([^)]*)\\)"); //NON-NLS + private static final Pattern BITLOCKER_OFFSET_PATTERN = Pattern.compile("\\(Volume offset: (\\d+)\\)"); //NON-NLS + + /** + * One password field per locked BitLocker volume, keyed by volume offset + * and recovery key identifier. Guarded by its own monitor because it is + * read by the background validation thread and updated on the EDT. + */ + private final Map bitlockerVolumeRows = new LinkedHashMap<>(); + private DocumentListener delayedValidationListener = null; + private String bitlockerVolumesImagePath = null; + /** * Creates new form ImageFilePanel * @@ -98,6 +119,7 @@ private ImageFilePanel(String context, List fileChooserFilters) { errorLabel.setVisible(false); loadingLabel.setVisible(false); + bitlockerVolumesPanel.setVisible(false); this.fileChooserFilters = fileChooserFilters; } @@ -128,7 +150,8 @@ private void createTimeZoneList() { public static synchronized ImageFilePanel createInstance(String context, List fileChooserFilters) { ImageFilePanel instance = new ImageFilePanel(context, fileChooserFilters); DocumentListener delayedValidationListener = instance.new DelayedValidationDocListener(); - + instance.delayedValidationListener = delayedValidationListener; + // post-constructor initialization of listener support without leaking references of uninitialized objects for (JTextField textField: List.of( instance.getPathTextField(), @@ -204,6 +227,7 @@ private void initComponents() { hashValuesNoteLabel = new javax.swing.JLabel(); passwordLabel = new javax.swing.JLabel(); passwordTextField = new javax.swing.JTextField(); + bitlockerVolumesPanel = new javax.swing.JPanel(); javax.swing.JPanel spacer = new javax.swing.JPanel(); loadingLabel = new javax.swing.JLabel(); @@ -280,9 +304,7 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { errorLabel.setForeground(new java.awt.Color(255, 0, 0)); org.openide.awt.Mnemonics.setLocalizedText(errorLabel, org.openide.util.NbBundle.getMessage(ImageFilePanel.class, "ImageFilePanel.errorLabel.text")); // NOI18N errorLabel.setVerticalAlignment(javax.swing.SwingConstants.TOP); - errorLabel.setMaximumSize(new java.awt.Dimension(500, 60)); errorLabel.setMinimumSize(new java.awt.Dimension(200, 20)); - errorLabel.setPreferredSize(new java.awt.Dimension(200, 60)); gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; gridBagConstraints.gridy = 11; @@ -417,6 +439,17 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 5); add(passwordTextField, gridBagConstraints); + bitlockerVolumesPanel.setLayout(new java.awt.GridBagLayout()); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 12; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.insets = new java.awt.Insets(0, 5, 5, 5); + add(bitlockerVolumesPanel, gridBagConstraints); + javax.swing.GroupLayout spacerLayout = new javax.swing.GroupLayout(spacer); spacer.setLayout(spacerLayout); spacerLayout.setHorizontalGroup( @@ -430,7 +463,7 @@ public void actionPerformed(java.awt.event.ActionEvent evt) { gridBagConstraints = new java.awt.GridBagConstraints(); gridBagConstraints.gridx = 0; - gridBagConstraints.gridy = 12; + gridBagConstraints.gridy = 13; gridBagConstraints.weighty = 1.0; add(spacer, gridBagConstraints); @@ -488,12 +521,16 @@ private void browseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN- sha1HashTextField.setText(null); sha256HashTextField.setText(null); } - } - updateHelper(); + // Only update after a selection: nothing changed on cancel, and + // with no delayed validation pending this call would otherwise + // run the image test synchronously on the EDT. + updateHelper(); + } }//GEN-LAST:event_browseButtonActionPerformed // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JPanel bitlockerVolumesPanel; private javax.swing.JButton browseButton; private javax.swing.JLabel errorLabel; private javax.swing.JLabel hashValuesLabel; @@ -593,13 +630,240 @@ String getPassword() { return this.passwordTextField.getText(); } + /** + * Gets all candidate passwords entered by the user: the main password + * field plus any per-volume BitLocker password fields. Each will be tried + * when opening the encrypted volumes in the image. + * + * @return De-duplicated list of non-empty candidate passwords. + */ + List getPasswords() { + List passwords = new ArrayList<>(); + // This runs off the EDT; a concurrent edit can make getText() return + // null, so default to "". + String mainPassword = StringUtils.defaultString(getPassword()); + if (!mainPassword.isEmpty()) { + passwords.add(mainPassword); + } + synchronized (bitlockerVolumeRows) { + // Row passwords belong to the image they were shown for; skip + // them if the selected path has changed since (the rows + // themselves clear asynchronously on the EDT). + if (Objects.equals(getContentPaths(), bitlockerVolumesImagePath)) { + for (BitlockerVolumeRow volumeRow : bitlockerVolumeRows.values()) { + String volumePassword = StringUtils.defaultString(volumeRow.passwordField.getText()); + if (!volumePassword.isEmpty() && !passwords.contains(volumePassword)) { + passwords.add(volumePassword); + } + } + } + } + return passwords; + } + + /** + * A locked BitLocker volume parsed from the test open image result + * message. + */ + private static class BitlockerVolumeInfo { + + private final String volumeOffset; // null if not in the message + private final String recoveryKeyId; // null if not in the message + + BitlockerVolumeInfo(String volumeOffset, String recoveryKeyId) { + this.volumeOffset = volumeOffset; + this.recoveryKeyId = recoveryKeyId; + } + + String getKey() { + // The offset suffix is only present in the message while more + // than one volume is locked, so it is not stable across + // re-validations; key on the recovery key GUID when there is + // one and fall back to the offset. + if (recoveryKeyId != null) { + return recoveryKeyId; + } + if (volumeOffset != null) { + return volumeOffset; + } + return ""; + } + } + + /** + * The Swing components of one per-volume BitLocker password row. + */ + private static class BitlockerVolumeRow { + + private final JLabel label; + private final JTextField passwordField; + + BitlockerVolumeRow(JLabel label, JTextField passwordField) { + this.label = label; + this.passwordField = passwordField; + } + } + + /** + * Parses the locked BitLocker volumes out of a test open image result + * message. The message contains one line per locked volume in the form + * "BitLocker status - ... (Recovery key identifier: GUID) (Volume offset: N)" + * where the identifier may be blank and the offset is only present when + * there are multiple locked volumes. + * + * @param message The test open image result message. + * + * @return The locked volumes, empty if none were found. + */ + private List parseBitlockerVolumes(String message) { + List volumes = new ArrayList<>(); + if (StringUtils.isBlank(message)) { + return volumes; + } + for (String line : message.split("\n")) { + if (!line.contains(BITLOCKER_LINE_MARKER)) { + continue; + } + Matcher guidMatcher = BITLOCKER_GUID_PATTERN.matcher(line); + String recoveryKeyId = guidMatcher.find() ? StringUtils.trimToNull(guidMatcher.group(1)) : null; + Matcher offsetMatcher = BITLOCKER_OFFSET_PATTERN.matcher(line); + String volumeOffset = offsetMatcher.find() ? offsetMatcher.group(1) : null; + volumes.add(new BitlockerVolumeInfo(volumeOffset, recoveryKeyId)); + } + return volumes; + } + + @NbBundle.Messages({ + "# {0} - volumeOffset", + "# {1} - recoveryKeyId", + "ImageFilePanel_bitlockerVolume_labelWithId=BitLocker volume at offset {0} (Recovery key ID: {1}):", + "# {0} - volumeOffset", + "ImageFilePanel_bitlockerVolume_labelNoId=BitLocker volume at offset {0} (user password):", + "# {0} - recoveryKeyId", + "ImageFilePanel_bitlockerVolume_labelIdOnly=BitLocker volume (Recovery key ID: {0}):", + "ImageFilePanel_bitlockerVolume_labelPlain=BitLocker volume password:" + }) + private static String getBitlockerVolumeLabel(BitlockerVolumeInfo volumeInfo) { + if (volumeInfo.volumeOffset != null && volumeInfo.recoveryKeyId != null) { + return Bundle.ImageFilePanel_bitlockerVolume_labelWithId(volumeInfo.volumeOffset, volumeInfo.recoveryKeyId); + } else if (volumeInfo.volumeOffset != null) { + return Bundle.ImageFilePanel_bitlockerVolume_labelNoId(volumeInfo.volumeOffset); + } else if (volumeInfo.recoveryKeyId != null) { + return Bundle.ImageFilePanel_bitlockerVolume_labelIdOnly(volumeInfo.recoveryKeyId); + } + return Bundle.ImageFilePanel_bitlockerVolume_labelPlain(); + } + + /** + * Shows a labeled password field for each locked BitLocker volume. Fields + * for volumes that are already shown keep their contents; a volume that + * unlocks is no longer reported in the message but its field (and + * password) is kept so it remains part of the candidate list. + * + * @param imagePath The image the volumes belong to; switching images + * clears all fields. + * @param volumes The locked volumes parsed from the latest validation. + */ + private void updateBitlockerVolumeRows(String imagePath, List volumes) { + SwingUtilities.invokeLater(() -> { + if (!Objects.equals(imagePath, getContentPaths())) { + // Result of a validation for a path that is no longer + // selected (the path changed or the panel was reset while + // the image test was running). + return; + } + synchronized (bitlockerVolumeRows) { + if (!Objects.equals(imagePath, bitlockerVolumesImagePath)) { + clearBitlockerVolumeRows(); + bitlockerVolumesImagePath = imagePath; + } + boolean changed = false; + for (BitlockerVolumeInfo volumeInfo : volumes) { + String volumeKey = volumeInfo.getKey(); + if (bitlockerVolumeRows.containsKey(volumeKey)) { + continue; + } + if (volumeKey.isEmpty()) { + if (!bitlockerVolumeRows.isEmpty()) { + // A line with neither a recovery key GUID nor an + // offset cannot be matched to a specific existing + // row; every field is pooled into the candidate + // list anyway, so do not add an unidentifiable + // duplicate. + continue; + } + } else { + // A volume first reported without any identifier gets + // its row re-keyed and relabeled once an identified + // report arrives, instead of gaining a second row. + BitlockerVolumeRow orphanRow = bitlockerVolumeRows.remove(""); + if (orphanRow != null) { + orphanRow.label.setText(getBitlockerVolumeLabel(volumeInfo)); + bitlockerVolumeRows.put(volumeKey, orphanRow); + changed = true; + continue; + } + } + + int row = bitlockerVolumeRows.size(); + JLabel volumeLabel = new JLabel(getBitlockerVolumeLabel(volumeInfo)); + java.awt.GridBagConstraints labelConstraints = new java.awt.GridBagConstraints(); + labelConstraints.gridx = 0; + labelConstraints.gridy = row; + labelConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + labelConstraints.insets = new java.awt.Insets(0, 0, 5, 5); + bitlockerVolumesPanel.add(volumeLabel, labelConstraints); + + JTextField volumeField = new JTextField(); + if (delayedValidationListener != null) { + volumeField.getDocument().addDocumentListener(delayedValidationListener); + } + java.awt.GridBagConstraints fieldConstraints = new java.awt.GridBagConstraints(); + fieldConstraints.gridx = 1; + fieldConstraints.gridy = row; + fieldConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + fieldConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + fieldConstraints.weightx = 1.0; + fieldConstraints.insets = new java.awt.Insets(0, 0, 5, 0); + bitlockerVolumesPanel.add(volumeField, fieldConstraints); + + bitlockerVolumeRows.put(volumeKey, new BitlockerVolumeRow(volumeLabel, volumeField)); + changed = true; + } + if (changed) { + bitlockerVolumesPanel.setVisible(!bitlockerVolumeRows.isEmpty()); + bitlockerVolumesPanel.revalidate(); + bitlockerVolumesPanel.repaint(); + } + } + }); + } + + /** + * Removes all per-volume BitLocker password fields. Must be called on the + * EDT while holding the bitlockerVolumeRows monitor. + */ + private void clearBitlockerVolumeRows() { + bitlockerVolumeRows.clear(); + bitlockerVolumesPanel.removeAll(); + bitlockerVolumesPanel.setVisible(false); + bitlockerVolumesPanel.revalidate(); + bitlockerVolumesPanel.repaint(); + } + public void reset() { - //reset the UI elements to default + //reset the UI elements to default pathTextField.setText(null); this.md5HashTextField.setText(null); this.sha1HashTextField.setText(null); this.sha256HashTextField.setText(null); this.passwordTextField.setText(null); + SwingUtilities.invokeLater(() -> { + synchronized (bitlockerVolumeRows) { + clearBitlockerVolumeRows(); + bitlockerVolumesImagePath = null; + } + }); } /** @@ -608,15 +872,35 @@ public void reset() { * @param enabled True */ private void setUIEnabled(boolean enabled, boolean validNonE01) { - this.browseButton.setEnabled(enabled); - this.noFatOrphansCheckbox.setEnabled(enabled); - this.passwordTextField.setEnabled(enabled); - this.pathTextField.setEnabled(enabled); - this.sectorSizeComboBox.setEnabled(enabled); - this.md5HashTextField.setEnabled(enabled && validNonE01); - this.sha1HashTextField.setEnabled(enabled && validNonE01); - this.sha256HashTextField.setEnabled(enabled && validNonE01); - this.timeZoneComboBox.setEnabled(enabled); + SwingUtilities.invokeLater(() -> { + this.browseButton.setEnabled(enabled); + this.noFatOrphansCheckbox.setEnabled(enabled); + setTextFieldEnabled(this.passwordTextField, enabled); + setTextFieldEnabled(this.pathTextField, enabled); + this.sectorSizeComboBox.setEnabled(enabled); + setTextFieldEnabled(this.md5HashTextField, enabled && validNonE01); + setTextFieldEnabled(this.sha1HashTextField, enabled && validNonE01); + setTextFieldEnabled(this.sha256HashTextField, enabled && validNonE01); + this.timeZoneComboBox.setEnabled(enabled); + synchronized (bitlockerVolumeRows) { + for (BitlockerVolumeRow volumeRow : bitlockerVolumeRows.values()) { + setTextFieldEnabled(volumeRow.passwordField, enabled); + } + } + }); + } + + /** + * Enables or disables a text field, leaving the field the user is typing + * in editable. Validation snapshots all field values before it starts and + * any edit during validation schedules a re-validation, so an edit to the + * focused field cannot produce a stale accepted state, while disabling it + * mid-typing would drop keystrokes and move the focus away. + */ + private static void setTextFieldEnabled(JTextField field, boolean enabled) { + if (enabled || !field.isFocusOwner()) { + field.setEnabled(enabled); + } } /** @@ -630,7 +914,7 @@ private void setUIEnabled(boolean enabled, boolean validNonE01) { "ImageFilePanel.validatePanel.invalidSHA1=Invalid SHA1 hash", "ImageFilePanel.validatePanel.invalidSHA256=Invalid SHA256 hash", "# {0} - imageOpenError", - "ImageFilePanel_validatePanel_imageOpenError=

An error occurred while opening the image:{0}

", + "ImageFilePanel_validatePanel_imageOpenError=

An error occurred while opening the image:{0}

", "ImageFilePanel_validatePanel_unknownErrorMsg=", "ImageFilePanel_validatePanel_unknownError=

An unknown error occurred while attempting to validate the image

" }) @@ -648,7 +932,12 @@ public boolean validatePanel() { String md5 = getMd5(); String sha1 = getSha1(); String sha256 = getSha256(); - String password = getPassword(); + List passwords = getPasswords(); + + // A path change clears the rows of the previous image even + // when validation exits early below; for an unchanged path + // this is a no-op that keeps the rows. + updateBitlockerVolumeRows(path, new ArrayList<>()); if (!isImagePathValid(path)) { showError(null); @@ -671,14 +960,20 @@ public boolean validatePanel() { } try { - TestOpenImageResult testResult = SleuthkitJNI.testOpenImage(path, password); + TestOpenImageResult testResult = SleuthkitJNI.testOpenImage(path, passwords); if (!testResult.wasSuccessful()) { - showError(Bundle.ImageFilePanel_validatePanel_imageOpenError( - StringUtils.defaultIfBlank( - testResult.getMessage(), - Bundle.ImageFilePanel_validatePanel_unknownErrorMsg()))); + // Show a password field for each locked BitLocker volume + // reported in the message. + updateBitlockerVolumeRows(path, parseBitlockerVolumes(testResult.getMessage())); + String message = StringUtils.defaultIfBlank( + testResult.getMessage(), + Bundle.ImageFilePanel_validatePanel_unknownErrorMsg()); + // The error label renders HTML, so multiple locked + // volumes need
tags to show as separate lines. + showError(Bundle.ImageFilePanel_validatePanel_imageOpenError(message.replace("\n", "
"))); return false; } + updateBitlockerVolumeRows(path, new ArrayList<>()); } catch (Throwable t) { logger.log(Level.SEVERE, "An unknown error occurred test opening image: " + path, t); showError(Bundle.ImageFilePanel_validatePanel_unknownError()); From 9b4b797c0ccf98b7a872324a090a9e4aac5d106c Mon Sep 17 00:00:00 2001 From: Ganesh <65601315+ganeshbs17@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:01:59 +0530 Subject: [PATCH 3/5] Add multiple bitlocker partition support --- .../casemodule/Bundle.properties-MERGED | 5 +++ .../autopsy/casemodule/ImageFilePanel.java | 43 +++++++++++++++---- .../actionhelpers/Bundle.properties-MERGED | 3 -- .../netbeans/core/startup/Bundle.properties | 2 +- .../core/windows/view/ui/Bundle.properties | 2 +- 5 files changed, 42 insertions(+), 13 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED index acbdf386c57..5350f45c7f1 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED @@ -6,6 +6,9 @@ AddImageWizardSelectHostVisual_title=Select Host # {0} - exception message Case.closeException.couldNotCloseCase=Error closing case: {0} # {0} - provider name +Case.contentProviderLoadFailed.message=The content provider plugin (''{0}'') is installed but could not be loaded. Check the module for errors. +Case.contentProviderLoadFailed.title=Content Provider Load Failed +# {0} - provider name Case.contentProviderNotFound.message=This case requires a content provider plugin (''{0}'') that is not installed. Please install the appropriate plugin. Case.contentProviderNotFound.title=Content Provider Not Found Case.creationException.couldNotAcquireResourcesLock=Failed to get lock on case resources @@ -20,6 +23,7 @@ Case.exceptionMessage.cannotGetLockToDeleteCase=Cannot delete case because it is Case.exceptionMessage.cannotLocateMainWindow=Cannot locate main application window Case.exceptionMessage.cannotOpenMultiUserCaseNoSettings=Multi-user settings are missing (see Tools, Options, Multi-user tab), cannot open a multi-user case. Case.exceptionMessage.contentProviderCouldNotBeFound=Content provider was specified for the case but could not be loaded. +Case.exceptionMessage.contentProviderLoadFailed=The content provider plugin is installed but failed to load. Case.exceptionMessage.contentProviderVersionMismatch=The installed content provider plugin is not compatible with this case. # {0} - exception message Case.exceptionMessage.couldNotCreatCollaborationMonitor=Failed to create collaboration monitor:\n{0}. @@ -174,6 +178,7 @@ ImageFilePanel_bitlockerVolume_labelPlain=BitLocker volume password: # {0} - volumeOffset # {1} - recoveryKeyId ImageFilePanel_bitlockerVolume_labelWithId=BitLocker volume at offset {0} (Recovery key ID: {1}): +ImageFilePanel_validatePanel_bitlockerLocked=

One or more BitLocker volumes require a password to open this image. Enter a password or recovery key for each locked volume below.

# {0} - imageOpenError ImageFilePanel_validatePanel_imageOpenError=

An error occurred while opening the image:{0}

ImageFilePanel_validatePanel_unknownError=

An unknown error occurred while attempting to validate the image

diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java index b1a998dbbdd..9e75c709f85 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java @@ -834,6 +834,9 @@ private void updateBitlockerVolumeRows(String imagePath, List

An error occurred while opening the image:{0}

", "ImageFilePanel_validatePanel_unknownErrorMsg=", - "ImageFilePanel_validatePanel_unknownError=

An unknown error occurred while attempting to validate the image

" + "ImageFilePanel_validatePanel_unknownError=

An unknown error occurred while attempting to validate the image

", + "ImageFilePanel_validatePanel_bitlockerLocked=

One or more BitLocker volumes require a password to open this " + + "image. Enter a password or recovery key for each locked volume below.

" }) public boolean validatePanel() { return runWithLock(this.validationLock, () -> { @@ -964,13 +983,21 @@ public boolean validatePanel() { if (!testResult.wasSuccessful()) { // Show a password field for each locked BitLocker volume // reported in the message. - updateBitlockerVolumeRows(path, parseBitlockerVolumes(testResult.getMessage())); - String message = StringUtils.defaultIfBlank( - testResult.getMessage(), - Bundle.ImageFilePanel_validatePanel_unknownErrorMsg()); - // The error label renders HTML, so multiple locked - // volumes need
tags to show as separate lines. - showError(Bundle.ImageFilePanel_validatePanel_imageOpenError(message.replace("\n", "
"))); + List volumes = parseBitlockerVolumes(testResult.getMessage()); + updateBitlockerVolumeRows(path, volumes); + if (volumes.isEmpty()) { + // Not a BitLocker failure; show the detailed message. + String message = StringUtils.defaultIfBlank( + testResult.getMessage(), + Bundle.ImageFilePanel_validatePanel_unknownErrorMsg()); + // The error label renders HTML, so multiple locked + // volumes need
tags to show as separate lines. + showError(Bundle.ImageFilePanel_validatePanel_imageOpenError(message.replace("\n", "
"))); + } else { + // The per-volume rows below already show each + // volume's identifier, so a short prompt is enough. + showError(Bundle.ImageFilePanel_validatePanel_bitlockerLocked()); + } return false; } updateBitlockerVolumeRows(path, new ArrayList<>()); diff --git a/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/Bundle.properties-MERGED index 28c8c2c7a5d..ec1a9bc5ccb 100644 --- a/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/directorytree/actionhelpers/Bundle.properties-MERGED @@ -1,9 +1,6 @@ ExtractActionHelper.extractFiles.cantCreateFolderErr.msg=Could not create selected folder. ExtractActionHelper.confDlg.destFileExist.msg=Destination file {0} already exists, overwrite? ExtractActionHelper.confDlg.destFileExist.title=File Exists -# {0} - fileName -ExtractActionHelper.extractOverwrite.msg=A file already exists at {0}. Do you want to overwrite the existing file? -ExtractActionHelper.extractOverwrite.title=Export to csv file ExtractActionHelper.msgDlg.cantOverwriteFile.msg=Could not overwrite existing file {0} ExtractActionHelper.noOpenCase.errMsg=No open case available. ExtractActionHelper.notifyDlg.noFileToExtr.msg=No file(s) to extract. diff --git a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties index 3952efeedf5..a2c81a67cec 100644 --- a/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties +++ b/branding/core/core.jar/org/netbeans/core/startup/Bundle.properties @@ -1,5 +1,5 @@ #Updated by build script -#Tue, 14 Apr 2026 10:25:14 -0400 +#Fri, 14 Aug 2026 20:09:30 +0530 LBL_splash_window_title=Starting Autopsy SPLASH_HEIGHT=314 SPLASH_WIDTH=538 diff --git a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties index cfb88e63196..5c8b75b398e 100644 --- a/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties +++ b/branding/modules/org-netbeans-core-windows.jar/org/netbeans/core/windows/view/ui/Bundle.properties @@ -1,4 +1,4 @@ #Updated by build script -#Tue, 14 Apr 2026 10:25:14 -0400 +#Fri, 14 Aug 2026 20:09:30 +0530 CTL_MainWindow_Title=Autopsy 4.23.0 CTL_MainWindow_Title_No_Project=Autopsy 4.23.0 From 1223983e47606a5f8f41edba2ecd77fabbb555d4 Mon Sep 17 00:00:00 2001 From: Ganesh <65601315+ganeshbs17@users.noreply.github.com> Date: Fri, 14 Aug 2026 21:54:48 +0530 Subject: [PATCH 4/5] Streamline the per-volume BitLocker password UI Follow-up polish based on live GUI testing of the multi-BitLocker feature against real dual-partition images: - Row labels now show only the recovery key ID, which is what BitLocker users actually record/reference; the raw volume byte offset is dropped from the label and used only as a fallback when a volume has no recovery-key protector at all (e.g. password-only protection), since it's then the only way to tell two locked volumes apart. - Removed the single "Bitlocker Password (optional)" field entirely, now that every locked volume gets its own labeled field. In ImageDSProcessor, readConfigSettings() no longer reads a password from the panel; the five external-caller entry points that take a password argument directly (run(), runWithIngestStream(), process(), processWithIngestStream(), canProcess()) are unaffected. - Each per-volume row now shows a live status line: the volume's exact BitLocker status text (e.g. "Incorrect password entered", "Password required to decrypt volume") in red while still locked, or a green "Unlocked" once its password is accepted, refreshed on every re-validation. Fixed a lifecycle bug where the top-of-validatePanel() defensive row-clear (which runs before any test-open-image result is known) could flash a false "unlocked" status: split it into a clear-only path used before validation runs, keeping the full status-refreshing path for after a real result comes back. Co-Authored-By: Claude Fable 5 Co-Authored-By: Claude Sonnet 5 --- .../autopsy/casemodule/Bundle.properties | 4 +- .../casemodule/Bundle.properties-MERGED | 13 +- .../autopsy/casemodule/ImageDSProcessor.java | 7 +- .../autopsy/casemodule/ImageFilePanel.form | 24 -- .../autopsy/casemodule/ImageFilePanel.java | 214 ++++++++++-------- 5 files changed, 128 insertions(+), 134 deletions(-) diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties index 9fc3d3315e7..f26800bbda9 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties @@ -269,7 +269,5 @@ LocalFilesPanel.jLabel2.text=NOTE: Time stamps may have changed when the files w LocalFilesPanel.timestampToIncludeLabel.text=Timestamps To Include: LocalFilesPanel.accessTimeCheckBox.text=Access Time - Can be changed when the file is opened LocalFilesPanel.timeStampToIncludeLabel.text=Timestamps To Include: -LocalFilesPanel.timeStampNoteLabel.text=NOTE: Time stamps may have changed when the files were copied to the current location. -ImageFilePanel.passwordLabel.text=Bitlocker Password (optional): -ImageFilePanel.passwordTextField.text= +LocalFilesPanel.timeStampNoteLabel.text=NOTE: Time stamps may have changed when the files were copied to the current location. ImageFilePanel.loadingLabel.text=loading... diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED index 5350f45c7f1..64507f3407a 100755 --- a/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/casemodule/Bundle.properties-MERGED @@ -170,14 +170,13 @@ ImageFilePanel.validatePanel.dataSourceOnCDriveError=Warning: Path to multi-user ImageFilePanel.validatePanel.invalidMD5=Invalid MD5 hash ImageFilePanel.validatePanel.invalidSHA1=Invalid SHA1 hash ImageFilePanel.validatePanel.invalidSHA256=Invalid SHA256 hash -# {0} - recoveryKeyId -ImageFilePanel_bitlockerVolume_labelIdOnly=BitLocker volume (Recovery key ID: {0}): # {0} - volumeOffset ImageFilePanel_bitlockerVolume_labelNoId=BitLocker volume at offset {0} (user password): ImageFilePanel_bitlockerVolume_labelPlain=BitLocker volume password: -# {0} - volumeOffset -# {1} - recoveryKeyId -ImageFilePanel_bitlockerVolume_labelWithId=BitLocker volume at offset {0} (Recovery key ID: {1}): +# {0} - recoveryKeyId +ImageFilePanel_bitlockerVolume_labelWithId=BitLocker volume \u2014 Recovery key ID: {0}: +ImageFilePanel_bitlockerVolume_statusLocked=Password required +ImageFilePanel_bitlockerVolume_statusUnlocked=\u2713 Unlocked ImageFilePanel_validatePanel_bitlockerLocked=

One or more BitLocker volumes require a password to open this image. Enter a password or recovery key for each locked volume below.

# {0} - imageOpenError ImageFilePanel_validatePanel_imageOpenError=

An error occurred while opening the image:{0}

@@ -527,7 +526,5 @@ LocalFilesPanel.jLabel2.text=NOTE: Time stamps may have changed when the files w LocalFilesPanel.timestampToIncludeLabel.text=Timestamps To Include: LocalFilesPanel.accessTimeCheckBox.text=Access Time - Can be changed when the file is opened LocalFilesPanel.timeStampToIncludeLabel.text=Timestamps To Include: -LocalFilesPanel.timeStampNoteLabel.text=NOTE: Time stamps may have changed when the files were copied to the current location. -ImageFilePanel.passwordLabel.text=Bitlocker Password (optional): -ImageFilePanel.passwordTextField.text= +LocalFilesPanel.timeStampNoteLabel.text=NOTE: Time stamps may have changed when the files were copied to the current location. ImageFilePanel.loadingLabel.text=loading... diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java index 15eba56bf69..70ac0a25966 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageDSProcessor.java @@ -380,10 +380,9 @@ private void readConfigSettings() { if (sha256.isEmpty()) { sha256 = null; } - this.password = configPanel.getPassword(); - if (this.password.isEmpty()) { - password = null; - } + // The panel no longer offers a single/main password field, only the + // per-volume BitLocker fields harvested into `passwords` below. + this.password = null; this.passwords = configPanel.getPasswords(); if (this.passwords.isEmpty()) { this.passwords = null; diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form index e70c509f9f8..04e7dbb918f 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.form @@ -256,30 +256,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java index 9e75c709f85..4a49ab88d6b 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java @@ -22,6 +22,7 @@ import java.io.File; import java.util.ArrayList; import java.util.Calendar; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -86,6 +87,13 @@ public class ImageFilePanel extends JPanel { private static final String BITLOCKER_LINE_MARKER = "BitLocker status - "; //NON-NLS private static final Pattern BITLOCKER_GUID_PATTERN = Pattern.compile("Recovery key identifier: ([^)]*)\\)"); //NON-NLS private static final Pattern BITLOCKER_OFFSET_PATTERN = Pattern.compile("\\(Volume offset: (\\d+)\\)"); //NON-NLS + // Matches the whole optional "(Recovery key identifier: ...)" annotation + // (including its parentheses) so it can be stripped when isolating a + // line's plain-text description; BITLOCKER_GUID_PATTERN above only + // captures the identifier itself, not the surrounding parentheses. + private static final Pattern BITLOCKER_GUID_ANNOTATION_PATTERN = Pattern.compile("\\(Recovery key identifier: [^)]*\\)"); //NON-NLS + private static final java.awt.Color BITLOCKER_STATUS_LOCKED_COLOR = new java.awt.Color(255, 0, 0); + private static final java.awt.Color BITLOCKER_STATUS_UNLOCKED_COLOR = new java.awt.Color(0, 128, 0); /** * One password field per locked BitLocker volume, keyed by volume offset @@ -155,10 +163,9 @@ public static synchronized ImageFilePanel createInstance(String context, List sectorSizeComboBox; @@ -626,25 +606,15 @@ String getSha256() { return this.sha256HashTextField.getText(); } - String getPassword() { - return this.passwordTextField.getText(); - } - /** - * Gets all candidate passwords entered by the user: the main password - * field plus any per-volume BitLocker password fields. Each will be tried - * when opening the encrypted volumes in the image. + * Gets all candidate passwords entered by the user via the per-volume + * BitLocker password fields. Each will be tried when opening the + * encrypted volumes in the image. * * @return De-duplicated list of non-empty candidate passwords. */ List getPasswords() { List passwords = new ArrayList<>(); - // This runs off the EDT; a concurrent edit can make getText() return - // null, so default to "". - String mainPassword = StringUtils.defaultString(getPassword()); - if (!mainPassword.isEmpty()) { - passwords.add(mainPassword); - } synchronized (bitlockerVolumeRows) { // Row passwords belong to the image they were shown for; skip // them if the selected path has changed since (the rows @@ -669,10 +639,12 @@ private static class BitlockerVolumeInfo { private final String volumeOffset; // null if not in the message private final String recoveryKeyId; // null if not in the message + private final String description; // null if not parsed from the message - BitlockerVolumeInfo(String volumeOffset, String recoveryKeyId) { + BitlockerVolumeInfo(String volumeOffset, String recoveryKeyId, String description) { this.volumeOffset = volumeOffset; this.recoveryKeyId = recoveryKeyId; + this.description = description; } String getKey() { @@ -695,12 +667,14 @@ String getKey() { */ private static class BitlockerVolumeRow { - private final JLabel label; + private final JLabel titleLabel; private final JTextField passwordField; + private final JLabel statusLabel; - BitlockerVolumeRow(JLabel label, JTextField passwordField) { - this.label = label; + BitlockerVolumeRow(JLabel titleLabel, JTextField passwordField, JLabel statusLabel) { + this.titleLabel = titleLabel; this.passwordField = passwordField; + this.statusLabel = statusLabel; } } @@ -728,37 +702,72 @@ private List parseBitlockerVolumes(String message) { String recoveryKeyId = guidMatcher.find() ? StringUtils.trimToNull(guidMatcher.group(1)) : null; Matcher offsetMatcher = BITLOCKER_OFFSET_PATTERN.matcher(line); String volumeOffset = offsetMatcher.find() ? offsetMatcher.group(1) : null; - volumes.add(new BitlockerVolumeInfo(volumeOffset, recoveryKeyId)); + // The identifier/offset annotations are optional (e.g. a + // password-only-protected volume has neither), so the + // description is derived by stripping them rather than + // anchoring on either being present. + String description = StringUtils.trimToNull( + line.substring(line.indexOf(BITLOCKER_LINE_MARKER) + BITLOCKER_LINE_MARKER.length()) + .replaceAll(BITLOCKER_GUID_ANNOTATION_PATTERN.pattern(), "") + .replaceAll(BITLOCKER_OFFSET_PATTERN.pattern(), "")); + volumes.add(new BitlockerVolumeInfo(volumeOffset, recoveryKeyId, description)); } return volumes; } @NbBundle.Messages({ - "# {0} - volumeOffset", - "# {1} - recoveryKeyId", - "ImageFilePanel_bitlockerVolume_labelWithId=BitLocker volume at offset {0} (Recovery key ID: {1}):", + "# {0} - recoveryKeyId", + "ImageFilePanel_bitlockerVolume_labelWithId=BitLocker volume — Recovery key ID: {0}:", "# {0} - volumeOffset", "ImageFilePanel_bitlockerVolume_labelNoId=BitLocker volume at offset {0} (user password):", - "# {0} - recoveryKeyId", - "ImageFilePanel_bitlockerVolume_labelIdOnly=BitLocker volume (Recovery key ID: {0}):", - "ImageFilePanel_bitlockerVolume_labelPlain=BitLocker volume password:" + "ImageFilePanel_bitlockerVolume_labelPlain=BitLocker volume password:", + "ImageFilePanel_bitlockerVolume_statusLocked=Password required", + "ImageFilePanel_bitlockerVolume_statusUnlocked=✓ Unlocked" }) private static String getBitlockerVolumeLabel(BitlockerVolumeInfo volumeInfo) { - if (volumeInfo.volumeOffset != null && volumeInfo.recoveryKeyId != null) { - return Bundle.ImageFilePanel_bitlockerVolume_labelWithId(volumeInfo.volumeOffset, volumeInfo.recoveryKeyId); + // The recovery key ID is what BitLocker users record/reference; the + // volume offset is only shown as a fallback when there is no ID to + // tell two locked volumes apart (e.g. a user-password-only volume). + if (volumeInfo.recoveryKeyId != null) { + return Bundle.ImageFilePanel_bitlockerVolume_labelWithId(volumeInfo.recoveryKeyId); } else if (volumeInfo.volumeOffset != null) { return Bundle.ImageFilePanel_bitlockerVolume_labelNoId(volumeInfo.volumeOffset); - } else if (volumeInfo.recoveryKeyId != null) { - return Bundle.ImageFilePanel_bitlockerVolume_labelIdOnly(volumeInfo.recoveryKeyId); } return Bundle.ImageFilePanel_bitlockerVolume_labelPlain(); } /** - * Shows a labeled password field for each locked BitLocker volume. Fields - * for volumes that are already shown keep their contents; a volume that - * unlocks is no longer reported in the message but its field (and - * password) is kept so it remains part of the candidate list. + * Clears the per-volume BitLocker rows if the given path is no longer the + * currently selected image path. Unlike {@link #updateBitlockerVolumeRows}, + * this never creates rows or updates their lock status — it is meant to + * be called defensively before a real test-open-image result is known + * (e.g. at the top of {@code validatePanel()}), so it cannot flash a + * false "unlocked" status on an in-progress validation. + * + * @param imagePath The currently selected image path. + */ + private void clearStaleBitlockerVolumeRows(String imagePath) { + SwingUtilities.invokeLater(() -> { + if (!Objects.equals(imagePath, getContentPaths())) { + return; + } + synchronized (bitlockerVolumeRows) { + if (!Objects.equals(imagePath, bitlockerVolumesImagePath)) { + clearBitlockerVolumeRows(); + bitlockerVolumesImagePath = imagePath; + } + } + }); + } + + /** + * Shows a labeled password field for each locked BitLocker volume, and + * refreshes every row's live status. Fields for volumes that are already + * shown keep their contents; a volume that unlocks is no longer reported + * in the message but its field (and password) is kept so it remains part + * of the candidate list — its row instead switches to an "Unlocked" + * status. Call only with a real (possibly empty, on success) list of + * currently locked volumes from a completed test-open-image result. * * @param imagePath The image the volumes belong to; switching images * clears all fields. @@ -777,9 +786,10 @@ private void updateBitlockerVolumeRows(String imagePath, List lockedByKey = new HashMap<>(); for (BitlockerVolumeInfo volumeInfo : volumes) { String volumeKey = volumeInfo.getKey(); + lockedByKey.put(volumeKey, volumeInfo); if (bitlockerVolumeRows.containsKey(volumeKey)) { continue; } @@ -798,18 +808,19 @@ private void updateBitlockerVolumeRows(String imagePath, List entry : bitlockerVolumeRows.entrySet()) { + BitlockerVolumeInfo lockedInfo = lockedByKey.get(entry.getKey()); + BitlockerVolumeRow volumeRow = entry.getValue(); + if (lockedInfo != null) { + volumeRow.statusLabel.setText(StringUtils.defaultIfBlank( + lockedInfo.description, Bundle.ImageFilePanel_bitlockerVolume_statusLocked())); + volumeRow.statusLabel.setForeground(BITLOCKER_STATUS_LOCKED_COLOR); + } else { + volumeRow.statusLabel.setText(Bundle.ImageFilePanel_bitlockerVolume_statusUnlocked()); + volumeRow.statusLabel.setForeground(BITLOCKER_STATUS_UNLOCKED_COLOR); + } } + + bitlockerVolumesPanel.setVisible(!bitlockerVolumeRows.isEmpty()); + bitlockerVolumesPanel.revalidate(); + bitlockerVolumesPanel.repaint(); + this.revalidate(); + this.repaint(); } }); } @@ -852,20 +888,8 @@ private void clearBitlockerVolumeRows() { bitlockerVolumesPanel.setVisible(false); bitlockerVolumesPanel.revalidate(); bitlockerVolumesPanel.repaint(); - setMainPasswordFieldVisible(true); - } - - /** - * Shows or hides the single "Bitlocker Password" field. Its value is not - * cleared while hidden, so it keeps counting as a candidate password. - * - * @param visible True to show the field, false to hide it. - */ - private void setMainPasswordFieldVisible(boolean visible) { - passwordLabel.setVisible(visible); - passwordTextField.setVisible(visible); - revalidate(); - repaint(); + this.revalidate(); + this.repaint(); } public void reset() { @@ -874,7 +898,6 @@ public void reset() { this.md5HashTextField.setText(null); this.sha1HashTextField.setText(null); this.sha256HashTextField.setText(null); - this.passwordTextField.setText(null); SwingUtilities.invokeLater(() -> { synchronized (bitlockerVolumeRows) { clearBitlockerVolumeRows(); @@ -892,7 +915,6 @@ private void setUIEnabled(boolean enabled, boolean validNonE01) { SwingUtilities.invokeLater(() -> { this.browseButton.setEnabled(enabled); this.noFatOrphansCheckbox.setEnabled(enabled); - setTextFieldEnabled(this.passwordTextField, enabled); setTextFieldEnabled(this.pathTextField, enabled); this.sectorSizeComboBox.setEnabled(enabled); setTextFieldEnabled(this.md5HashTextField, enabled && validNonE01); @@ -955,8 +977,10 @@ public boolean validatePanel() { // A path change clears the rows of the previous image even // when validation exits early below; for an unchanged path - // this is a no-op that keeps the rows. - updateBitlockerVolumeRows(path, new ArrayList<>()); + // this is a no-op that keeps the rows. Uses the lightweight + // clear-only path so it can't flash a false "unlocked" + // status before the real test-open-image result is in. + clearStaleBitlockerVolumeRows(path); if (!isImagePathValid(path)) { showError(null); From 42cb6ebd63a780567f1f4ad4df3e52c8b6600a62 Mon Sep 17 00:00:00 2001 From: Ganesh <65601315+ganeshbs17@users.noreply.github.com> Date: Fri, 14 Aug 2026 22:37:50 +0530 Subject: [PATCH 5/5] Don't show a false Unlocked status when a locked volume can't be identified If an image starts with multiple locked BitLocker volumes and the last one still locked has no recovery-key protector, its status line carries neither a recovery key GUID nor a volume offset (the offset suffix is only emitted while more than one volume is locked), so it cannot be matched to the row it created earlier. The status-refresh pass then marked every row green "Unlocked" while the error banner still asked for a password. When an unidentifiable volume remains locked, unmatched rows now keep their previous status instead of claiming to be unlocked. All row passwords stay pooled as candidates, so entering the right password in any field still unlocks the volume. Co-Authored-By: Claude Fable 5 --- .../autopsy/casemodule/ImageFilePanel.java | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java index 4a49ab88d6b..e5cc361570d 100644 --- a/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java +++ b/Core/src/org/sleuthkit/autopsy/casemodule/ImageFilePanel.java @@ -850,6 +850,18 @@ private void updateBitlockerVolumeRows(String imagePath, List