Autopsy  4.16.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  /* Cycle through index and get the CacheAddress for each CacheEntry. Process each entry
364  * to extract data, add artifacts, etc. from the f_XXXX and data_x files */
365  for (int i = 0; i < indexHdr.getTableLen(); i++) {
366 
367  if (context.dataSourceIngestIsCancelled()) {
368  cleanup();
369  return;
370  }
371 
372  CacheAddress addr = new CacheAddress(indexFileROBuffer.getInt() & UINT32_MASK, cacheFolderName);
373  if (addr.isInitialized()) {
374  progressBar.progress(NbBundle.getMessage(this.getClass(),
375  "ChromeCacheExtractor.progressMsg",
376  moduleName, i, indexHdr.getTableLen(), cacheFolderName) );
377  try {
378  List<DerivedFile> addedFiles = processCacheEntry(addr, artifactsAdded);
379  derivedFiles.addAll(addedFiles);
380  }
381  catch (TskCoreException | IngestModuleException ex) {
382  logger.log(Level.WARNING, String.format("Failed to get cache entry at address %s", addr), ex); //NON-NLS
383  }
384  }
385  }
386 
387  if (context.dataSourceIngestIsCancelled()) {
388  cleanup();
389  return;
390  }
391 
392 
393  // notify listeners of new files and schedule for analysis
394  progressBar.progress(String.format(Bundle.ChromeCacheExtract_adding_extracted_files_msg(), derivedFiles.size()));
395  derivedFiles.forEach((derived) -> {
396  services.fireModuleContentEvent(new ModuleContentEvent(derived));
397  });
398  context.addFilesToJob(derivedFiles);
399 
400  // notify listeners about new artifacts
401  progressBar.progress(String.format(Bundle.ChromeCacheExtract_adding_artifacts_msg(), artifactsAdded.size()));
402  Blackboard blackboard = currentCase.getSleuthkitCase().getBlackboard();
403  try {
404  blackboard.postArtifacts(artifactsAdded, moduleName);
405  } catch (Blackboard.BlackboardException ex) {
406  logger.log(Level.WARNING, String.format("Failed to post cacheIndex artifacts "), ex); //NON-NLS
407  }
408 
409  cleanup();
410  }
411 
424  private List<DerivedFile> processCacheEntry(CacheAddress cacheAddress, Collection<BlackboardArtifact> artifactsAdded ) throws TskCoreException, IngestModuleException {
425 
426  List<DerivedFile> derivedFiles = new ArrayList<>();
427 
428  // get the path to the corresponding data_X file for the cache entry
429  String cacheEntryFileName = cacheAddress.getFilename();
430  String cachePath = cacheAddress.getCachePath();
431 
432  Optional<FileWrapper> cacheEntryFileOptional = findDataOrIndexFile(cacheEntryFileName, cachePath);
433  if (!cacheEntryFileOptional.isPresent()) {
434  String msg = String.format("Failed to find data file %s", cacheEntryFileName); //NON-NLS
435  throw new IngestModuleException(msg);
436  }
437 
438  // Load the entry to get its metadata, segments, etc.
439  CacheEntry cacheEntry = new CacheEntry(cacheAddress, cacheEntryFileOptional.get() );
440  List<CacheDataSegment> dataSegments = cacheEntry.getDataSegments();
441 
442  // Only process the first payload data segment in each entry
443  // first data segement has the HTTP headers, 2nd is the payload
444  if (dataSegments.size() < 2) {
445  return derivedFiles;
446  }
447  CacheDataSegment dataSegment = dataSegments.get(1);
448 
449  // Name where segment is located (could be diffrent from where entry was located)
450  String segmentFileName = dataSegment.getCacheAddress().getFilename();
451  Optional<AbstractFile> segmentFileAbstractFile = findAbstractFile(segmentFileName, cachePath);
452  if (!segmentFileAbstractFile.isPresent()) {
453  logger.log(Level.WARNING, "Error finding segment file: " + cachePath + "/" + segmentFileName); //NON-NLS
454  return derivedFiles;
455  }
456 
457  boolean isBrotliCompressed = false;
458  if (dataSegment.getType() != CacheDataTypeEnum.HTTP_HEADER && cacheEntry.isBrotliCompressed() ) {
459  isBrotliCompressed = true;
460  }
461 
462 
463  // Make artifacts around the cached item and extract data from data_X file
464  try {
465  AbstractFile cachedItemFile; //
466  /* If the cached data is in a f_XXXX file, we only need to make artifacts. */
467  if (dataSegment.isInExternalFile() ) {
468  cachedItemFile = segmentFileAbstractFile.get();
469  }
470  /* If the data is in a data_X file, we need to extract it out and then make the similar artifacts */
471  else {
472 
473  // Data segments in "data_x" files are saved in individual files and added as derived files
474  String filename = dataSegment.save();
475  String relPathname = getRelOutputFolderName() + dataSegment.getCacheAddress().getCachePath() + filename;
476 
477  // @@@ We should batch these up and do them in one big insert / transaction
478  DerivedFile derivedFile = fileManager.addDerivedFile(filename, relPathname,
479  dataSegment.getDataLength(),
480  cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), // TBD
481  true,
482  segmentFileAbstractFile.get(),
483  "",
484  moduleName,
485  VERSION_NUMBER,
486  "",
487  TskData.EncodingType.NONE);
488 
489  derivedFiles.add(derivedFile);
490  cachedItemFile = derivedFile;
491  }
492 
493  addArtifacts(cacheEntry, cacheEntryFileOptional.get().getAbstractFile(), cachedItemFile, artifactsAdded);
494 
495  // Tika doesn't detect these types. So, make sure they have the correct MIME type */
496  if (isBrotliCompressed) {
497  cachedItemFile.setMIMEType(BROTLI_MIMETYPE);
498  cachedItemFile.save();
499  }
500 
501  } catch (TskException ex) {
502  logger.log(Level.SEVERE, "Error while trying to add an artifact", ex); //NON-NLS
503  }
504 
505  return derivedFiles;
506  }
507 
517  private void addArtifacts(CacheEntry cacheEntry, AbstractFile cacheEntryFile, AbstractFile cachedItemFile, Collection<BlackboardArtifact> artifactsAdded) throws TskCoreException {
518 
519  // Create a TSK_WEB_CACHE entry with the parent as data_X file that had the cache entry
520  BlackboardArtifact webCacheArtifact = cacheEntryFile.newArtifact(ARTIFACT_TYPE.TSK_WEB_CACHE);
521  if (webCacheArtifact != null) {
522  Collection<BlackboardAttribute> webAttr = new ArrayList<>();
523  String url = cacheEntry.getKey() != null ? cacheEntry.getKey() : "";
524  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
525  moduleName, url));
526  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DOMAIN,
527  moduleName, NetworkUtils.extractDomain(url)));
528  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED,
529  moduleName, cacheEntry.getCreationTime()));
530  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_HEADERS,
531  moduleName, cacheEntry.getHTTPHeaders()));
532  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH,
533  moduleName, cachedItemFile.getUniquePath()));
534  webAttr.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID,
535  moduleName, cachedItemFile.getId()));
536  webCacheArtifact.addAttributes(webAttr);
537  artifactsAdded.add(webCacheArtifact);
538 
539  // Create a TSK_ASSOCIATED_OBJECT on the f_XXX or derived file file back to the CACHE entry
540  BlackboardArtifact associatedObjectArtifact = cachedItemFile.newArtifact(ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT);
541  if (associatedObjectArtifact != null) {
542  associatedObjectArtifact.addAttribute(
543  new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT,
544  moduleName, webCacheArtifact.getArtifactID()));
545  artifactsAdded.add(associatedObjectArtifact);
546  }
547  }
548  }
549 
558  private void findExternalFiles(String cachePath) throws TskCoreException {
559 
560  List<AbstractFile> effFiles = fileManager.findFiles(dataSource, "f_%", cachePath); //NON-NLS
561  for (AbstractFile abstractFile : effFiles ) {
562  if (cachePath.equals(abstractFile.getParentPath()) && abstractFile.isFile()) {
563  this.externalFilesTable.put(cachePath + abstractFile.getName(), abstractFile);
564  }
565  }
566  }
575  private Optional<AbstractFile> findAbstractFile(String cacheFileName, String cacheFolderName) throws TskCoreException {
576 
577  // see if it is cached
578  String fileTableKey = cacheFolderName + cacheFileName;
579  if (cacheFileName.startsWith("f_") && externalFilesTable.containsKey(fileTableKey)) {
580  return Optional.of(externalFilesTable.get(fileTableKey));
581  }
582 
583  if (fileCopyCache.containsKey(fileTableKey)) {
584  return Optional.of(fileCopyCache.get(fileTableKey).getAbstractFile());
585  }
586 
587 
588  List<AbstractFile> cacheFiles = fileManager.findFiles(dataSource, cacheFileName, cacheFolderName); //NON-NLS
589  if (!cacheFiles.isEmpty()) {
590  for (AbstractFile abstractFile: cacheFiles ) {
591  if (abstractFile.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)) {
592  return Optional.of(abstractFile);
593  }
594  }
595  return Optional.of(cacheFiles.get(0));
596  }
597 
598  return Optional.empty();
599  }
600 
608  private List<AbstractFile> findIndexFiles() throws TskCoreException {
609  return fileManager.findFiles(dataSource, "index", DEFAULT_CACHE_PATH_STR); //NON-NLS
610  }
611 
612 
613 
625  private Optional<FileWrapper> findDataOrIndexFile(String cacheFileName, String cacheFolderName) throws TskCoreException, IngestModuleException {
626 
627  // Check if the file is already in the cache
628  String fileTableKey = cacheFolderName + cacheFileName;
629  if (fileCopyCache.containsKey(fileTableKey)) {
630  return Optional.of(fileCopyCache.get(fileTableKey));
631  }
632 
633  // Use Autopsy to get the AbstractFile
634  Optional<AbstractFile> abstractFileOptional = findAbstractFile(cacheFileName, cacheFolderName);
635  if (!abstractFileOptional.isPresent()) {
636  return Optional.empty();
637  }
638 
639  // Wrap the file so that we can get the ByteBuffer later.
640  // @@@ BC: I think this should nearly all go into FileWrapper and be done lazily and perhaps based on size.
641  // Many of the files are small enough to keep in memory for the ByteBuffer
642 
643  // write the file to disk so that we can have a memory-mapped ByteBuffer
644  AbstractFile cacheFile = abstractFileOptional.get();
645  RandomAccessFile randomAccessFile = null;
646  String tempFilePathname = RAImageIngestModule.getRATempPath(currentCase, moduleName) + cacheFolderName + cacheFile.getName(); //NON-NLS
647  try {
648  File newFile = new File(tempFilePathname);
649  ContentUtils.writeToFile(cacheFile, newFile, context::dataSourceIngestIsCancelled);
650 
651  randomAccessFile = new RandomAccessFile(tempFilePathname, "r");
652  FileChannel roChannel = randomAccessFile.getChannel();
653  ByteBuffer cacheFileROBuf = roChannel.map(FileChannel.MapMode.READ_ONLY, 0,
654  (int) roChannel.size());
655 
656  cacheFileROBuf.order(ByteOrder.nativeOrder());
657  FileWrapper cacheFileWrapper = new FileWrapper(cacheFile, randomAccessFile, cacheFileROBuf );
658 
659  if (!cacheFileName.startsWith("f_")) {
660  fileCopyCache.put(cacheFolderName + cacheFileName, cacheFileWrapper);
661  }
662 
663  return Optional.of(cacheFileWrapper);
664  }
665  catch (IOException ex) {
666 
667  try {
668  if (randomAccessFile != null) {
669  randomAccessFile.close();
670  }
671  }
672  catch (IOException ex2) {
673  logger.log(Level.SEVERE, "Error while trying to close temp file after exception.", ex2); //NON-NLS
674  }
675  String msg = String.format("Error reading/copying Chrome cache file '%s' (id=%d).", //NON-NLS
676  cacheFile.getName(), cacheFile.getId());
677  throw new IngestModuleException(msg, ex);
678  }
679  }
680 
684  final class IndexFileHeader {
685 
686  private final long magic;
687  private final int version;
688  private final int numEntries;
689  private final int numBytes;
690  private final int lastFile;
691  private final int tableLen;
692 
693  IndexFileHeader(ByteBuffer indexFileROBuf) {
694 
695  magic = indexFileROBuf.getInt() & UINT32_MASK;
696 
697  indexFileROBuf.position(indexFileROBuf.position()+2);
698 
699  version = indexFileROBuf.getShort();
700  numEntries = indexFileROBuf.getInt();
701  numBytes = indexFileROBuf.getInt();
702  lastFile = indexFileROBuf.getInt();
703 
704  indexFileROBuf.position(indexFileROBuf.position()+4); // this_id
705  indexFileROBuf.position(indexFileROBuf.position()+4); // stats cache cacheAddress
706 
707  tableLen = indexFileROBuf.getInt();
708  }
709 
710  public long getMagic() {
711  return magic;
712  }
713 
714  public int getVersion() {
715  return version;
716  }
717 
718  public int getNumEntries() {
719  return numEntries;
720  }
721 
722  public int getNumBytes() {
723  return numBytes;
724  }
725 
726  public int getLastFile() {
727  return lastFile;
728  }
729 
730  public int getTableLen() {
731  return tableLen;
732  }
733 
734  @Override
735  public String toString() {
736  StringBuilder sb = new StringBuilder();
737 
738  sb.append(String.format("Index Header:"))
739  .append(String.format("\tMagic = %x" , getMagic()) )
740  .append(String.format("\tVersion = %x" , getVersion()) )
741  .append(String.format("\tNumEntries = %x" , getNumEntries()) )
742  .append(String.format("\tNumBytes = %x" , getNumBytes()) )
743  .append(String.format("\tLastFile = %x" , getLastFile()) )
744  .append(String.format("\tTableLen = %x" , getTableLen()) );
745 
746  return sb.toString();
747  }
748  }
749 
753  enum CacheFileTypeEnum {
754  EXTERNAL,
755  RANKINGS,
756  BLOCK_256,
757  BLOCK_1K,
758  BLOCK_4K,
759  BLOCK_FILES,
760  BLOCK_ENTRIES,
761  BLOCK_EVICTED
762  }
763 
764 
765 
788  final class CacheAddress {
789  // sundry constants to parse the bit fields
790  private static final long ADDR_INITIALIZED_MASK = 0x80000000l;
791  private static final long FILE_TYPE_MASK = 0x70000000;
792  private static final long FILE_TYPE_OFFSET = 28;
793  private static final long NUM_BLOCKS_MASK = 0x03000000;
794  private static final long NUM_BLOCKS_OFFSET = 24;
795  private static final long FILE_SELECTOR_MASK = 0x00ff0000;
796  private static final long FILE_SELECTOR_OFFSET = 16;
797  private static final long START_BLOCK_MASK = 0x0000FFFF;
798  private static final long EXTERNAL_FILE_NAME_MASK = 0x0FFFFFFF;
799 
800  private final long uint32CacheAddr;
801  private final CacheFileTypeEnum fileType;
802  private final int numBlocks;
803  private final int startBlock;
804  private final String fileName;
805  private final int fileNumber;
806 
807  private final String cachePath;
808 
809 
815  CacheAddress(long uint32, String cachePath) {
816 
817  uint32CacheAddr = uint32;
818  this.cachePath = cachePath;
819 
820 
821  // analyze the
822  int fileTypeEnc = (int)(uint32CacheAddr & FILE_TYPE_MASK) >> FILE_TYPE_OFFSET;
823  fileType = CacheFileTypeEnum.values()[fileTypeEnc];
824 
825  if (isInitialized()) {
826  if (isInExternalFile()) {
827  fileNumber = (int)(uint32CacheAddr & EXTERNAL_FILE_NAME_MASK);
828  fileName = String.format("f_%06x", getFileNumber() );
829  numBlocks = 0;
830  startBlock = 0;
831  } else {
832  fileNumber = (int)((uint32CacheAddr & FILE_SELECTOR_MASK) >> FILE_SELECTOR_OFFSET);
833  fileName = String.format("data_%d", getFileNumber() );
834  numBlocks = (int)(uint32CacheAddr & NUM_BLOCKS_MASK >> NUM_BLOCKS_OFFSET);
835  startBlock = (int)(uint32CacheAddr & START_BLOCK_MASK);
836  }
837  }
838  else {
839  fileName = null;
840  fileNumber = 0;
841  numBlocks = 0;
842  startBlock = 0;
843  }
844  }
845 
846  boolean isInitialized() {
847  return ((uint32CacheAddr & ADDR_INITIALIZED_MASK) != 0);
848  }
849 
850  CacheFileTypeEnum getFileType() {
851  return fileType;
852  }
853 
858  String getFilename() {
859  return fileName;
860  }
861 
862  String getCachePath() {
863  return cachePath;
864  }
865 
866  boolean isInExternalFile() {
867  return (fileType == CacheFileTypeEnum.EXTERNAL);
868  }
869 
870  int getFileNumber() {
871  return fileNumber;
872  }
873 
874  int getStartBlock() {
875  return startBlock;
876  }
877 
878  int getNumBlocks() {
879  return numBlocks;
880  }
881 
882  int getBlockSize() {
883  switch (fileType) {
884  case RANKINGS:
885  return 36;
886  case BLOCK_256:
887  return 256;
888  case BLOCK_1K:
889  return 1024;
890  case BLOCK_4K:
891  return 4096;
892  case BLOCK_FILES:
893  return 8;
894  case BLOCK_ENTRIES:
895  return 104;
896  case BLOCK_EVICTED:
897  return 48;
898  default:
899  return 0;
900  }
901  }
902 
903  public long getUint32CacheAddr() {
904  return uint32CacheAddr;
905  }
906 
907  @Override
908  public String toString() {
909  StringBuilder sb = new StringBuilder();
910  sb.append(String.format("CacheAddr %08x : %s : filename %s",
911  uint32CacheAddr,
912  isInitialized() ? "Initialized" : "UnInitialized",
913  getFilename()));
914 
915  if ((fileType == CacheFileTypeEnum.BLOCK_256) ||
916  (fileType == CacheFileTypeEnum.BLOCK_1K) ||
917  (fileType == CacheFileTypeEnum.BLOCK_4K) ) {
918  sb.append(String.format(" (%d blocks starting at %08X)",
919  this.getNumBlocks(),
920  this.getStartBlock()
921  ));
922  }
923 
924  return sb.toString();
925  }
926 
927  }
928 
932  enum CacheDataTypeEnum {
933  HTTP_HEADER,
934  UNKNOWN,
935  };
936 
946  final class CacheDataSegment {
947 
948  private int length;
949  private final CacheAddress cacheAddress;
950  private CacheDataTypeEnum type;
951 
952  private boolean isHTTPHeaderHint;
953 
954  private FileWrapper cacheFileCopy = null;
955  private byte[] data = null;
956 
957  private String httpResponse;
958  private final Map<String, String> httpHeaders = new HashMap<>();
959 
960  CacheDataSegment(CacheAddress cacheAddress, int len) {
961  this(cacheAddress, len, false);
962  }
963 
964  CacheDataSegment(CacheAddress cacheAddress, int len, boolean isHTTPHeader ) {
965  this.type = CacheDataTypeEnum.UNKNOWN;
966  this.length = len;
967  this.cacheAddress = cacheAddress;
968  this.isHTTPHeaderHint = isHTTPHeader;
969  }
970 
971  boolean isInExternalFile() {
972  return cacheAddress.isInExternalFile();
973  }
974 
975  boolean hasHTTPHeaders() {
976  return this.type == CacheDataTypeEnum.HTTP_HEADER;
977  }
978 
979  String getHTTPHeader(String key) {
980  return this.httpHeaders.get(key);
981  }
982 
988  String getHTTPHeaders() {
989  if (!hasHTTPHeaders()) {
990  return "";
991  }
992 
993  StringBuilder sb = new StringBuilder();
994  httpHeaders.entrySet().forEach((entry) -> {
995  if (sb.length() > 0) {
996  sb.append(" \n");
997  }
998  sb.append(String.format("%s : %s",
999  entry.getKey(), entry.getValue()));
1000  });
1001 
1002  return sb.toString();
1003  }
1004 
1005  String getHTTPRespone() {
1006  return httpResponse;
1007  }
1008 
1014  void extract() throws TskCoreException, IngestModuleException {
1015 
1016  // do nothing if already extracted,
1017  if (data != null) {
1018  return;
1019  }
1020 
1021  // Don't extract data from external files.
1022  if (!cacheAddress.isInExternalFile() ) {
1023 
1024  cacheFileCopy = findDataOrIndexFile(cacheAddress.getFilename(), cacheAddress.getCachePath()).get();
1025 
1026  this.data = new byte [length];
1027  ByteBuffer buf = cacheFileCopy.getByteBuffer();
1028  int dataOffset = DATAFILE_HDR_SIZE + cacheAddress.getStartBlock() * cacheAddress.getBlockSize();
1029  buf.position(dataOffset);
1030  buf.get(data, 0, length);
1031 
1032  // if this might be a HTPP header, lets try to parse it as such
1033  if ((isHTTPHeaderHint)) {
1034  String strData = new String(data);
1035  if (strData.contains("HTTP")) {
1036 
1037  // Http headers if present, are usually in frst data segment in an entry
1038  // General Parsing algo:
1039  // - Find start of HTTP header by searching for string "HTTP"
1040  // - Skip to the first 0x00 to get to the end of the HTTP response line, this makrs start of headers section
1041  // - Find the end of the header by searching for 0x00 0x00 bytes
1042  // - Extract the headers section
1043  // - Parse the headers section - each null terminated string is a header
1044  // - Each header is of the format "name: value" e.g.
1045 
1046  type = CacheDataTypeEnum.HTTP_HEADER;
1047 
1048  int startOff = strData.indexOf("HTTP");
1049  Charset charset = Charset.forName("UTF-8");
1050  boolean done = false;
1051  int i = startOff;
1052  int hdrNum = 1;
1053 
1054  while (!done) {
1055  // each header is null terminated
1056  int start = i;
1057  while (i < data.length && data[i] != 0) {
1058  i++;
1059  }
1060 
1061  // http headers are terminated by 0x00 0x00
1062  if (i == data.length || data[i+1] == 0) {
1063  done = true;
1064  }
1065 
1066  int len = (i - start);
1067  String headerLine = new String(data, start, len, charset);
1068 
1069  // first line is the http response
1070  if (hdrNum == 1) {
1071  httpResponse = headerLine;
1072  } else {
1073  int nPos = headerLine.indexOf(':');
1074  if (nPos > 0 ) {
1075  String key = headerLine.substring(0, nPos);
1076  String val= headerLine.substring(nPos+1);
1077  httpHeaders.put(key.toLowerCase(), val);
1078  }
1079  }
1080 
1081  i++;
1082  hdrNum++;
1083  }
1084  }
1085  }
1086  }
1087  }
1088 
1089  String getDataString() throws TskCoreException, IngestModuleException {
1090  if (data == null) {
1091  extract();
1092  }
1093  return new String(data);
1094  }
1095 
1096  byte[] getDataBytes() throws TskCoreException, IngestModuleException {
1097  if (data == null) {
1098  extract();
1099  }
1100  return data.clone();
1101  }
1102 
1103  int getDataLength() {
1104  return this.length;
1105  }
1106 
1107  CacheDataTypeEnum getType() {
1108  return type;
1109  }
1110 
1111  CacheAddress getCacheAddress() {
1112  return cacheAddress;
1113  }
1114 
1115 
1124  String save() throws TskCoreException, IngestModuleException {
1125  String fileName;
1126 
1127  if (cacheAddress.isInExternalFile()) {
1128  fileName = cacheAddress.getFilename();
1129  } else {
1130  fileName = String.format("%s__%08x", cacheAddress.getFilename(), cacheAddress.getUint32CacheAddr());
1131  }
1132 
1133  String filePathName = getAbsOutputFolderName() + cacheAddress.getCachePath() + fileName;
1134  save(filePathName);
1135 
1136  return fileName;
1137  }
1138 
1148  void save(String filePathName) throws TskCoreException, IngestModuleException {
1149 
1150  // Save the data to specified file
1151  if (data == null) {
1152  extract();
1153  }
1154 
1155  // Data in external files is not saved in local files
1156  if (!this.isInExternalFile()) {
1157  // write the
1158  try (FileOutputStream stream = new FileOutputStream(filePathName)) {
1159  stream.write(data);
1160  } catch (IOException ex) {
1161  throw new TskCoreException(String.format("Failed to write output file %s", filePathName), ex);
1162  }
1163  }
1164  }
1165 
1166  @Override
1167  public String toString() {
1168  StringBuilder strBuilder = new StringBuilder();
1169  strBuilder.append(String.format("\t\tData type = : %s, Data Len = %d ",
1170  this.type.toString(), this.length ));
1171 
1172  if (hasHTTPHeaders()) {
1173  String str = getHTTPHeader("content-encoding");
1174  if (str != null) {
1175  strBuilder.append(String.format("\t%s=%s", "content-encoding", str ));
1176  }
1177  }
1178 
1179  return strBuilder.toString();
1180  }
1181 
1182  }
1183 
1184 
1188  enum EntryStateEnum {
1189  ENTRY_NORMAL,
1190  ENTRY_EVICTED,
1191  ENTRY_DOOMED
1192  };
1193 
1194 
1195 // Main structure for an entry on the backing storage.
1196 //
1197 // Each entry has a key, identifying the URL the cache entry pertains to.
1198 // If the key is longer than
1199 // what can be stored on this structure, it will be extended on consecutive
1200 // blocks (adding 256 bytes each time), up to 4 blocks (1024 - 32 - 1 chars).
1201 // After that point, the whole key will be stored as a data block or external
1202 // file.
1203 //
1204 // Each entry can have upto 4 data segments
1205 //
1206 // struct EntryStore {
1207 // uint32 hash; // Full hash of the key.
1208 // CacheAddr next; // Next entry with the same hash or bucket.
1209 // CacheAddr rankings_node; // Rankings node for this entry.
1210 // int32 reuse_count; // How often is this entry used.
1211 // int32 refetch_count; // How often is this fetched from the net.
1212 // int32 state; // Current state.
1213 // uint64 creation_time;
1214 // int32 key_len;
1215 // CacheAddr long_key; // Optional cacheAddress of a long key.
1216 // int32 data_size[4]; // We can store up to 4 data streams for each
1217 // CacheAddr data_addr[4]; // entry.
1218 // uint32 flags; // Any combination of EntryFlags.
1219 // int32 pad[4];
1220 // uint32 self_hash; // The hash of EntryStore up to this point.
1221 // char key[256 - 24 * 4]; // null terminated
1222 // };
1223 
1227  final class CacheEntry {
1228 
1229  // each entry is 256 bytes. The last section of the entry, after all the other fields is a null terminated key
1230  private static final int MAX_KEY_LEN = 256-24*4;
1231 
1232  private final CacheAddress selfAddress;
1233  private final FileWrapper cacheFileCopy;
1234 
1235  private final long hash;
1236  private final CacheAddress nextAddress;
1237  private final CacheAddress rankingsNodeAddress;
1238 
1239  private final int reuseCount;
1240  private final int refetchCount;
1241  private final EntryStateEnum state;
1242 
1243  private final long creationTime;
1244  private final int keyLen;
1245 
1246  private final CacheAddress longKeyAddresses; // cacheAddress of the key, if the key is external to the entry
1247 
1248  private final int[] dataSegmentSizes;
1249  private final CacheAddress[] dataSegmentIndexFileEntries;
1250  private List<CacheDataSegment> dataSegments;
1251 
1252  private final long flags;
1253 
1254  private String key; // Key may be found within the entry or may be external
1255 
1256  CacheEntry(CacheAddress cacheAdress, FileWrapper cacheFileCopy ) {
1257  this.selfAddress = cacheAdress;
1258  this.cacheFileCopy = cacheFileCopy;
1259 
1260  ByteBuffer fileROBuf = cacheFileCopy.getByteBuffer();
1261 
1262  int entryOffset = DATAFILE_HDR_SIZE + cacheAdress.getStartBlock() * cacheAdress.getBlockSize();
1263 
1264  // reposition the buffer to the the correct offset
1265  fileROBuf.position(entryOffset);
1266 
1267  hash = fileROBuf.getInt() & UINT32_MASK;
1268 
1269  long uint32 = fileROBuf.getInt() & UINT32_MASK;
1270  nextAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1271 
1272  uint32 = fileROBuf.getInt() & UINT32_MASK;
1273  rankingsNodeAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1274 
1275  reuseCount = fileROBuf.getInt();
1276  refetchCount = fileROBuf.getInt();
1277 
1278  state = EntryStateEnum.values()[fileROBuf.getInt()];
1279  creationTime = (fileROBuf.getLong() / 1000000) - Long.valueOf("11644473600");
1280 
1281  keyLen = fileROBuf.getInt();
1282 
1283  uint32 = fileROBuf.getInt() & UINT32_MASK;
1284  longKeyAddresses = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1285 
1286  dataSegments = null;
1287  dataSegmentSizes= new int[4];
1288  for (int i = 0; i < 4; i++) {
1289  dataSegmentSizes[i] = fileROBuf.getInt();
1290  }
1291  dataSegmentIndexFileEntries = new CacheAddress[4];
1292  for (int i = 0; i < 4; i++) {
1293  dataSegmentIndexFileEntries[i] = new CacheAddress(fileROBuf.getInt() & UINT32_MASK, selfAddress.getCachePath());
1294  }
1295 
1296  flags = fileROBuf.getInt() & UINT32_MASK;
1297  // skip over pad
1298  for (int i = 0; i < 4; i++) {
1299  fileROBuf.getInt();
1300  }
1301 
1302  // skip over self hash
1303  fileROBuf.getInt();
1304 
1305  // get the key
1306  if (longKeyAddresses != null) {
1307  // Key is stored outside of the entry
1308  try {
1309  CacheDataSegment data = new CacheDataSegment(longKeyAddresses, this.keyLen, true);
1310  key = data.getDataString();
1311  } catch (TskCoreException | IngestModuleException ex) {
1312  logger.log(Level.WARNING, String.format("Failed to get external key from address %s", longKeyAddresses)); //NON-NLS
1313  }
1314  }
1315  else { // key stored within entry
1316  StringBuilder strBuilder = new StringBuilder(MAX_KEY_LEN);
1317  int keyLen = 0;
1318  while (fileROBuf.remaining() > 0 && keyLen < MAX_KEY_LEN) {
1319  char keyChar = (char)fileROBuf.get();
1320  if (keyChar == '\0') {
1321  break;
1322  }
1323  strBuilder.append(keyChar);
1324  keyLen++;
1325  }
1326 
1327  key = strBuilder.toString();
1328  }
1329  }
1330 
1331  public CacheAddress getCacheAddress() {
1332  return selfAddress;
1333  }
1334 
1335  public long getHash() {
1336  return hash;
1337  }
1338 
1339  public CacheAddress getNextCacheAddress() {
1340  return nextAddress;
1341  }
1342 
1343  public int getReuseCount() {
1344  return reuseCount;
1345  }
1346 
1347  public int getRefetchCount() {
1348  return refetchCount;
1349  }
1350 
1351  public EntryStateEnum getState() {
1352  return state;
1353  }
1354 
1355  public long getCreationTime() {
1356  return creationTime;
1357  }
1358 
1359  public long getFlags() {
1360  return flags;
1361  }
1362 
1363  public String getKey() {
1364  return key;
1365  }
1366 
1375  public List<CacheDataSegment> getDataSegments() throws TskCoreException, IngestModuleException {
1376 
1377  if (dataSegments == null) {
1378  dataSegments = new ArrayList<>();
1379  for (int i = 0; i < 4; i++) {
1380  if (dataSegmentSizes[i] > 0) {
1381  CacheDataSegment cacheData = new CacheDataSegment(dataSegmentIndexFileEntries[i], dataSegmentSizes[i], true );
1382 
1383  cacheData.extract();
1384  dataSegments.add(cacheData);
1385  }
1386  }
1387  }
1388  return dataSegments;
1389  }
1390 
1398  boolean hasHTTPHeaders() {
1399  if ((dataSegments == null) || dataSegments.isEmpty()) {
1400  return false;
1401  }
1402  return dataSegments.get(0).hasHTTPHeaders();
1403  }
1404 
1411  String getHTTPHeader(String key) {
1412  if ((dataSegments == null) || dataSegments.isEmpty()) {
1413  return null;
1414  }
1415  // First data segment has the HTTP headers, if any
1416  return dataSegments.get(0).getHTTPHeader(key);
1417  }
1418 
1424  String getHTTPHeaders() {
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).getHTTPHeaders();
1430  }
1431 
1440  boolean isBrotliCompressed() {
1441 
1442  if (hasHTTPHeaders() ) {
1443  String encodingHeader = getHTTPHeader("content-encoding");
1444  if (encodingHeader!= null) {
1445  return encodingHeader.trim().equalsIgnoreCase("br");
1446  }
1447  }
1448 
1449  return false;
1450  }
1451 
1452  @Override
1453  public String toString() {
1454  StringBuilder sb = new StringBuilder();
1455  sb.append(String.format("Entry = Hash: %08x, State: %s, ReuseCount: %d, RefetchCount: %d",
1456  this.hash, this.state.toString(), this.reuseCount, this.refetchCount ))
1457  .append(String.format("\n\tKey: %s, Keylen: %d",
1458  this.key, this.keyLen, this.reuseCount, this.refetchCount ))
1459  .append(String.format("\n\tCreationTime: %s",
1460  TimeUtilities.epochToTime(this.creationTime) ))
1461  .append(String.format("\n\tNext Address: %s",
1462  (nextAddress != null) ? nextAddress.toString() : "None"));
1463 
1464  for (int i = 0; i < 4; i++) {
1465  if (dataSegmentSizes[i] > 0) {
1466  sb.append(String.format("\n\tData %d: cache address = %s, Data = %s",
1467  i, dataSegmentIndexFileEntries[i].toString(),
1468  (dataSegments != null)
1469  ? dataSegments.get(i).toString()
1470  : "Data not retrived yet."));
1471  }
1472  }
1473 
1474  return sb.toString();
1475  }
1476  }
1477 }

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