Autopsy  4.17.0
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 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  */
21 package org.sleuthkit.autopsy.recentactivity;
22 
23 import java.io.File;
24 import java.io.FileOutputStream;
25 import java.io.IOException;
26 import java.io.RandomAccessFile;
27 import java.nio.ByteBuffer;
28 import java.nio.ByteOrder;
29 import java.nio.channels.FileChannel;
30 import java.nio.charset.Charset;
31 import java.nio.file.Path;
32 import java.nio.file.Paths;
33 import java.util.ArrayList;
34 import java.util.Collection;
35 import java.util.HashMap;
36 import java.util.List;
37 import java.util.Map;
38 import java.util.Map.Entry;
39 import java.util.Optional;
40 import java.util.logging.Level;
41 import org.openide.util.NbBundle;
42 import org.openide.util.NbBundle.Messages;
54 import org.sleuthkit.datamodel.AbstractFile;
55 import org.sleuthkit.datamodel.Blackboard;
56 import org.sleuthkit.datamodel.BlackboardArtifact;
57 import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
58 import org.sleuthkit.datamodel.BlackboardAttribute;
59 import org.sleuthkit.datamodel.Content;
60 import org.sleuthkit.datamodel.DerivedFile;
61 import org.sleuthkit.datamodel.TimeUtilities;
62 import org.sleuthkit.datamodel.TskCoreException;
63 import org.sleuthkit.datamodel.TskData;
64 import org.sleuthkit.datamodel.TskException;
65 
90 final class ChromeCacheExtractor {
91 
92  private final static String DEFAULT_CACHE_PATH_STR = "default/cache"; //NON-NLS
93  private final static String BROTLI_MIMETYPE ="application/x-brotli"; //NON-NLS
94 
95  private final static long UINT32_MASK = 0xFFFFFFFFl;
96 
97  private final static int INDEXFILE_HDR_SIZE = 92*4;
98  private final static int DATAFILE_HDR_SIZE = 8192;
99 
100  private final static Logger logger = Logger.getLogger(ChromeCacheExtractor.class.getName());
101 
102  private static final String VERSION_NUMBER = "1.0.0"; //NON-NLS
103  private final String moduleName;
104 
105  private String absOutputFolderName;
106  private String relOutputFolderName;
107 
108  private final Content dataSource;
109  private final IngestJobContext context;
110  private final DataSourceIngestModuleProgress progressBar;
111  private final IngestServices services = IngestServices.getInstance();
112  private Case currentCase;
113  private FileManager fileManager;
114 
115  // A file table to cache copies of index and data_n files.
116  private final Map<String, FileWrapper> fileCopyCache = new HashMap<>();
117 
118  // A file table to cache the f_* files.
119  private final Map<String, AbstractFile> externalFilesTable = new HashMap<>();
120 
126  final class FileWrapper {
127  private final AbstractFile abstractFile;
128  private final RandomAccessFile fileCopy;
129  private final ByteBuffer byteBuffer;
130 
131  FileWrapper (AbstractFile abstractFile, RandomAccessFile fileCopy, ByteBuffer buffer ) {
132  this.abstractFile = abstractFile;
133  this.fileCopy = fileCopy;
134  this.byteBuffer = buffer;
135  }
136 
137  public RandomAccessFile getFileCopy() {
138  return fileCopy;
139  }
140  public ByteBuffer getByteBuffer() {
141  return byteBuffer;
142  }
143  AbstractFile getAbstractFile() {
144  return abstractFile;
145  }
146  }
147 
148  @NbBundle.Messages({
149  "ChromeCacheExtractor.moduleName=ChromeCacheExtractor",
150  "# {0} - module name",
151  "# {1} - row number",
152  "# {2} - table length",
153  "# {3} - cache path",
154  "ChromeCacheExtractor.progressMsg={0}: Extracting cache entry {1} of {2} entries from {3}"
155  })
156  ChromeCacheExtractor(Content dataSource, IngestJobContext context, DataSourceIngestModuleProgress progressBar ) {
157  moduleName = Bundle.ChromeCacheExtractor_moduleName();
158  this.dataSource = dataSource;
159  this.context = context;
160  this.progressBar = progressBar;
161  }
162 
163 
169  private void moduleInit() throws IngestModuleException {
170 
171  try {
172  currentCase = Case.getCurrentCaseThrows();
173  fileManager = currentCase.getServices().getFileManager();
174 
175  // Create an output folder to save any derived files
176  absOutputFolderName = RAImageIngestModule.getRAOutputPath(currentCase, moduleName);
177  relOutputFolderName = Paths.get( RAImageIngestModule.getRelModuleOutputPath(), moduleName).normalize().toString();
178 
179  File dir = new File(absOutputFolderName);
180  if (dir.exists() == false) {
181  dir.mkdirs();
182  }
183  } catch (NoCurrentCaseException ex) {
184  String msg = "Failed to get current case."; //NON-NLS
185  throw new IngestModuleException(msg, ex);
186  }
187  }
188 
196  private void resetForNewCacheFolder(String cachePath) throws IngestModuleException {
197 
198  fileCopyCache.clear();
199  externalFilesTable.clear();
200 
201  String cacheAbsOutputFolderName = this.getAbsOutputFolderName() + cachePath;
202  File outDir = new File(cacheAbsOutputFolderName);
203  if (outDir.exists() == false) {
204  outDir.mkdirs();
205  }
206 
207  String cacheTempPath = RAImageIngestModule.getRATempPath(currentCase, moduleName) + cachePath;
208  File tempDir = new File(cacheTempPath);
209  if (tempDir.exists() == false) {
210  tempDir.mkdirs();
211  }
212  }
213 
220  private void cleanup () {
221 
222  for (Entry<String, FileWrapper> entry : this.fileCopyCache.entrySet()) {
223  Path tempFilePath = Paths.get(RAImageIngestModule.getRATempPath(currentCase, moduleName), entry.getKey() );
224  try {
225  entry.getValue().getFileCopy().getChannel().close();
226  entry.getValue().getFileCopy().close();
227 
228  File tmpFile = tempFilePath.toFile();
229  if (!tmpFile.delete()) {
230  tmpFile.deleteOnExit();
231  }
232  } catch (IOException ex) {
233  logger.log(Level.WARNING, String.format("Failed to delete cache file copy %s", tempFilePath.toString()), ex); //NON-NLS
234  }
235  }
236  }
237 
243  private String getAbsOutputFolderName() {
244  return absOutputFolderName;
245  }
246 
252  private String getRelOutputFolderName() {
253  return relOutputFolderName;
254  }
255 
262  void processCaches() {
263 
264  try {
265  moduleInit();
266  } catch (IngestModuleException ex) {
267  String msg = "Failed to initialize ChromeCacheExtractor."; //NON-NLS
268  logger.log(Level.SEVERE, msg, ex);
269  return;
270  }
271 
272  // Find and process the cache folders. There could be one per user
273  try {
274  // Identify each cache folder by searching for the index files in each
275  List<AbstractFile> indexFiles = findIndexFiles();
276 
277  // Process each of the cache folders
278  for (AbstractFile indexFile: indexFiles) {
279 
280  if (context.dataSourceIngestIsCancelled()) {
281  return;
282  }
283 
284  processCacheFolder(indexFile);
285  }
286 
287  } catch (TskCoreException ex) {
288  String msg = "Failed to find cache index files"; //NON-NLS
289  logger.log(Level.WARNING, msg, ex);
290  }
291  }
292 
293  @Messages({
294  "ChromeCacheExtract_adding_extracted_files_msg=Chrome Cache: Adding %d extracted files for analysis.",
295  "ChromeCacheExtract_adding_artifacts_msg=Chrome Cache: Adding %d artifacts for analysis.",
296  "ChromeCacheExtract_loading_files_msg=Chrome Cache: Loading files from %s."
297  })
298 
305  private void processCacheFolder(AbstractFile indexFile) {
306 
307  String cacheFolderName = indexFile.getParentPath();
308  Optional<FileWrapper> indexFileWrapper;
309 
310  /*
311  * The first part of this method is all about finding the needed files in the cache
312  * folder and making internal copies/caches of them so that we can later process them
313  * and effeciently look them up.
314  */
315  try {
316  progressBar.progress(String.format(Bundle.ChromeCacheExtract_loading_files_msg(), cacheFolderName));
317  resetForNewCacheFolder(cacheFolderName);
318 
319  // @@@ This is little ineffecient because we later in this call search for the AbstractFile that we currently have
320  // Load the index file into the caches
321  indexFileWrapper = findDataOrIndexFile(indexFile.getName(), cacheFolderName);
322  if (!indexFileWrapper.isPresent()) {
323  String msg = String.format("Failed to find copy cache index file %s", indexFile.getUniquePath());
324  logger.log(Level.WARNING, msg);
325  return;
326  }
327 
328 
329  // load the data files into the internal cache. We do this because we often
330  // jump in between the various data_X files resolving segments
331  for (int i = 0; i < 4; i ++) {
332  Optional<FileWrapper> dataFile = findDataOrIndexFile(String.format("data_%1d",i), cacheFolderName );
333  if (!dataFile.isPresent()) {
334  return;
335  }
336  }
337 
338  // find all f_* files in a single query and load them into the cache
339  // we do this here so that it is a single query instead of hundreds of individual ones
340  findExternalFiles(cacheFolderName);
341 
342  } catch (TskCoreException | IngestModuleException ex) {
343  String msg = "Failed to find cache files in path " + cacheFolderName; //NON-NLS
344  logger.log(Level.WARNING, msg, ex);
345  return;
346  }
347 
348  /*
349  * Now the analysis begins. We parse the index file and that drives parsing entries
350  * from data_X or f_XXXX files.
351  */
352  logger.log(Level.INFO, "{0}- Now reading Cache index file from path {1}", new Object[]{moduleName, cacheFolderName }); //NON-NLS
353 
354  List<AbstractFile> derivedFiles = new ArrayList<>();
355  Collection<BlackboardArtifact> artifactsAdded = new ArrayList<>();
356 
357  ByteBuffer indexFileROBuffer = indexFileWrapper.get().getByteBuffer();
358  IndexFileHeader indexHdr = new IndexFileHeader(indexFileROBuffer);
359 
360  // seek past the header
361  indexFileROBuffer.position(INDEXFILE_HDR_SIZE);
362 
363  try {
364  /* Cycle through index and get the CacheAddress for each CacheEntry. Process each entry
365  * to extract data, add artifacts, etc. from the f_XXXX and data_x files */
366  for (int i = 0; i < indexHdr.getTableLen(); i++) {
367 
368  if (context.dataSourceIngestIsCancelled()) {
369  cleanup();
370  return;
371  }
372 
373  CacheAddress addr = new CacheAddress(indexFileROBuffer.getInt() & UINT32_MASK, cacheFolderName);
374  if (addr.isInitialized()) {
375  progressBar.progress(NbBundle.getMessage(this.getClass(),
376  "ChromeCacheExtractor.progressMsg",
377  moduleName, i, indexHdr.getTableLen(), cacheFolderName) );
378  try {
379  List<DerivedFile> addedFiles = processCacheEntry(addr, artifactsAdded);
380  derivedFiles.addAll(addedFiles);
381  }
382  catch (TskCoreException | IngestModuleException ex) {
383  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
384  }
385  }
386  }
387  } catch (java.nio.BufferUnderflowException ex) {
388  logger.log(Level.WARNING, String.format("Ran out of data unexpectedly reading file %s (ObjID: %d)", indexFile.getName(), indexFile.getId()));
389  }
390 
391  if (context.dataSourceIngestIsCancelled()) {
392  cleanup();
393  return;
394  }
395 
396 
397  // notify listeners of new files and schedule for analysis
398  progressBar.progress(String.format(Bundle.ChromeCacheExtract_adding_extracted_files_msg(), derivedFiles.size()));
399  derivedFiles.forEach((derived) -> {
400  services.fireModuleContentEvent(new ModuleContentEvent(derived));
401  });
402  context.addFilesToJob(derivedFiles);
403 
404  // notify listeners about new artifacts
405  progressBar.progress(String.format(Bundle.ChromeCacheExtract_adding_artifacts_msg(), artifactsAdded.size()));
406  Blackboard blackboard = currentCase.getSleuthkitCase().getBlackboard();
407  try {
408  blackboard.postArtifacts(artifactsAdded, moduleName);
409  } catch (Blackboard.BlackboardException ex) {
410  logger.log(Level.WARNING, String.format("Failed to post cacheIndex artifacts "), ex); //NON-NLS
411  }
412 
413  cleanup();
414  }
415 
428  private List<DerivedFile> processCacheEntry(CacheAddress cacheAddress, Collection<BlackboardArtifact> artifactsAdded ) throws TskCoreException, IngestModuleException {
429 
430  List<DerivedFile> derivedFiles = new ArrayList<>();
431 
432  // get the path to the corresponding data_X file for the cache entry
433  String cacheEntryFileName = cacheAddress.getFilename();
434  String cachePath = cacheAddress.getCachePath();
435 
436  Optional<FileWrapper> cacheEntryFileOptional = findDataOrIndexFile(cacheEntryFileName, cachePath);
437  if (!cacheEntryFileOptional.isPresent()) {
438  String msg = String.format("Failed to find data file %s", cacheEntryFileName); //NON-NLS
439  throw new IngestModuleException(msg);
440  }
441 
442  // Load the entry to get its metadata, segments, etc.
443  CacheEntry cacheEntry = new CacheEntry(cacheAddress, cacheEntryFileOptional.get() );
444  List<CacheDataSegment> dataSegments = cacheEntry.getDataSegments();
445 
446  // Only process the first payload data segment in each entry
447  // first data segement has the HTTP headers, 2nd is the payload
448  if (dataSegments.size() < 2) {
449  return derivedFiles;
450  }
451  CacheDataSegment dataSegment = dataSegments.get(1);
452 
453  // Name where segment is located (could be diffrent from where entry was located)
454  String segmentFileName = dataSegment.getCacheAddress().getFilename();
455  Optional<AbstractFile> segmentFileAbstractFile = findAbstractFile(segmentFileName, cachePath);
456  if (!segmentFileAbstractFile.isPresent()) {
457  logger.log(Level.WARNING, "Error finding segment file: " + cachePath + "/" + segmentFileName); //NON-NLS
458  return derivedFiles;
459  }
460 
461  boolean isBrotliCompressed = false;
462  if (dataSegment.getType() != CacheDataTypeEnum.HTTP_HEADER && cacheEntry.isBrotliCompressed() ) {
463  isBrotliCompressed = true;
464  }
465 
466 
467  // Make artifacts around the cached item and extract data from data_X file
468  try {
469  AbstractFile cachedItemFile; //
470  /* If the cached data is in a f_XXXX file, we only need to make artifacts. */
471  if (dataSegment.isInExternalFile() ) {
472  cachedItemFile = segmentFileAbstractFile.get();
473  }
474  /* If the data is in a data_X file, we need to extract it out and then make the similar artifacts */
475  else {
476 
477  // Data segments in "data_x" files are saved in individual files and added as derived files
478  String filename = dataSegment.save();
479  String relPathname = getRelOutputFolderName() + dataSegment.getCacheAddress().getCachePath() + filename;
480 
481  // @@@ We should batch these up and do them in one big insert / transaction
482  DerivedFile derivedFile = fileManager.addDerivedFile(filename, relPathname,
483  dataSegment.getDataLength(),
484  cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), // TBD
485  true,
486  segmentFileAbstractFile.get(),
487  "",
488  moduleName,
489  VERSION_NUMBER,
490  "",
491  TskData.EncodingType.NONE);
492 
493  derivedFiles.add(derivedFile);
494  cachedItemFile = derivedFile;
495  }
496 
497  addArtifacts(cacheEntry, cacheEntryFileOptional.get().getAbstractFile(), cachedItemFile, artifactsAdded);
498 
499  // Tika doesn't detect these types. So, make sure they have the correct MIME type */
500  if (isBrotliCompressed) {
501  cachedItemFile.setMIMEType(BROTLI_MIMETYPE);
502  cachedItemFile.save();
503  }
504 
505  } catch (TskException ex) {
506  logger.log(Level.SEVERE, "Error while trying to add an artifact", ex); //NON-NLS
507  }
508 
509  return derivedFiles;
510  }
511 
521  private void addArtifacts(CacheEntry cacheEntry, AbstractFile cacheEntryFile, AbstractFile cachedItemFile, Collection<BlackboardArtifact> artifactsAdded) throws TskCoreException {
522 
523  // Create a TSK_WEB_CACHE entry with the parent as data_X file that had the cache entry
524  BlackboardArtifact webCacheArtifact = cacheEntryFile.newArtifact(ARTIFACT_TYPE.TSK_WEB_CACHE);
525  if (webCacheArtifact != null) {
526  Collection<BlackboardAttribute> webAttr = new ArrayList<>();
527  String url = cacheEntry.getKey() != null ? cacheEntry.getKey() : "";
528  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
529  moduleName, url));
530  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
531  moduleName, NetworkUtils.extractDomain(url)));
532  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED,
533  moduleName, cacheEntry.getCreationTime()));
534  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_HEADERS,
535  moduleName, cacheEntry.getHTTPHeaders()));
536  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH,
537  moduleName, cachedItemFile.getUniquePath()));
538  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID,
539  moduleName, cachedItemFile.getId()));
540  webCacheArtifact.addAttributes(webAttr);
541  artifactsAdded.add(webCacheArtifact);
542 
543  // Create a TSK_ASSOCIATED_OBJECT on the f_XXX or derived file file back to the CACHE entry
544  BlackboardArtifact associatedObjectArtifact = cachedItemFile.newArtifact(ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT);
545  if (associatedObjectArtifact != null) {
546  associatedObjectArtifact.addAttribute(
547  new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT,
548  moduleName, webCacheArtifact.getArtifactID()));
549  artifactsAdded.add(associatedObjectArtifact);
550  }
551  }
552  }
553 
562  private void findExternalFiles(String cachePath) throws TskCoreException {
563 
564  List<AbstractFile> effFiles = fileManager.findFiles(dataSource, "f_%", cachePath); //NON-NLS
565  for (AbstractFile abstractFile : effFiles ) {
566  if (cachePath.equals(abstractFile.getParentPath()) && abstractFile.isFile()) {
567  this.externalFilesTable.put(cachePath + abstractFile.getName(), abstractFile);
568  }
569  }
570  }
579  private Optional<AbstractFile> findAbstractFile(String cacheFileName, String cacheFolderName) throws TskCoreException {
580 
581  // see if it is cached
582  String fileTableKey = cacheFolderName + cacheFileName;
583  if (cacheFileName.startsWith("f_") && externalFilesTable.containsKey(fileTableKey)) {
584  return Optional.of(externalFilesTable.get(fileTableKey));
585  }
586 
587  if (fileCopyCache.containsKey(fileTableKey)) {
588  return Optional.of(fileCopyCache.get(fileTableKey).getAbstractFile());
589  }
590 
591 
592  List<AbstractFile> cacheFiles = fileManager.findFiles(dataSource, cacheFileName, cacheFolderName); //NON-NLS
593  if (!cacheFiles.isEmpty()) {
594  for (AbstractFile abstractFile: cacheFiles ) {
595  if (abstractFile.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)) {
596  return Optional.of(abstractFile);
597  }
598  }
599  return Optional.of(cacheFiles.get(0));
600  }
601 
602  return Optional.empty();
603  }
604 
612  private List<AbstractFile> findIndexFiles() throws TskCoreException {
613  return fileManager.findFiles(dataSource, "index", DEFAULT_CACHE_PATH_STR); //NON-NLS
614  }
615 
616 
617 
629  private Optional<FileWrapper> findDataOrIndexFile(String cacheFileName, String cacheFolderName) throws TskCoreException, IngestModuleException {
630 
631  // Check if the file is already in the cache
632  String fileTableKey = cacheFolderName + cacheFileName;
633  if (fileCopyCache.containsKey(fileTableKey)) {
634  return Optional.of(fileCopyCache.get(fileTableKey));
635  }
636 
637  // Use Autopsy to get the AbstractFile
638  Optional<AbstractFile> abstractFileOptional = findAbstractFile(cacheFileName, cacheFolderName);
639  if (!abstractFileOptional.isPresent()) {
640  return Optional.empty();
641  }
642 
643  // Wrap the file so that we can get the ByteBuffer later.
644  // @@@ BC: I think this should nearly all go into FileWrapper and be done lazily and perhaps based on size.
645  // Many of the files are small enough to keep in memory for the ByteBuffer
646 
647  // write the file to disk so that we can have a memory-mapped ByteBuffer
648  AbstractFile cacheFile = abstractFileOptional.get();
649  RandomAccessFile randomAccessFile = null;
650  String tempFilePathname = RAImageIngestModule.getRATempPath(currentCase, moduleName) + cacheFolderName + cacheFile.getName(); //NON-NLS
651  try {
652  File newFile = new File(tempFilePathname);
653  ContentUtils.writeToFile(cacheFile, newFile, context::dataSourceIngestIsCancelled);
654 
655  randomAccessFile = new RandomAccessFile(tempFilePathname, "r");
656  FileChannel roChannel = randomAccessFile.getChannel();
657  ByteBuffer cacheFileROBuf = roChannel.map(FileChannel.MapMode.READ_ONLY, 0,
658  (int) roChannel.size());
659 
660  cacheFileROBuf.order(ByteOrder.nativeOrder());
661  FileWrapper cacheFileWrapper = new FileWrapper(cacheFile, randomAccessFile, cacheFileROBuf );
662 
663  if (!cacheFileName.startsWith("f_")) {
664  fileCopyCache.put(cacheFolderName + cacheFileName, cacheFileWrapper);
665  }
666 
667  return Optional.of(cacheFileWrapper);
668  }
669  catch (IOException ex) {
670 
671  try {
672  if (randomAccessFile != null) {
673  randomAccessFile.close();
674  }
675  }
676  catch (IOException ex2) {
677  logger.log(Level.SEVERE, "Error while trying to close temp file after exception.", ex2); //NON-NLS
678  }
679  String msg = String.format("Error reading/copying Chrome cache file '%s' (id=%d).", //NON-NLS
680  cacheFile.getName(), cacheFile.getId());
681  throw new IngestModuleException(msg, ex);
682  }
683  }
684 
688  final class IndexFileHeader {
689 
690  private final long magic;
691  private final int version;
692  private final int numEntries;
693  private final int numBytes;
694  private final int lastFile;
695  private final int tableLen;
696 
697  IndexFileHeader(ByteBuffer indexFileROBuf) {
698 
699  magic = indexFileROBuf.getInt() & UINT32_MASK;
700 
701  indexFileROBuf.position(indexFileROBuf.position()+2);
702 
703  version = indexFileROBuf.getShort();
704  numEntries = indexFileROBuf.getInt();
705  numBytes = indexFileROBuf.getInt();
706  lastFile = indexFileROBuf.getInt();
707 
708  indexFileROBuf.position(indexFileROBuf.position()+4); // this_id
709  indexFileROBuf.position(indexFileROBuf.position()+4); // stats cache cacheAddress
710 
711  tableLen = indexFileROBuf.getInt();
712  }
713 
714  public long getMagic() {
715  return magic;
716  }
717 
718  public int getVersion() {
719  return version;
720  }
721 
722  public int getNumEntries() {
723  return numEntries;
724  }
725 
726  public int getNumBytes() {
727  return numBytes;
728  }
729 
730  public int getLastFile() {
731  return lastFile;
732  }
733 
734  public int getTableLen() {
735  return tableLen;
736  }
737 
738  @Override
739  public String toString() {
740  StringBuilder sb = new StringBuilder();
741 
742  sb.append(String.format("Index Header:"))
743  .append(String.format("\tMagic = %x" , getMagic()) )
744  .append(String.format("\tVersion = %x" , getVersion()) )
745  .append(String.format("\tNumEntries = %x" , getNumEntries()) )
746  .append(String.format("\tNumBytes = %x" , getNumBytes()) )
747  .append(String.format("\tLastFile = %x" , getLastFile()) )
748  .append(String.format("\tTableLen = %x" , getTableLen()) );
749 
750  return sb.toString();
751  }
752  }
753 
757  enum CacheFileTypeEnum {
758  EXTERNAL,
759  RANKINGS,
760  BLOCK_256,
761  BLOCK_1K,
762  BLOCK_4K,
763  BLOCK_FILES,
764  BLOCK_ENTRIES,
765  BLOCK_EVICTED
766  }
767 
768 
769 
792  final class CacheAddress {
793  // sundry constants to parse the bit fields
794  private static final long ADDR_INITIALIZED_MASK = 0x80000000l;
795  private static final long FILE_TYPE_MASK = 0x70000000;
796  private static final long FILE_TYPE_OFFSET = 28;
797  private static final long NUM_BLOCKS_MASK = 0x03000000;
798  private static final long NUM_BLOCKS_OFFSET = 24;
799  private static final long FILE_SELECTOR_MASK = 0x00ff0000;
800  private static final long FILE_SELECTOR_OFFSET = 16;
801  private static final long START_BLOCK_MASK = 0x0000FFFF;
802  private static final long EXTERNAL_FILE_NAME_MASK = 0x0FFFFFFF;
803 
804  private final long uint32CacheAddr;
805  private final CacheFileTypeEnum fileType;
806  private final int numBlocks;
807  private final int startBlock;
808  private final String fileName;
809  private final int fileNumber;
810 
811  private final String cachePath;
812 
813 
819  CacheAddress(long uint32, String cachePath) {
820 
821  uint32CacheAddr = uint32;
822  this.cachePath = cachePath;
823 
824 
825  // analyze the
826  int fileTypeEnc = (int)(uint32CacheAddr & FILE_TYPE_MASK) >> FILE_TYPE_OFFSET;
827  fileType = CacheFileTypeEnum.values()[fileTypeEnc];
828 
829  if (isInitialized()) {
830  if (isInExternalFile()) {
831  fileNumber = (int)(uint32CacheAddr & EXTERNAL_FILE_NAME_MASK);
832  fileName = String.format("f_%06x", getFileNumber() );
833  numBlocks = 0;
834  startBlock = 0;
835  } else {
836  fileNumber = (int)((uint32CacheAddr & FILE_SELECTOR_MASK) >> FILE_SELECTOR_OFFSET);
837  fileName = String.format("data_%d", getFileNumber() );
838  numBlocks = (int)(uint32CacheAddr & NUM_BLOCKS_MASK >> NUM_BLOCKS_OFFSET);
839  startBlock = (int)(uint32CacheAddr & START_BLOCK_MASK);
840  }
841  }
842  else {
843  fileName = null;
844  fileNumber = 0;
845  numBlocks = 0;
846  startBlock = 0;
847  }
848  }
849 
850  boolean isInitialized() {
851  return ((uint32CacheAddr & ADDR_INITIALIZED_MASK) != 0);
852  }
853 
854  CacheFileTypeEnum getFileType() {
855  return fileType;
856  }
857 
862  String getFilename() {
863  return fileName;
864  }
865 
866  String getCachePath() {
867  return cachePath;
868  }
869 
870  boolean isInExternalFile() {
871  return (fileType == CacheFileTypeEnum.EXTERNAL);
872  }
873 
874  int getFileNumber() {
875  return fileNumber;
876  }
877 
878  int getStartBlock() {
879  return startBlock;
880  }
881 
882  int getNumBlocks() {
883  return numBlocks;
884  }
885 
886  int getBlockSize() {
887  switch (fileType) {
888  case RANKINGS:
889  return 36;
890  case BLOCK_256:
891  return 256;
892  case BLOCK_1K:
893  return 1024;
894  case BLOCK_4K:
895  return 4096;
896  case BLOCK_FILES:
897  return 8;
898  case BLOCK_ENTRIES:
899  return 104;
900  case BLOCK_EVICTED:
901  return 48;
902  default:
903  return 0;
904  }
905  }
906 
907  public long getUint32CacheAddr() {
908  return uint32CacheAddr;
909  }
910 
911  @Override
912  public String toString() {
913  StringBuilder sb = new StringBuilder();
914  sb.append(String.format("CacheAddr %08x : %s : filename %s",
915  uint32CacheAddr,
916  isInitialized() ? "Initialized" : "UnInitialized",
917  getFilename()));
918 
919  if ((fileType == CacheFileTypeEnum.BLOCK_256) ||
920  (fileType == CacheFileTypeEnum.BLOCK_1K) ||
921  (fileType == CacheFileTypeEnum.BLOCK_4K) ) {
922  sb.append(String.format(" (%d blocks starting at %08X)",
923  this.getNumBlocks(),
924  this.getStartBlock()
925  ));
926  }
927 
928  return sb.toString();
929  }
930 
931  }
932 
936  enum CacheDataTypeEnum {
937  HTTP_HEADER,
938  UNKNOWN,
939  };
940 
950  final class CacheDataSegment {
951 
952  private int length;
953  private final CacheAddress cacheAddress;
954  private CacheDataTypeEnum type;
955 
956  private boolean isHTTPHeaderHint;
957 
958  private FileWrapper cacheFileCopy = null;
959  private byte[] data = null;
960 
961  private String httpResponse;
962  private final Map<String, String> httpHeaders = new HashMap<>();
963 
964  CacheDataSegment(CacheAddress cacheAddress, int len) {
965  this(cacheAddress, len, false);
966  }
967 
968  CacheDataSegment(CacheAddress cacheAddress, int len, boolean isHTTPHeader ) {
969  this.type = CacheDataTypeEnum.UNKNOWN;
970  this.length = len;
971  this.cacheAddress = cacheAddress;
972  this.isHTTPHeaderHint = isHTTPHeader;
973  }
974 
975  boolean isInExternalFile() {
976  return cacheAddress.isInExternalFile();
977  }
978 
979  boolean hasHTTPHeaders() {
980  return this.type == CacheDataTypeEnum.HTTP_HEADER;
981  }
982 
983  String getHTTPHeader(String key) {
984  return this.httpHeaders.get(key);
985  }
986 
992  String getHTTPHeaders() {
993  if (!hasHTTPHeaders()) {
994  return "";
995  }
996 
997  StringBuilder sb = new StringBuilder();
998  httpHeaders.entrySet().forEach((entry) -> {
999  if (sb.length() > 0) {
1000  sb.append(" \n");
1001  }
1002  sb.append(String.format("%s : %s",
1003  entry.getKey(), entry.getValue()));
1004  });
1005 
1006  return sb.toString();
1007  }
1008 
1009  String getHTTPRespone() {
1010  return httpResponse;
1011  }
1012 
1018  void extract() throws TskCoreException, IngestModuleException {
1019 
1020  // do nothing if already extracted,
1021  if (data != null) {
1022  return;
1023  }
1024 
1025  // Don't extract data from external files.
1026  if (!cacheAddress.isInExternalFile()) {
1027 
1028  if (cacheAddress.getFilename() == null) {
1029  throw new TskCoreException("Cache address has no file name");
1030  }
1031 
1032  cacheFileCopy = findDataOrIndexFile(cacheAddress.getFilename(), cacheAddress.getCachePath()).get();
1033 
1034  this.data = new byte [length];
1035  ByteBuffer buf = cacheFileCopy.getByteBuffer();
1036  int dataOffset = DATAFILE_HDR_SIZE + cacheAddress.getStartBlock() * cacheAddress.getBlockSize();
1037  buf.position(dataOffset);
1038  buf.get(data, 0, length);
1039 
1040  // if this might be a HTPP header, lets try to parse it as such
1041  if ((isHTTPHeaderHint)) {
1042  String strData = new String(data);
1043  if (strData.contains("HTTP")) {
1044 
1045  // Http headers if present, are usually in frst data segment in an entry
1046  // General Parsing algo:
1047  // - Find start of HTTP header by searching for string "HTTP"
1048  // - Skip to the first 0x00 to get to the end of the HTTP response line, this makrs start of headers section
1049  // - Find the end of the header by searching for 0x00 0x00 bytes
1050  // - Extract the headers section
1051  // - Parse the headers section - each null terminated string is a header
1052  // - Each header is of the format "name: value" e.g.
1053 
1054  type = CacheDataTypeEnum.HTTP_HEADER;
1055 
1056  int startOff = strData.indexOf("HTTP");
1057  Charset charset = Charset.forName("UTF-8");
1058  boolean done = false;
1059  int i = startOff;
1060  int hdrNum = 1;
1061 
1062  while (!done) {
1063  // each header is null terminated
1064  int start = i;
1065  while (i < data.length && data[i] != 0) {
1066  i++;
1067  }
1068 
1069  // http headers are terminated by 0x00 0x00
1070  if (i == data.length || data[i+1] == 0) {
1071  done = true;
1072  }
1073 
1074  int len = (i - start);
1075  String headerLine = new String(data, start, len, charset);
1076 
1077  // first line is the http response
1078  if (hdrNum == 1) {
1079  httpResponse = headerLine;
1080  } else {
1081  int nPos = headerLine.indexOf(':');
1082  if (nPos > 0 ) {
1083  String key = headerLine.substring(0, nPos);
1084  String val= headerLine.substring(nPos+1);
1085  httpHeaders.put(key.toLowerCase(), val);
1086  }
1087  }
1088 
1089  i++;
1090  hdrNum++;
1091  }
1092  }
1093  }
1094  }
1095  }
1096 
1097  String getDataString() throws TskCoreException, IngestModuleException {
1098  if (data == null) {
1099  extract();
1100  }
1101  return new String(data);
1102  }
1103 
1104  byte[] getDataBytes() throws TskCoreException, IngestModuleException {
1105  if (data == null) {
1106  extract();
1107  }
1108  return data.clone();
1109  }
1110 
1111  int getDataLength() {
1112  return this.length;
1113  }
1114 
1115  CacheDataTypeEnum getType() {
1116  return type;
1117  }
1118 
1119  CacheAddress getCacheAddress() {
1120  return cacheAddress;
1121  }
1122 
1123 
1132  String save() throws TskCoreException, IngestModuleException {
1133  String fileName;
1134 
1135  if (cacheAddress.isInExternalFile()) {
1136  fileName = cacheAddress.getFilename();
1137  } else {
1138  fileName = String.format("%s__%08x", cacheAddress.getFilename(), cacheAddress.getUint32CacheAddr());
1139  }
1140 
1141  String filePathName = getAbsOutputFolderName() + cacheAddress.getCachePath() + fileName;
1142  save(filePathName);
1143 
1144  return fileName;
1145  }
1146 
1156  void save(String filePathName) throws TskCoreException, IngestModuleException {
1157 
1158  // Save the data to specified file
1159  if (data == null) {
1160  extract();
1161  }
1162 
1163  // Data in external files is not saved in local files
1164  if (!this.isInExternalFile()) {
1165  // write the
1166  try (FileOutputStream stream = new FileOutputStream(filePathName)) {
1167  stream.write(data);
1168  } catch (IOException ex) {
1169  throw new TskCoreException(String.format("Failed to write output file %s", filePathName), ex);
1170  }
1171  }
1172  }
1173 
1174  @Override
1175  public String toString() {
1176  StringBuilder strBuilder = new StringBuilder();
1177  strBuilder.append(String.format("\t\tData type = : %s, Data Len = %d ",
1178  this.type.toString(), this.length ));
1179 
1180  if (hasHTTPHeaders()) {
1181  String str = getHTTPHeader("content-encoding");
1182  if (str != null) {
1183  strBuilder.append(String.format("\t%s=%s", "content-encoding", str ));
1184  }
1185  }
1186 
1187  return strBuilder.toString();
1188  }
1189 
1190  }
1191 
1192 
1196  enum EntryStateEnum {
1197  ENTRY_NORMAL,
1198  ENTRY_EVICTED,
1199  ENTRY_DOOMED
1200  };
1201 
1202 
1203 // Main structure for an entry on the backing storage.
1204 //
1205 // Each entry has a key, identifying the URL the cache entry pertains to.
1206 // If the key is longer than
1207 // what can be stored on this structure, it will be extended on consecutive
1208 // blocks (adding 256 bytes each time), up to 4 blocks (1024 - 32 - 1 chars).
1209 // After that point, the whole key will be stored as a data block or external
1210 // file.
1211 //
1212 // Each entry can have upto 4 data segments
1213 //
1214 // struct EntryStore {
1215 // uint32 hash; // Full hash of the key.
1216 // CacheAddr next; // Next entry with the same hash or bucket.
1217 // CacheAddr rankings_node; // Rankings node for this entry.
1218 // int32 reuse_count; // How often is this entry used.
1219 // int32 refetch_count; // How often is this fetched from the net.
1220 // int32 state; // Current state.
1221 // uint64 creation_time;
1222 // int32 key_len;
1223 // CacheAddr long_key; // Optional cacheAddress of a long key.
1224 // int32 data_size[4]; // We can store up to 4 data streams for each
1225 // CacheAddr data_addr[4]; // entry.
1226 // uint32 flags; // Any combination of EntryFlags.
1227 // int32 pad[4];
1228 // uint32 self_hash; // The hash of EntryStore up to this point.
1229 // char key[256 - 24 * 4]; // null terminated
1230 // };
1231 
1235  final class CacheEntry {
1236 
1237  // each entry is 256 bytes. The last section of the entry, after all the other fields is a null terminated key
1238  private static final int MAX_KEY_LEN = 256-24*4;
1239 
1240  private final CacheAddress selfAddress;
1241  private final FileWrapper cacheFileCopy;
1242 
1243  private final long hash;
1244  private final CacheAddress nextAddress;
1245  private final CacheAddress rankingsNodeAddress;
1246 
1247  private final int reuseCount;
1248  private final int refetchCount;
1249  private final EntryStateEnum state;
1250 
1251  private final long creationTime;
1252  private final int keyLen;
1253 
1254  private final CacheAddress longKeyAddresses; // cacheAddress of the key, if the key is external to the entry
1255 
1256  private final int[] dataSegmentSizes;
1257  private final CacheAddress[] dataSegmentIndexFileEntries;
1258  private List<CacheDataSegment> dataSegments;
1259 
1260  private final long flags;
1261 
1262  private String key; // Key may be found within the entry or may be external
1263 
1264  CacheEntry(CacheAddress cacheAdress, FileWrapper cacheFileCopy ) throws TskCoreException {
1265  this.selfAddress = cacheAdress;
1266  this.cacheFileCopy = cacheFileCopy;
1267 
1268  ByteBuffer fileROBuf = cacheFileCopy.getByteBuffer();
1269 
1270  int entryOffset = DATAFILE_HDR_SIZE + cacheAdress.getStartBlock() * cacheAdress.getBlockSize();
1271 
1272  // reposition the buffer to the the correct offset
1273  fileROBuf.position(entryOffset);
1274 
1275  hash = fileROBuf.getInt() & UINT32_MASK;
1276 
1277  long uint32 = fileROBuf.getInt() & UINT32_MASK;
1278  nextAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1279 
1280  uint32 = fileROBuf.getInt() & UINT32_MASK;
1281  rankingsNodeAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1282 
1283  reuseCount = fileROBuf.getInt();
1284  refetchCount = fileROBuf.getInt();
1285 
1286  int stateVal = fileROBuf.getInt();
1287  if ((stateVal >= 0) && (stateVal < EntryStateEnum.values().length)) {
1288  state = EntryStateEnum.values()[stateVal];
1289  } else {
1290  throw new TskCoreException("Invalid EntryStateEnum value"); // NON-NLS
1291  }
1292  creationTime = (fileROBuf.getLong() / 1000000) - Long.valueOf("11644473600");
1293 
1294  keyLen = fileROBuf.getInt();
1295 
1296  uint32 = fileROBuf.getInt() & UINT32_MASK;
1297  longKeyAddresses = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1298 
1299  dataSegments = null;
1300  dataSegmentSizes= new int[4];
1301  for (int i = 0; i < 4; i++) {
1302  dataSegmentSizes[i] = fileROBuf.getInt();
1303  }
1304  dataSegmentIndexFileEntries = new CacheAddress[4];
1305  for (int i = 0; i < 4; i++) {
1306  dataSegmentIndexFileEntries[i] = new CacheAddress(fileROBuf.getInt() & UINT32_MASK, selfAddress.getCachePath());
1307  }
1308 
1309  flags = fileROBuf.getInt() & UINT32_MASK;
1310  // skip over pad
1311  for (int i = 0; i < 4; i++) {
1312  fileROBuf.getInt();
1313  }
1314 
1315  // skip over self hash
1316  fileROBuf.getInt();
1317 
1318  // get the key
1319  if (longKeyAddresses != null) {
1320  // Key is stored outside of the entry
1321  try {
1322  CacheDataSegment data = new CacheDataSegment(longKeyAddresses, this.keyLen, true);
1323  key = data.getDataString();
1324  } catch (TskCoreException | IngestModuleException ex) {
1325  throw new TskCoreException(String.format("Failed to get external key from address %s", longKeyAddresses)); //NON-NLS
1326  }
1327  }
1328  else { // key stored within entry
1329  StringBuilder strBuilder = new StringBuilder(MAX_KEY_LEN);
1330  int keyLen = 0;
1331  while (fileROBuf.remaining() > 0 && keyLen < MAX_KEY_LEN) {
1332  char keyChar = (char)fileROBuf.get();
1333  if (keyChar == '\0') {
1334  break;
1335  }
1336  strBuilder.append(keyChar);
1337  keyLen++;
1338  }
1339 
1340  key = strBuilder.toString();
1341  }
1342  }
1343 
1344  public CacheAddress getCacheAddress() {
1345  return selfAddress;
1346  }
1347 
1348  public long getHash() {
1349  return hash;
1350  }
1351 
1352  public CacheAddress getNextCacheAddress() {
1353  return nextAddress;
1354  }
1355 
1356  public int getReuseCount() {
1357  return reuseCount;
1358  }
1359 
1360  public int getRefetchCount() {
1361  return refetchCount;
1362  }
1363 
1364  public EntryStateEnum getState() {
1365  return state;
1366  }
1367 
1368  public long getCreationTime() {
1369  return creationTime;
1370  }
1371 
1372  public long getFlags() {
1373  return flags;
1374  }
1375 
1376  public String getKey() {
1377  return key;
1378  }
1379 
1388  public List<CacheDataSegment> getDataSegments() throws TskCoreException, IngestModuleException {
1389 
1390  if (dataSegments == null) {
1391  dataSegments = new ArrayList<>();
1392  for (int i = 0; i < 4; i++) {
1393  if (dataSegmentSizes[i] > 0) {
1394  CacheDataSegment cacheData = new CacheDataSegment(dataSegmentIndexFileEntries[i], dataSegmentSizes[i], true );
1395 
1396  cacheData.extract();
1397  dataSegments.add(cacheData);
1398  }
1399  }
1400  }
1401  return dataSegments;
1402  }
1403 
1411  boolean hasHTTPHeaders() {
1412  if ((dataSegments == null) || dataSegments.isEmpty()) {
1413  return false;
1414  }
1415  return dataSegments.get(0).hasHTTPHeaders();
1416  }
1417 
1424  String getHTTPHeader(String key) {
1425  if ((dataSegments == null) || dataSegments.isEmpty()) {
1426  return null;
1427  }
1428  // First data segment has the HTTP headers, if any
1429  return dataSegments.get(0).getHTTPHeader(key);
1430  }
1431 
1437  String getHTTPHeaders() {
1438  if ((dataSegments == null) || dataSegments.isEmpty()) {
1439  return null;
1440  }
1441  // First data segment has the HTTP headers, if any
1442  return dataSegments.get(0).getHTTPHeaders();
1443  }
1444 
1453  boolean isBrotliCompressed() {
1454 
1455  if (hasHTTPHeaders() ) {
1456  String encodingHeader = getHTTPHeader("content-encoding");
1457  if (encodingHeader!= null) {
1458  return encodingHeader.trim().equalsIgnoreCase("br");
1459  }
1460  }
1461 
1462  return false;
1463  }
1464 
1465  @Override
1466  public String toString() {
1467  StringBuilder sb = new StringBuilder();
1468  sb.append(String.format("Entry = Hash: %08x, State: %s, ReuseCount: %d, RefetchCount: %d",
1469  this.hash, this.state.toString(), this.reuseCount, this.refetchCount ))
1470  .append(String.format("\n\tKey: %s, Keylen: %d",
1471  this.key, this.keyLen, this.reuseCount, this.refetchCount ))
1472  .append(String.format("\n\tCreationTime: %s",
1473  TimeUtilities.epochToTime(this.creationTime) ))
1474  .append(String.format("\n\tNext Address: %s",
1475  (nextAddress != null) ? nextAddress.toString() : "None"));
1476 
1477  for (int i = 0; i < 4; i++) {
1478  if (dataSegmentSizes[i] > 0) {
1479  sb.append(String.format("\n\tData %d: cache address = %s, Data = %s",
1480  i, dataSegmentIndexFileEntries[i].toString(),
1481  (dataSegments != null)
1482  ? dataSegments.get(i).toString()
1483  : "Data not retrived yet."));
1484  }
1485  }
1486 
1487  return sb.toString();
1488  }
1489  }
1490 }

Copyright © 2012-2021 Basis Technology. Generated on: Tue Jan 19 2021
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.