Autopsy 4.22.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
ChromeCacheExtractor.java
Go to the documentation of this file.
1/*
2 *
3 * Autopsy Forensic Browser
4 *
5 * Copyright 2019-2021 Basis Technology Corp.
6 *
7 * Project Contact/Architect: carrier <at> sleuthkit <dot> org
8 *
9 * Licensed under the Apache License, Version 2.0 (the "License");
10 * you may not use this file except in compliance with the License.
11 * You may obtain a copy of the License at
12 *
13 * http://www.apache.org/licenses/LICENSE-2.0
14 *
15 * Unless required by applicable law or agreed to in writing, software
16 * distributed under the License is distributed on an "AS IS" BASIS,
17 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18 * See the License for the specific language governing permissions and
19 * limitations under the License.
20 */
21package org.sleuthkit.autopsy.recentactivity;
22
23import java.io.File;
24import java.io.FileOutputStream;
25import java.io.IOException;
26import java.io.RandomAccessFile;
27import java.nio.ByteBuffer;
28import java.nio.ByteOrder;
29import java.nio.channels.FileChannel;
30import java.nio.charset.Charset;
31import java.nio.file.Path;
32import java.nio.file.Paths;
33import java.util.ArrayList;
34import java.util.Arrays;
35import java.util.Collection;
36import java.util.Collections;
37import java.util.Comparator;
38import java.util.HashMap;
39import java.util.List;
40import java.util.Map;
41import java.util.Map.Entry;
42import java.util.Optional;
43import java.util.logging.Level;
44import org.openide.util.NbBundle;
45import org.openide.util.NbBundle.Messages;
46import org.sleuthkit.autopsy.casemodule.Case;
47import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
48import org.sleuthkit.autopsy.casemodule.services.FileManager;
49import org.sleuthkit.autopsy.coreutils.Logger;
50import org.sleuthkit.autopsy.coreutils.NetworkUtils;
51import org.sleuthkit.autopsy.datamodel.ContentUtils;
52import org.sleuthkit.autopsy.ingest.DataSourceIngestModuleProgress;
53import org.sleuthkit.autopsy.ingest.IngestJobContext;
54import org.sleuthkit.autopsy.ingest.IngestModule.IngestModuleException;
55import org.sleuthkit.autopsy.ingest.IngestServices;
56import org.sleuthkit.autopsy.ingest.ModuleContentEvent;
57import org.sleuthkit.datamodel.AbstractFile;
58import org.sleuthkit.datamodel.Blackboard;
59import org.sleuthkit.datamodel.BlackboardArtifact;
60import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
61import static org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE;
62import org.sleuthkit.datamodel.BlackboardAttribute;
63import org.sleuthkit.datamodel.Content;
64import org.sleuthkit.datamodel.DerivedFile;
65import org.sleuthkit.datamodel.OsAccount;
66import org.sleuthkit.datamodel.TimeUtilities;
67import org.sleuthkit.datamodel.TskCoreException;
68import org.sleuthkit.datamodel.TskData;
69import org.sleuthkit.datamodel.TskException;
70
95final class ChromeCacheExtractor {
96
97 private final static String DEFAULT_CACHE_PATH_STR = "default/cache"; //NON-NLS
98 private final static String BROTLI_MIMETYPE ="application/x-brotli"; //NON-NLS
99
100 private final static long UINT32_MASK = 0xFFFFFFFFl;
101
102 private final static int INDEXFILE_HDR_SIZE = 92*4;
103 private final static int DATAFILE_HDR_SIZE = 8192;
104
105 private final static Logger logger = Logger.getLogger(ChromeCacheExtractor.class.getName());
106
107 private static final String VERSION_NUMBER = "1.0.0"; //NON-NLS
108 private final String moduleName;
109
110 private String absOutputFolderName;
111 private String relOutputFolderName;
112
113 private final Content dataSource;
114 private final IngestJobContext context;
115 private final DataSourceIngestModuleProgress progressBar;
116 private final IngestServices services = IngestServices.getInstance();
117 private Case currentCase;
118 private FileManager fileManager;
119
120 // A file table to cache copies of index and data_n files.
121 private final Map<String, FileWrapper> fileCopyCache = new HashMap<>();
122
123 // A file table to cache the f_* files.
124 private final Map<String, AbstractFile> externalFilesTable = new HashMap<>();
125
131 final class FileWrapper {
132 private final AbstractFile abstractFile;
133 private final RandomAccessFile fileCopy;
134 private final ByteBuffer byteBuffer;
135
136 FileWrapper (AbstractFile abstractFile, RandomAccessFile fileCopy, ByteBuffer buffer ) {
137 this.abstractFile = abstractFile;
138 this.fileCopy = fileCopy;
139 this.byteBuffer = buffer;
140 }
141
142 public RandomAccessFile getFileCopy() {
143 return fileCopy;
144 }
145 public ByteBuffer getByteBuffer() {
146 return byteBuffer;
147 }
148 AbstractFile getAbstractFile() {
149 return abstractFile;
150 }
151 }
152
153 @NbBundle.Messages({
154 "# {0} - module name",
155 "# {1} - row number",
156 "# {2} - table length",
157 "# {3} - cache path",
158 "ChromeCacheExtractor.progressMsg={0}: Extracting cache entry {1} of {2} entries from {3}"
159 })
160 ChromeCacheExtractor(Content dataSource, IngestJobContext context, DataSourceIngestModuleProgress progressBar) {
161 moduleName = NbBundle.getMessage(Chromium.class, "Chrome.moduleName");
162 this.dataSource = dataSource;
163 this.context = context;
164 this.progressBar = progressBar;
165 }
166
167
173 private void moduleInit() throws IngestModuleException {
174
175 try {
176 currentCase = Case.getCurrentCaseThrows();
177 fileManager = currentCase.getServices().getFileManager();
178
179 } catch (NoCurrentCaseException ex) {
180 String msg = "Failed to get current case."; //NON-NLS
181 throw new IngestModuleException(msg, ex);
182 }
183 }
184
192 private void resetForNewCacheFolder(String cachePath) throws IngestModuleException {
193
194 fileCopyCache.clear();
195 externalFilesTable.clear();
196
197 String cacheAbsOutputFolderName = this.getAbsOutputFolderName() + cachePath;
198 File outDir = new File(cacheAbsOutputFolderName);
199 if (outDir.exists() == false) {
200 outDir.mkdirs();
201 }
202
203 String cacheTempPath = RAImageIngestModule.getRATempPath(currentCase, moduleName, context.getJobId()) + cachePath;
204 File tempDir = new File(cacheTempPath);
205 if (tempDir.exists() == false) {
206 tempDir.mkdirs();
207 }
208 }
209
216 private void cleanup () {
217
218 for (Entry<String, FileWrapper> entry : this.fileCopyCache.entrySet()) {
219 Path tempFilePath = Paths.get(RAImageIngestModule.getRATempPath(currentCase, moduleName, context.getJobId()), entry.getKey() );
220 try {
221 entry.getValue().getFileCopy().getChannel().close();
222 entry.getValue().getFileCopy().close();
223
224 File tmpFile = tempFilePath.toFile();
225 if (!tmpFile.delete()) {
226 tmpFile.deleteOnExit();
227 }
228 } catch (IOException ex) {
229 logger.log(Level.WARNING, String.format("Failed to delete cache file copy %s", tempFilePath.toString()), ex); //NON-NLS
230 }
231 }
232 }
233
239 private String getAbsOutputFolderName() {
240 return absOutputFolderName;
241 }
242
248 private String getRelOutputFolderName() {
249 return relOutputFolderName;
250 }
251
258 void processCaches() {
259
260 try {
261 moduleInit();
262 } catch (IngestModuleException ex) {
263 String msg = "Failed to initialize ChromeCacheExtractor."; //NON-NLS
264 logger.log(Level.SEVERE, msg, ex);
265 return;
266 }
267
268 // Find and process the cache folders. There could be one per user
269 try {
270 // Identify each cache folder by searching for the index files in each
271 List<AbstractFile> indexFiles = findIndexFiles();
272
273 if (indexFiles.size() > 0) {
274 // Create an output folder to save any derived files
275 absOutputFolderName = RAImageIngestModule.getRAOutputPath(currentCase, moduleName, context.getJobId());
276 relOutputFolderName = Paths.get(RAImageIngestModule.getRelModuleOutputPath(currentCase, moduleName, context.getJobId())).normalize().toString();
277
278 File dir = new File(absOutputFolderName);
279 if (dir.exists() == false) {
280 dir.mkdirs();
281 }
282 }
283
284 // Process each of the cache folders
285 for (AbstractFile indexFile: indexFiles) {
286
287 if (context.dataSourceIngestIsCancelled()) {
288 return;
289 }
290
291 if (indexFile.getSize() > 0) {
292 processCacheFolder(indexFile);
293 }
294 }
295
296 } catch (TskCoreException ex) {
297 String msg = "Failed to find cache index files"; //NON-NLS
298 logger.log(Level.WARNING, msg, ex);
299 }
300 }
301
302 @Messages({
303 "ChromeCacheExtract_adding_extracted_files_msg=Chrome Cache: Adding %d extracted files for analysis.",
304 "ChromeCacheExtract_adding_artifacts_msg=Chrome Cache: Adding %d artifacts for analysis.",
305 "ChromeCacheExtract_loading_files_msg=Chrome Cache: Loading files from %s."
306 })
307
314 private void processCacheFolder(AbstractFile indexFile) {
315
316 String cacheFolderName = indexFile.getParentPath();
317 Optional<FileWrapper> indexFileWrapper;
318
319 /*
320 * The first part of this method is all about finding the needed files in the cache
321 * folder and making internal copies/caches of them so that we can later process them
322 * and effeciently look them up.
323 */
324 try {
325 progressBar.progress(String.format(Bundle.ChromeCacheExtract_loading_files_msg(), cacheFolderName));
326 resetForNewCacheFolder(cacheFolderName);
327
328 // @@@ This is little ineffecient because we later in this call search for the AbstractFile that we currently have
329 // Load the index file into the caches
330 indexFileWrapper = findDataOrIndexFile(indexFile.getName(), cacheFolderName);
331 if (!indexFileWrapper.isPresent()) {
332 String msg = String.format("Failed to find copy cache index file %s", indexFile.getUniquePath());
333 logger.log(Level.WARNING, msg);
334 return;
335 }
336
337
338 // load the data files into the internal cache. We do this because we often
339 // jump in between the various data_X files resolving segments
340 for (int i = 0; i < 4; i ++) {
341 Optional<FileWrapper> dataFile = findDataOrIndexFile(String.format("data_%1d",i), cacheFolderName );
342 if (!dataFile.isPresent()) {
343 return;
344 }
345 }
346
347 // find all f_* files in a single query and load them into the cache
348 // we do this here so that it is a single query instead of hundreds of individual ones
349 findExternalFiles(cacheFolderName);
350
351 } catch (TskCoreException | IngestModuleException ex) {
352 String msg = "Failed to find cache files in path " + cacheFolderName; //NON-NLS
353 logger.log(Level.WARNING, msg, ex);
354 return;
355 }
356
357 /*
358 * Now the analysis begins. We parse the index file and that drives parsing entries
359 * from data_X or f_XXXX files.
360 */
361 logger.log(Level.INFO, "{0}- Now reading Cache index file from path {1}", new Object[]{moduleName, cacheFolderName }); //NON-NLS
362
363 List<AbstractFile> derivedFiles = new ArrayList<>();
364 Collection<BlackboardArtifact> artifactsAdded = new ArrayList<>();
365
366 ByteBuffer indexFileROBuffer = indexFileWrapper.get().getByteBuffer();
367 IndexFileHeader indexHdr = new IndexFileHeader(indexFileROBuffer);
368
369 // seek past the header
370 indexFileROBuffer.position(INDEXFILE_HDR_SIZE);
371
372 try {
373 /* Cycle through index and get the CacheAddress for each CacheEntry. Process each entry
374 * to extract data, add artifacts, etc. from the f_XXXX and data_x files */
375 for (int i = 0; i < indexHdr.getTableLen(); i++) {
376
377 if (context.dataSourceIngestIsCancelled()) {
378 cleanup();
379 return;
380 }
381
382 CacheAddress addr = new CacheAddress(indexFileROBuffer.getInt() & UINT32_MASK, cacheFolderName);
383 if (addr.isInitialized()) {
384 progressBar.progress(NbBundle.getMessage(this.getClass(),
385 "ChromeCacheExtractor.progressMsg",
386 moduleName, i, indexHdr.getTableLen(), cacheFolderName) );
387 try {
388 List<DerivedFile> addedFiles = processCacheEntry(addr, artifactsAdded);
389 derivedFiles.addAll(addedFiles);
390 }
391 catch (TskCoreException | IngestModuleException ex) {
392 logger.log(Level.WARNING, String.format("Failed to get cache entry at address %s for file with object ID %d (%s)", addr, indexFile.getId(), ex.getLocalizedMessage())); //NON-NLS
393 }
394 }
395 }
396 } catch (java.nio.BufferUnderflowException ex) {
397 logger.log(Level.WARNING, String.format("Ran out of data unexpectedly reading file %s (ObjID: %d)", indexFile.getName(), indexFile.getId()));
398 }
399
400 if (context.dataSourceIngestIsCancelled()) {
401 cleanup();
402 return;
403 }
404
405
406 // notify listeners of new files and schedule for analysis
407 progressBar.progress(String.format(Bundle.ChromeCacheExtract_adding_extracted_files_msg(), derivedFiles.size()));
408 derivedFiles.forEach((derived) -> {
409 services.fireModuleContentEvent(new ModuleContentEvent(derived));
410 });
411 context.addFilesToJob(derivedFiles);
412
413 // notify listeners about new artifacts
414 progressBar.progress(String.format(Bundle.ChromeCacheExtract_adding_artifacts_msg(), artifactsAdded.size()));
415 Blackboard blackboard = currentCase.getSleuthkitCase().getBlackboard();
416 try {
417 blackboard.postArtifacts(artifactsAdded, moduleName, context.getJobId());
418 } catch (Blackboard.BlackboardException ex) {
419 logger.log(Level.WARNING, String.format("Failed to post cacheIndex artifacts "), ex); //NON-NLS
420 }
421
422 cleanup();
423 }
424
437 private List<DerivedFile> processCacheEntry(CacheAddress cacheAddress, Collection<BlackboardArtifact> artifactsAdded ) throws TskCoreException, IngestModuleException {
438
439 List<DerivedFile> derivedFiles = new ArrayList<>();
440
441 // get the path to the corresponding data_X file for the cache entry
442 String cacheEntryFileName = cacheAddress.getFilename();
443 String cachePath = cacheAddress.getCachePath();
444
445 Optional<FileWrapper> cacheEntryFileOptional = findDataOrIndexFile(cacheEntryFileName, cachePath);
446 if (!cacheEntryFileOptional.isPresent()) {
447 String msg = String.format("Failed to find data file %s", cacheEntryFileName); //NON-NLS
448 throw new IngestModuleException(msg);
449 }
450
451 // Load the entry to get its metadata, segments, etc.
452 CacheEntry cacheEntry = new CacheEntry(cacheAddress, cacheEntryFileOptional.get() );
453 List<CacheDataSegment> dataSegments = cacheEntry.getDataSegments();
454
455 // Only process the first payload data segment in each entry
456 // first data segement has the HTTP headers, 2nd is the payload
457 if (dataSegments.size() < 2) {
458 return derivedFiles;
459 }
460 CacheDataSegment dataSegment = dataSegments.get(1);
461
462 // Name where segment is located (could be diffrent from where entry was located)
463 String segmentFileName = dataSegment.getCacheAddress().getFilename();
464 Optional<AbstractFile> segmentFileAbstractFile = findAbstractFile(segmentFileName, cachePath);
465 if (!segmentFileAbstractFile.isPresent()) {
466 logger.log(Level.WARNING, "Error finding segment file: " + cachePath + "/" + segmentFileName); //NON-NLS
467 return derivedFiles;
468 }
469
470 boolean isBrotliCompressed = false;
471 if (dataSegment.getType() != CacheDataTypeEnum.HTTP_HEADER && cacheEntry.isBrotliCompressed() ) {
472 isBrotliCompressed = true;
473 }
474
475
476 // Make artifacts around the cached item and extract data from data_X file
477 try {
478 AbstractFile cachedItemFile; //
479 /* If the cached data is in a f_XXXX file, we only need to make artifacts. */
480 if (dataSegment.isInExternalFile() ) {
481 cachedItemFile = segmentFileAbstractFile.get();
482 }
483 /* If the data is in a data_X file, we need to extract it out and then make the similar artifacts */
484 else {
485
486 // Data segments in "data_x" files are saved in individual files and added as derived files
487 String filename = dataSegment.save();
488 String relPathname = getRelOutputFolderName() + dataSegment.getCacheAddress().getCachePath() + filename;
489
490 // @@@ We should batch these up and do them in one big insert / transaction
491 DerivedFile derivedFile = fileManager.addDerivedFile(filename, relPathname,
492 dataSegment.getDataLength(),
493 cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), // TBD
494 true,
495 segmentFileAbstractFile.get(),
496 "",
497 moduleName,
498 VERSION_NUMBER,
499 "",
500 TskData.EncodingType.NONE);
501
502 derivedFiles.add(derivedFile);
503 cachedItemFile = derivedFile;
504 }
505
506 addArtifacts(cacheEntry, cacheEntryFileOptional.get().getAbstractFile(), cachedItemFile, artifactsAdded);
507
508 // Tika doesn't detect these types. So, make sure they have the correct MIME type */
509 if (isBrotliCompressed) {
510 cachedItemFile.setMIMEType(BROTLI_MIMETYPE);
511 cachedItemFile.save();
512 }
513
514 } catch (TskException ex) {
515 logger.log(Level.SEVERE, "Error while trying to add an artifact", ex); //NON-NLS
516 }
517
518 return derivedFiles;
519 }
520
530 private void addArtifacts(CacheEntry cacheEntry, AbstractFile cacheEntryFile, AbstractFile cachedItemFile, Collection<BlackboardArtifact> artifactsAdded) throws TskCoreException {
531
532 // Create a TSK_WEB_CACHE entry with the parent as data_X file that had the cache entry
533 Collection<BlackboardAttribute> webAttr = new ArrayList<>();
534 String url = cacheEntry.getKey() != null ? cacheEntry.getKey() : "";
535 webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
536 moduleName, url));
537 webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
538 moduleName, NetworkUtils.extractDomain(url)));
539 webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED,
540 moduleName, cacheEntry.getCreationTime()));
541 webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_HEADERS,
542 moduleName, cacheEntry.getHTTPHeaders()));
543 webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH,
544 moduleName, cachedItemFile.getUniquePath()));
545 webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID,
546 moduleName, cachedItemFile.getId()));
547
548 BlackboardArtifact webCacheArtifact = cacheEntryFile.newDataArtifact(new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_WEB_CACHE), webAttr);
549 artifactsAdded.add(webCacheArtifact);
550
551 // Create a TSK_ASSOCIATED_OBJECT on the f_XXX or derived file file back to the CACHE entry
552 BlackboardArtifact associatedObjectArtifact = cachedItemFile.newDataArtifact(
553 new BlackboardArtifact.Type(ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT),
554 Arrays.asList(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT,
555 moduleName, webCacheArtifact.getArtifactID())));
556
557 artifactsAdded.add(associatedObjectArtifact);
558 }
559
568 private void findExternalFiles(String cachePath) throws TskCoreException {
569
570 List<AbstractFile> effFiles = fileManager.findFiles(dataSource, "f_%", cachePath); //NON-NLS
571 for (AbstractFile abstractFile : effFiles ) {
572 String cacheKey = cachePath + abstractFile.getName();
573 if (cachePath.equals(abstractFile.getParentPath()) && abstractFile.isFile()) {
574 // Don't overwrite an allocated version with an unallocated version
575 if (abstractFile.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)
576 || !externalFilesTable.containsKey(cacheKey)) {
577 this.externalFilesTable.put(cacheKey, abstractFile);
578 }
579 }
580 }
581 }
590 private Optional<AbstractFile> findAbstractFile(String cacheFileName, String cacheFolderName) throws TskCoreException {
591
592 // see if it is cached
593 String fileTableKey = cacheFolderName + cacheFileName;
594
595 if (cacheFileName != null) {
596 if (cacheFileName.startsWith("f_") && externalFilesTable.containsKey(fileTableKey)) {
597 return Optional.of(externalFilesTable.get(fileTableKey));
598 }
599 } else {
600 return Optional.empty();
601 }
602
603 if (fileCopyCache.containsKey(fileTableKey)) {
604 return Optional.of(fileCopyCache.get(fileTableKey).getAbstractFile());
605 }
606
607 List<AbstractFile> cacheFiles = currentCase.getSleuthkitCase().getFileManager().findFilesExactNameExactPath(dataSource,
608 cacheFileName, cacheFolderName);
609 if (!cacheFiles.isEmpty()) {
610 // Sort the list for consistency. Preference is:
611 // - In correct subfolder and allocated
612 // - In correct subfolder and unallocated
613 // - In incorrect subfolder and allocated
614 Collections.sort(cacheFiles, new Comparator<AbstractFile>() {
615 @Override
616 public int compare(AbstractFile file1, AbstractFile file2) {
617 try {
618 if (file1.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)
619 && ! file2.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)) {
620 return -1;
621 }
622
623 if (file2.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)
624 && ! file1.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)) {
625 return 1;
626 }
627 } catch (TskCoreException ex) {
628 logger.log(Level.WARNING, "Error getting unique path for file with ID " + file1.getId() + " or " + file2.getId(), ex);
629 }
630
631 if (file1.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)
632 && ! file2.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)) {
633 return -1;
634 }
635 if (file2.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)
636 && ! file1.isMetaFlagSet(TskData.TSK_FS_META_FLAG_ENUM.ALLOC)) {
637 return 1;
638 }
639
640 return Long.compare(file1.getId(), file2.getId());
641 }
642 });
643
644 // The best match will be the first element
645 return Optional.of(cacheFiles.get(0));
646 }
647
648 return Optional.empty();
649 }
650
658 private List<AbstractFile> findIndexFiles() throws TskCoreException {
659 return fileManager.findFiles(dataSource, "index", DEFAULT_CACHE_PATH_STR); //NON-NLS
660 }
661
662
663
675 private Optional<FileWrapper> findDataOrIndexFile(String cacheFileName, String cacheFolderName) throws TskCoreException, IngestModuleException {
676
677 // Check if the file is already in the cache
678 String fileTableKey = cacheFolderName + cacheFileName;
679 if (fileCopyCache.containsKey(fileTableKey)) {
680 return Optional.of(fileCopyCache.get(fileTableKey));
681 }
682
683 // Use Autopsy to get the AbstractFile
684 Optional<AbstractFile> abstractFileOptional = findAbstractFile(cacheFileName, cacheFolderName);
685 if (!abstractFileOptional.isPresent()) {
686 return Optional.empty();
687 }
688
689 // Wrap the file so that we can get the ByteBuffer later.
690 // @@@ BC: I think this should nearly all go into FileWrapper and be done lazily and perhaps based on size.
691 // Many of the files are small enough to keep in memory for the ByteBuffer
692
693 // write the file to disk so that we can have a memory-mapped ByteBuffer
694 AbstractFile cacheFile = abstractFileOptional.get();
695 RandomAccessFile randomAccessFile = null;
696 String tempFilePathname = RAImageIngestModule.getRATempPath(currentCase, moduleName, context.getJobId()) + cacheFolderName + cacheFile.getName(); //NON-NLS
697 try {
698 File newFile = new File(tempFilePathname);
699 ContentUtils.writeToFile(cacheFile, newFile, context::dataSourceIngestIsCancelled);
700
701 randomAccessFile = new RandomAccessFile(tempFilePathname, "r");
702 FileChannel roChannel = randomAccessFile.getChannel();
703 ByteBuffer cacheFileROBuf = roChannel.map(FileChannel.MapMode.READ_ONLY, 0,
704 (int) roChannel.size());
705
706 cacheFileROBuf.order(ByteOrder.nativeOrder());
707 FileWrapper cacheFileWrapper = new FileWrapper(cacheFile, randomAccessFile, cacheFileROBuf );
708
709 if (!cacheFileName.startsWith("f_")) {
710 fileCopyCache.put(cacheFolderName + cacheFileName, cacheFileWrapper);
711 }
712
713 return Optional.of(cacheFileWrapper);
714 }
715 catch (IOException ex) {
716
717 try {
718 if (randomAccessFile != null) {
719 randomAccessFile.close();
720 }
721 }
722 catch (IOException ex2) {
723 logger.log(Level.SEVERE, "Error while trying to close temp file after exception.", ex2); //NON-NLS
724 }
725 String msg = String.format("Error reading/copying Chrome cache file '%s' (id=%d).", //NON-NLS
726 cacheFile.getName(), cacheFile.getId());
727 throw new IngestModuleException(msg, ex);
728 }
729 }
730
734 final class IndexFileHeader {
735
736 private final long magic;
737 private final int version;
738 private final int numEntries;
739 private final int numBytes;
740 private final int lastFile;
741 private final int tableLen;
742
743 IndexFileHeader(ByteBuffer indexFileROBuf) {
744
745 magic = indexFileROBuf.getInt() & UINT32_MASK;
746
747 indexFileROBuf.position(indexFileROBuf.position()+2);
748
749 version = indexFileROBuf.getShort();
750 numEntries = indexFileROBuf.getInt();
751 numBytes = indexFileROBuf.getInt();
752 lastFile = indexFileROBuf.getInt();
753
754 indexFileROBuf.position(indexFileROBuf.position()+4); // this_id
755 indexFileROBuf.position(indexFileROBuf.position()+4); // stats cache cacheAddress
756
757 tableLen = indexFileROBuf.getInt();
758 }
759
760 public long getMagic() {
761 return magic;
762 }
763
764 public int getVersion() {
765 return version;
766 }
767
768 public int getNumEntries() {
769 return numEntries;
770 }
771
772 public int getNumBytes() {
773 return numBytes;
774 }
775
776 public int getLastFile() {
777 return lastFile;
778 }
779
780 public int getTableLen() {
781 return tableLen;
782 }
783
784 @Override
785 public String toString() {
786 StringBuilder sb = new StringBuilder();
787
788 sb.append(String.format("Index Header:"))
789 .append(String.format("\tMagic = %x" , getMagic()) )
790 .append(String.format("\tVersion = %x" , getVersion()) )
791 .append(String.format("\tNumEntries = %x" , getNumEntries()) )
792 .append(String.format("\tNumBytes = %x" , getNumBytes()) )
793 .append(String.format("\tLastFile = %x" , getLastFile()) )
794 .append(String.format("\tTableLen = %x" , getTableLen()) );
795
796 return sb.toString();
797 }
798 }
799
813
814
815
838 final class CacheAddress {
839 // sundry constants to parse the bit fields
840 private static final long ADDR_INITIALIZED_MASK = 0x80000000l;
841 private static final long FILE_TYPE_MASK = 0x70000000;
842 private static final long FILE_TYPE_OFFSET = 28;
843 private static final long NUM_BLOCKS_MASK = 0x03000000;
844 private static final long NUM_BLOCKS_OFFSET = 24;
845 private static final long FILE_SELECTOR_MASK = 0x00ff0000;
846 private static final long FILE_SELECTOR_OFFSET = 16;
847 private static final long START_BLOCK_MASK = 0x0000FFFF;
848 private static final long EXTERNAL_FILE_NAME_MASK = 0x0FFFFFFF;
849
850 private final long uint32CacheAddr;
851 private final CacheFileTypeEnum fileType;
852 private final int numBlocks;
853 private final int startBlock;
854 private final String fileName;
855 private final int fileNumber;
856
857 private final String cachePath;
858
859
865 CacheAddress(long uint32, String cachePath) {
866
867 uint32CacheAddr = uint32;
868 this.cachePath = cachePath;
869
870
871 // analyze the
872 int fileTypeEnc = (int)(uint32CacheAddr & FILE_TYPE_MASK) >> FILE_TYPE_OFFSET;
873 fileType = CacheFileTypeEnum.values()[fileTypeEnc];
874
875 if (isInitialized()) {
876 if (isInExternalFile()) {
877 fileNumber = (int)(uint32CacheAddr & EXTERNAL_FILE_NAME_MASK);
878 fileName = String.format("f_%06x", getFileNumber() );
879 numBlocks = 0;
880 startBlock = 0;
881 } else {
882 fileNumber = (int)((uint32CacheAddr & FILE_SELECTOR_MASK) >> FILE_SELECTOR_OFFSET);
883 fileName = String.format("data_%d", getFileNumber() );
884 numBlocks = (int)(uint32CacheAddr & NUM_BLOCKS_MASK >> NUM_BLOCKS_OFFSET);
885 startBlock = (int)(uint32CacheAddr & START_BLOCK_MASK);
886 }
887 }
888 else {
889 fileName = null;
890 fileNumber = 0;
891 numBlocks = 0;
892 startBlock = 0;
893 }
894 }
895
896 boolean isInitialized() {
897 return ((uint32CacheAddr & ADDR_INITIALIZED_MASK) != 0);
898 }
899
900 CacheFileTypeEnum getFileType() {
901 return fileType;
902 }
903
908 String getFilename() {
909 return fileName;
910 }
911
912 String getCachePath() {
913 return cachePath;
914 }
915
916 boolean isInExternalFile() {
917 return (fileType == CacheFileTypeEnum.EXTERNAL);
918 }
919
920 int getFileNumber() {
921 return fileNumber;
922 }
923
924 int getStartBlock() {
925 return startBlock;
926 }
927
928 int getNumBlocks() {
929 return numBlocks;
930 }
931
932 int getBlockSize() {
933 switch (fileType) {
934 case RANKINGS:
935 return 36;
936 case BLOCK_256:
937 return 256;
938 case BLOCK_1K:
939 return 1024;
940 case BLOCK_4K:
941 return 4096;
942 case BLOCK_FILES:
943 return 8;
944 case BLOCK_ENTRIES:
945 return 104;
946 case BLOCK_EVICTED:
947 return 48;
948 default:
949 return 0;
950 }
951 }
952
953 public long getUint32CacheAddr() {
954 return uint32CacheAddr;
955 }
956
957 @Override
958 public String toString() {
959 StringBuilder sb = new StringBuilder();
960 sb.append(String.format("CacheAddr %08x : %s : filename %s",
961 uint32CacheAddr,
962 isInitialized() ? "Initialized" : "UnInitialized",
963 getFilename()));
964
965 if ((fileType == CacheFileTypeEnum.BLOCK_256) ||
966 (fileType == CacheFileTypeEnum.BLOCK_1K) ||
967 (fileType == CacheFileTypeEnum.BLOCK_4K) ) {
968 sb.append(String.format(" (%d blocks starting at %08X)",
969 this.getNumBlocks(),
970 this.getStartBlock()
971 ));
972 }
973
974 return sb.toString();
975 }
976
977 }
978
986
996 final class CacheDataSegment {
997
998 private int length;
999 private final CacheAddress cacheAddress;
1000 private CacheDataTypeEnum type;
1001
1002 private boolean isHTTPHeaderHint;
1003
1004 private FileWrapper cacheFileCopy = null;
1005 private byte[] data = null;
1006
1007 private String httpResponse;
1008 private final Map<String, String> httpHeaders = new HashMap<>();
1009
1010 CacheDataSegment(CacheAddress cacheAddress, int len) {
1011 this(cacheAddress, len, false);
1012 }
1013
1014 CacheDataSegment(CacheAddress cacheAddress, int len, boolean isHTTPHeader ) {
1015 this.type = CacheDataTypeEnum.UNKNOWN;
1016 this.length = len;
1017 this.cacheAddress = cacheAddress;
1018 this.isHTTPHeaderHint = isHTTPHeader;
1019 }
1020
1021 boolean isInExternalFile() {
1022 return cacheAddress.isInExternalFile();
1023 }
1024
1025 boolean hasHTTPHeaders() {
1026 return this.type == CacheDataTypeEnum.HTTP_HEADER;
1027 }
1028
1029 String getHTTPHeader(String key) {
1030 return this.httpHeaders.get(key);
1031 }
1032
1038 String getHTTPHeaders() {
1039 if (!hasHTTPHeaders()) {
1040 return "";
1041 }
1042
1043 StringBuilder sb = new StringBuilder();
1044 httpHeaders.entrySet().forEach((entry) -> {
1045 if (sb.length() > 0) {
1046 sb.append(" \n");
1047 }
1048 sb.append(String.format("%s : %s",
1049 entry.getKey(), entry.getValue()));
1050 });
1051
1052 return sb.toString();
1053 }
1054
1055 String getHTTPRespone() {
1056 return httpResponse;
1057 }
1058
1064 void extract() throws TskCoreException, IngestModuleException {
1065
1066 // do nothing if already extracted,
1067 if (data != null) {
1068 return;
1069 }
1070
1071 // Don't extract data from external files.
1072 if (!cacheAddress.isInExternalFile()) {
1073
1074 if (cacheAddress.getFilename() == null) {
1075 throw new TskCoreException("Cache address has no file name");
1076 }
1077
1078 cacheFileCopy = findDataOrIndexFile(cacheAddress.getFilename(), cacheAddress.getCachePath()).get();
1079
1080 this.data = new byte [length];
1081 ByteBuffer buf = cacheFileCopy.getByteBuffer();
1082 int dataOffset = DATAFILE_HDR_SIZE + cacheAddress.getStartBlock() * cacheAddress.getBlockSize();
1083 if (dataOffset > buf.capacity()) {
1084 return;
1085 }
1086 buf.position(dataOffset);
1087 buf.get(data, 0, length);
1088
1089 // if this might be a HTPP header, lets try to parse it as such
1090 if ((isHTTPHeaderHint)) {
1091 String strData = new String(data);
1092 if (strData.contains("HTTP")) {
1093
1094 // Http headers if present, are usually in frst data segment in an entry
1095 // General Parsing algo:
1096 // - Find start of HTTP header by searching for string "HTTP"
1097 // - Skip to the first 0x00 to get to the end of the HTTP response line, this makrs start of headers section
1098 // - Find the end of the header by searching for 0x00 0x00 bytes
1099 // - Extract the headers section
1100 // - Parse the headers section - each null terminated string is a header
1101 // - Each header is of the format "name: value" e.g.
1102
1103 type = CacheDataTypeEnum.HTTP_HEADER;
1104
1105 int startOff = strData.indexOf("HTTP");
1106 Charset charset = Charset.forName("UTF-8");
1107 boolean done = false;
1108 int i = startOff;
1109 int hdrNum = 1;
1110
1111 while (!done) {
1112 // each header is null terminated
1113 int start = i;
1114 while (i < data.length && data[i] != 0) {
1115 i++;
1116 }
1117
1118 // http headers are terminated by 0x00 0x00
1119 if (i == data.length || data[i+1] == 0) {
1120 done = true;
1121 }
1122
1123 int len = (i - start);
1124 String headerLine = new String(data, start, len, charset);
1125
1126 // first line is the http response
1127 if (hdrNum == 1) {
1128 httpResponse = headerLine;
1129 } else {
1130 int nPos = headerLine.indexOf(':');
1131 if (nPos > 0 ) {
1132 String key = headerLine.substring(0, nPos);
1133 String val= headerLine.substring(nPos+1);
1134 httpHeaders.put(key.toLowerCase(), val);
1135 }
1136 }
1137
1138 i++;
1139 hdrNum++;
1140 }
1141 }
1142 }
1143 }
1144 }
1145
1146 String getDataString() throws TskCoreException, IngestModuleException {
1147 if (data == null) {
1148 extract();
1149 }
1150 return new String(data);
1151 }
1152
1153 byte[] getDataBytes() throws TskCoreException, IngestModuleException {
1154 if (data == null) {
1155 extract();
1156 }
1157 return data.clone();
1158 }
1159
1160 int getDataLength() {
1161 return this.length;
1162 }
1163
1164 CacheDataTypeEnum getType() {
1165 return type;
1166 }
1167
1168 CacheAddress getCacheAddress() {
1169 return cacheAddress;
1170 }
1171
1172
1181 String save() throws TskCoreException, IngestModuleException {
1182 String fileName;
1183
1184 if (cacheAddress.isInExternalFile()) {
1185 fileName = cacheAddress.getFilename();
1186 } else {
1187 fileName = String.format("%s__%08x", cacheAddress.getFilename(), cacheAddress.getUint32CacheAddr());
1188 }
1189
1190 String filePathName = getAbsOutputFolderName() + cacheAddress.getCachePath() + fileName;
1191 save(filePathName);
1192
1193 return fileName;
1194 }
1195
1204
1205 void save(String filePathName) throws TskCoreException, IngestModuleException {
1206
1207 // Save the data to specified file
1208 if (data == null) {
1209 extract();
1210 }
1211
1212 // Data in external files is not saved in local files
1213 if (!this.isInExternalFile()) {
1214 // write the
1215 try (FileOutputStream stream = new FileOutputStream(filePathName)) {
1216 stream.write(data);
1217 } catch (IOException ex) {
1218 throw new TskCoreException(String.format("Failed to write output file %s", filePathName), ex);
1219 }
1220 }
1221 }
1222
1223 @Override
1224 public String toString() {
1225 StringBuilder strBuilder = new StringBuilder();
1226 strBuilder.append(String.format("\t\tData type = : %s, Data Len = %d ",
1227 this.type.toString(), this.length ));
1228
1229 if (hasHTTPHeaders()) {
1230 String str = getHTTPHeader("content-encoding");
1231 if (str != null) {
1232 strBuilder.append(String.format("\t%s=%s", "content-encoding", str ));
1233 }
1234 }
1235
1236 return strBuilder.toString();
1237 }
1238
1239 }
1240
1241
1250
1251
1252// Main structure for an entry on the backing storage.
1253//
1254// Each entry has a key, identifying the URL the cache entry pertains to.
1255// If the key is longer than
1256// what can be stored on this structure, it will be extended on consecutive
1257// blocks (adding 256 bytes each time), up to 4 blocks (1024 - 32 - 1 chars).
1258// After that point, the whole key will be stored as a data block or external
1259// file.
1260//
1261// Each entry can have upto 4 data segments
1262//
1263// struct EntryStore {
1264// uint32 hash; // Full hash of the key.
1265// CacheAddr next; // Next entry with the same hash or bucket.
1266// CacheAddr rankings_node; // Rankings node for this entry.
1267// int32 reuse_count; // How often is this entry used.
1268// int32 refetch_count; // How often is this fetched from the net.
1269// int32 state; // Current state.
1270// uint64 creation_time;
1271// int32 key_len;
1272// CacheAddr long_key; // Optional cacheAddress of a long key.
1273// int32 data_size[4]; // We can store up to 4 data streams for each
1274// CacheAddr data_addr[4]; // entry.
1275// uint32 flags; // Any combination of EntryFlags.
1276// int32 pad[4];
1277// uint32 self_hash; // The hash of EntryStore up to this point.
1278// char key[256 - 24 * 4]; // null terminated
1279// };
1280
1284 final class CacheEntry {
1285
1286 // each entry is 256 bytes. The last section of the entry, after all the other fields is a null terminated key
1287 private static final int MAX_KEY_LEN = 256-24*4;
1288
1289 private final CacheAddress selfAddress;
1290 private final FileWrapper cacheFileCopy;
1291
1292 private final long hash;
1293 private final CacheAddress nextAddress;
1294 private final CacheAddress rankingsNodeAddress;
1295
1296 private final int reuseCount;
1297 private final int refetchCount;
1298 private final EntryStateEnum state;
1299
1300 private final long creationTime;
1301 private final int keyLen;
1302
1303 private final CacheAddress longKeyAddresses; // cacheAddress of the key, if the key is external to the entry
1304
1305 private final int[] dataSegmentSizes;
1306 private final CacheAddress[] dataSegmentIndexFileEntries;
1307 private List<CacheDataSegment> dataSegments;
1308
1309 private final long flags;
1310
1311 private String key; // Key may be found within the entry or may be external
1312
1313 CacheEntry(CacheAddress cacheAdress, FileWrapper cacheFileCopy ) throws TskCoreException, IngestModuleException {
1314 this.selfAddress = cacheAdress;
1315 this.cacheFileCopy = cacheFileCopy;
1316
1317 ByteBuffer fileROBuf = cacheFileCopy.getByteBuffer();
1318
1319 int entryOffset = DATAFILE_HDR_SIZE + cacheAdress.getStartBlock() * cacheAdress.getBlockSize();
1320
1321 // reposition the buffer to the the correct offset
1322 if (entryOffset < fileROBuf.capacity()) {
1323 fileROBuf.position(entryOffset);
1324 } else {
1325 throw new IngestModuleException("Position seeked in Buffer to big"); // NON-NLS
1326 }
1327
1328 hash = fileROBuf.getInt() & UINT32_MASK;
1329
1330 long uint32 = fileROBuf.getInt() & UINT32_MASK;
1331 nextAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1332
1333 uint32 = fileROBuf.getInt() & UINT32_MASK;
1334 rankingsNodeAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1335
1336 reuseCount = fileROBuf.getInt();
1337 refetchCount = fileROBuf.getInt();
1338
1339 int stateVal = fileROBuf.getInt();
1340 if ((stateVal >= 0) && (stateVal < EntryStateEnum.values().length)) {
1341 state = EntryStateEnum.values()[stateVal];
1342 } else {
1343 throw new TskCoreException("Invalid EntryStateEnum value"); // NON-NLS
1344 }
1345 creationTime = (fileROBuf.getLong() / 1000000) - Long.valueOf("11644473600");
1346
1347 keyLen = fileROBuf.getInt();
1348
1349 uint32 = fileROBuf.getInt() & UINT32_MASK;
1350 longKeyAddresses = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1351
1352 dataSegments = null;
1353 dataSegmentSizes= new int[4];
1354 for (int i = 0; i < 4; i++) {
1355 dataSegmentSizes[i] = fileROBuf.getInt();
1356 }
1357 dataSegmentIndexFileEntries = new CacheAddress[4];
1358 for (int i = 0; i < 4; i++) {
1359 dataSegmentIndexFileEntries[i] = new CacheAddress(fileROBuf.getInt() & UINT32_MASK, selfAddress.getCachePath());
1360 }
1361
1362 flags = fileROBuf.getInt() & UINT32_MASK;
1363 // skip over pad
1364 for (int i = 0; i < 4; i++) {
1365 fileROBuf.getInt();
1366 }
1367
1368 // skip over self hash
1369 fileROBuf.getInt();
1370
1371 // get the key
1372 if (longKeyAddresses != null) {
1373 // Key is stored outside of the entry
1374 try {
1375 if (longKeyAddresses.getFilename() != null) {
1376 CacheDataSegment data = new CacheDataSegment(longKeyAddresses, this.keyLen, true);
1377 key = data.getDataString();
1378 }
1379 } catch (TskCoreException | IngestModuleException ex) {
1380 throw new TskCoreException(String.format("Failed to get external key from address %s", longKeyAddresses)); //NON-NLS
1381 }
1382 }
1383 else { // key stored within entry
1384 StringBuilder strBuilder = new StringBuilder(MAX_KEY_LEN);
1385 int keyLen = 0;
1386 while (fileROBuf.remaining() > 0 && keyLen < MAX_KEY_LEN) {
1387 char keyChar = (char)fileROBuf.get();
1388 if (keyChar == '\0') {
1389 break;
1390 }
1391 strBuilder.append(keyChar);
1392 keyLen++;
1393 }
1394
1395 key = strBuilder.toString();
1396 }
1397 }
1398
1399 public CacheAddress getCacheAddress() {
1400 return selfAddress;
1401 }
1402
1403 public long getHash() {
1404 return hash;
1405 }
1406
1407 public CacheAddress getNextCacheAddress() {
1408 return nextAddress;
1409 }
1410
1411 public int getReuseCount() {
1412 return reuseCount;
1413 }
1414
1415 public int getRefetchCount() {
1416 return refetchCount;
1417 }
1418
1419 public EntryStateEnum getState() {
1420 return state;
1421 }
1422
1423 public long getCreationTime() {
1424 return creationTime;
1425 }
1426
1427 public long getFlags() {
1428 return flags;
1429 }
1430
1431 public String getKey() {
1432 return key;
1433 }
1434
1443 public List<CacheDataSegment> getDataSegments() throws TskCoreException, IngestModuleException {
1444
1445 if (dataSegments == null) {
1446 dataSegments = new ArrayList<>();
1447 for (int i = 0; i < 4; i++) {
1448 if (dataSegmentSizes[i] > 0) {
1449 CacheDataSegment cacheData = new CacheDataSegment(dataSegmentIndexFileEntries[i], dataSegmentSizes[i], true );
1450
1451 cacheData.extract();
1452 dataSegments.add(cacheData);
1453 }
1454 }
1455 }
1456 return dataSegments;
1457 }
1458
1466 boolean hasHTTPHeaders() {
1467 if ((dataSegments == null) || dataSegments.isEmpty()) {
1468 return false;
1469 }
1470 return dataSegments.get(0).hasHTTPHeaders();
1471 }
1472
1479 String getHTTPHeader(String key) {
1480 if ((dataSegments == null) || dataSegments.isEmpty()) {
1481 return null;
1482 }
1483 // First data segment has the HTTP headers, if any
1484 return dataSegments.get(0).getHTTPHeader(key);
1485 }
1486
1492 String getHTTPHeaders() {
1493 if ((dataSegments == null) || dataSegments.isEmpty()) {
1494 return null;
1495 }
1496 // First data segment has the HTTP headers, if any
1497 return dataSegments.get(0).getHTTPHeaders();
1498 }
1499
1508 boolean isBrotliCompressed() {
1509
1510 if (hasHTTPHeaders() ) {
1511 String encodingHeader = getHTTPHeader("content-encoding");
1512 if (encodingHeader!= null) {
1513 return encodingHeader.trim().equalsIgnoreCase("br");
1514 }
1515 }
1516
1517 return false;
1518 }
1519
1520 @Override
1521 public String toString() {
1522 StringBuilder sb = new StringBuilder();
1523 sb.append(String.format("Entry = Hash: %08x, State: %s, ReuseCount: %d, RefetchCount: %d",
1524 this.hash, this.state.toString(), this.reuseCount, this.refetchCount ))
1525 .append(String.format("\n\tKey: %s, Keylen: %d",
1526 this.key, this.keyLen, this.reuseCount, this.refetchCount ))
1527 .append(String.format("\n\tCreationTime: %s",
1528 TimeUtilities.epochToTime(this.creationTime) ))
1529 .append(String.format("\n\tNext Address: %s",
1530 (nextAddress != null) ? nextAddress.toString() : "None"));
1531
1532 for (int i = 0; i < 4; i++) {
1533 if (dataSegmentSizes[i] > 0) {
1534 sb.append(String.format("\n\tData %d: cache address = %s, Data = %s",
1535 i, dataSegmentIndexFileEntries[i].toString(),
1536 (dataSegments != null)
1537 ? dataSegments.get(i).toString()
1538 : "Data not retrived yet."));
1539 }
1540 }
1541
1542 return sb.toString();
1543 }
1544 }
1545}
ENTRY_DOOMED
ENTRY_NORMAL
ENTRY_EVICTED

Copyright © 2012-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.