Autopsy  4.13.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;
53 import org.sleuthkit.datamodel.AbstractFile;
54 import org.sleuthkit.datamodel.Blackboard;
55 import org.sleuthkit.datamodel.BlackboardArtifact;
56 import org.sleuthkit.datamodel.BlackboardArtifact.ARTIFACT_TYPE;
57 import org.sleuthkit.datamodel.BlackboardAttribute;
58 import org.sleuthkit.datamodel.Content;
59 import org.sleuthkit.datamodel.DerivedFile;
60 import org.sleuthkit.datamodel.TimeUtilities;
61 import org.sleuthkit.datamodel.TskCoreException;
62 import org.sleuthkit.datamodel.TskData;
63 import org.sleuthkit.datamodel.TskException;
64 
77 final class ChromeCacheExtractor {
78 
79  private final static String DEFAULT_CACHE_PATH_STR = "default/cache"; //NON-NLS
80  private final static String BROTLI_MIMETYPE ="application/x-brotli"; //NON-NLS
81 
82  private final static long UINT32_MASK = 0xFFFFFFFFl;
83 
84  private final static int INDEXFILE_HDR_SIZE = 92*4;
85  private final static int DATAFILE_HDR_SIZE = 8192;
86 
87  private final static Logger logger = Logger.getLogger(ChromeCacheExtractor.class.getName());
88 
89  private static final String VERSION_NUMBER = "1.0.0"; //NON-NLS
90  private final String moduleName;
91 
92  private String absOutputFolderName;
93  private String relOutputFolderName;
94 
95  private final Content dataSource;
96  private final IngestJobContext context;
97  private final DataSourceIngestModuleProgress progressBar;
98  private final IngestServices services = IngestServices.getInstance();
99  private Case currentCase;
100  private FileManager fileManager;
101 
102  // A file table to cache copies of index and data_n files.
103  private final Map<String, CacheFileCopy> fileCopyCache = new HashMap<>();
104 
105  // A file table to cache the f_* files.
106  private final Map<String, AbstractFile> externalFilesTable = new HashMap<>();
107 
112  final class CacheFileCopy {
113 
114  private final AbstractFile abstractFile;
115  private final RandomAccessFile fileCopy;
116  private final ByteBuffer byteBuffer;
117 
118  CacheFileCopy (AbstractFile abstractFile, RandomAccessFile fileCopy, ByteBuffer buffer ) {
119  this.abstractFile = abstractFile;
120  this.fileCopy = fileCopy;
121  this.byteBuffer = buffer;
122  }
123 
124  public RandomAccessFile getFileCopy() {
125  return fileCopy;
126  }
127  public ByteBuffer getByteBuffer() {
128  return byteBuffer;
129  }
130  AbstractFile getAbstractFile() {
131  return abstractFile;
132  }
133  }
134 
135  @NbBundle.Messages({
136  "ChromeCacheExtractor.moduleName=ChromeCacheExtractor",
137  "# {0} - module name",
138  "# {1} - row number",
139  "# {2} - table length",
140  "# {3} - cache path",
141  "ChromeCacheExtractor.progressMsg={0}: Extracting cache entry {1} of {2} entries from {3}"
142  })
143  ChromeCacheExtractor(Content dataSource, IngestJobContext context, DataSourceIngestModuleProgress progressBar ) {
144  moduleName = Bundle.ChromeCacheExtractor_moduleName();
145  this.dataSource = dataSource;
146  this.context = context;
147  this.progressBar = progressBar;
148  }
149 
150 
156  private void moduleInit() throws IngestModuleException {
157 
158  try {
159  currentCase = Case.getCurrentCaseThrows();
160  fileManager = currentCase.getServices().getFileManager();
161 
162  // Create an output folder to save any derived files
163  absOutputFolderName = RAImageIngestModule.getRAOutputPath(currentCase, moduleName);
164  relOutputFolderName = Paths.get( RAImageIngestModule.getRelModuleOutputPath(), moduleName).normalize().toString();
165 
166  File dir = new File(absOutputFolderName);
167  if (dir.exists() == false) {
168  dir.mkdirs();
169  }
170  } catch (NoCurrentCaseException ex) {
171  String msg = "Failed to get current case."; //NON-NLS
172  throw new IngestModuleException(msg, ex);
173  }
174  }
175 
183  private void resetForNewFolder(String cachePath) throws IngestModuleException {
184 
185  fileCopyCache.clear();
186  externalFilesTable.clear();
187 
188  String cacheAbsOutputFolderName = this.getAbsOutputFolderName() + cachePath;
189  File outDir = new File(cacheAbsOutputFolderName);
190  if (outDir.exists() == false) {
191  outDir.mkdirs();
192  }
193 
194  String cacheTempPath = RAImageIngestModule.getRATempPath(currentCase, moduleName) + cachePath;
195  File tempDir = new File(cacheTempPath);
196  if (tempDir.exists() == false) {
197  tempDir.mkdirs();
198  }
199  }
200 
207  private void cleanup () {
208 
209  for (Entry<String, CacheFileCopy> entry : this.fileCopyCache.entrySet()) {
210  Path tempFilePath = Paths.get(RAImageIngestModule.getRATempPath(currentCase, moduleName), entry.getKey() );
211  try {
212  entry.getValue().getFileCopy().getChannel().close();
213  entry.getValue().getFileCopy().close();
214 
215  File tmpFile = tempFilePath.toFile();
216  if (!tmpFile.delete()) {
217  tmpFile.deleteOnExit();
218  }
219  } catch (IOException ex) {
220  logger.log(Level.WARNING, String.format("Failed to delete cache file copy %s", tempFilePath.toString()), ex); //NON-NLS
221  }
222  }
223  }
224 
230  private String getAbsOutputFolderName() {
231  return absOutputFolderName;
232  }
233 
239  private String getRelOutputFolderName() {
240  return relOutputFolderName;
241  }
242 
249  void getCaches() {
250 
251  try {
252  moduleInit();
253  } catch (IngestModuleException ex) {
254  String msg = "Failed to initialize ChromeCacheExtractor."; //NON-NLS
255  logger.log(Level.SEVERE, msg, ex);
256  return;
257  }
258 
259  // Find and process the cache folders. There could be one per user
260  List<AbstractFile> indexFiles;
261  try {
262  indexFiles = findCacheIndexFiles();
263 
264  // Process each of the caches
265  for (AbstractFile indexFile: indexFiles) {
266 
267  if (context.dataSourceIngestIsCancelled()) {
268  return;
269  }
270 
271  processCacheIndexFile(indexFile);
272  }
273 
274  } catch (TskCoreException ex) {
275  String msg = "Failed to find cache index files"; //NON-NLS
276  logger.log(Level.WARNING, msg, ex);
277  }
278  }
279 
280  @Messages({
281  "ChromeCacheExtract_adding_extracted_files_msg=Adding %d extracted files for analysis."
282  })
283 
289  private void processCacheIndexFile(AbstractFile indexAbstractFile) {
290 
291  String cachePath = indexAbstractFile.getParentPath();
292  Optional<CacheFileCopy> indexFileCopy;
293  try {
294  resetForNewFolder(cachePath);
295 
296  // @@@ This is little ineffecient because we later in this call search for the AbstractFile that we currently have
297  indexFileCopy = this.getCacheFileCopy(indexAbstractFile.getName(), cachePath);
298  if (!indexFileCopy.isPresent()) {
299  String msg = String.format("Failed to find copy cache index file %s", indexAbstractFile.getUniquePath());
300  logger.log(Level.WARNING, msg);
301  return;
302  }
303 
304 
305  // load the data files. We do this now to load them into the cache
306  for (int i = 0; i < 4; i ++) {
307  Optional<CacheFileCopy> dataFile = findAndCopyCacheFile(String.format("data_%1d",i), cachePath );
308  if (!dataFile.isPresent()) {
309  return;
310  }
311  }
312 
313  // find all f_* files in a single query and load them into the cache
314  findExternalFiles(cachePath);
315 
316  } catch (TskCoreException | IngestModuleException ex) {
317  String msg = "Failed to find cache files in path " + cachePath; //NON-NLS
318  logger.log(Level.WARNING, msg, ex);
319  return;
320  }
321 
322 
323  // parse the index file
324  logger.log(Level.INFO, "{0}- Now reading Cache index file from path {1}", new Object[]{moduleName, cachePath }); //NON-NLS
325 
326  List<AbstractFile> derivedFiles = new ArrayList<>();
327  Collection<BlackboardArtifact> sourceArtifacts = new ArrayList<>();
328  Collection<BlackboardArtifact> webCacheArtifacts = new ArrayList<>();
329 
330  ByteBuffer indexFileROBuffer = indexFileCopy.get().getByteBuffer();
331  IndexFileHeader indexHdr = new IndexFileHeader(indexFileROBuffer);
332 
333  // seek past the header
334  indexFileROBuffer.position(INDEXFILE_HDR_SIZE);
335 
336  // Process each address in the table
337  for (int i = 0; i < indexHdr.getTableLen(); i++) {
338 
339  if (context.dataSourceIngestIsCancelled()) {
340  cleanup();
341  return;
342  }
343 
344  CacheAddress addr = new CacheAddress(indexFileROBuffer.getInt() & UINT32_MASK, cachePath);
345  if (addr.isInitialized()) {
346  progressBar.progress( NbBundle.getMessage(this.getClass(),
347  "ChromeCacheExtractor.progressMsg",
348  moduleName, i, indexHdr.getTableLen(), cachePath) );
349  try {
350  List<DerivedFile> addedFiles = this.processCacheEntry(addr, sourceArtifacts, webCacheArtifacts);
351  derivedFiles.addAll(addedFiles);
352  }
353  catch (TskCoreException | IngestModuleException ex) {
354  logger.log(Level.WARNING, String.format("Failed to get cache entry at address %s", addr), ex); //NON-NLS
355  }
356  }
357  }
358 
359  if (context.dataSourceIngestIsCancelled()) {
360  cleanup();
361  return;
362  }
363 
364  progressBar.progress(String.format(Bundle.ChromeCacheExtract_adding_extracted_files_msg(), derivedFiles.size()));
365 
366  derivedFiles.forEach((derived) -> {
367  services.fireModuleContentEvent(new ModuleContentEvent(derived));
368  });
369 
370  context.addFilesToJob(derivedFiles);
371 
372  Blackboard blackboard = currentCase.getSleuthkitCase().getBlackboard();
373 
374  try {
375  blackboard.postArtifacts(sourceArtifacts, moduleName);
376  blackboard.postArtifacts(webCacheArtifacts, moduleName);
377  } catch (Blackboard.BlackboardException ex) {
378  logger.log(Level.WARNING, String.format("Failed to post cacheIndex artifacts "), ex); //NON-NLS
379  }
380 
381  cleanup();
382  }
383 
395  private List<DerivedFile> processCacheEntry(CacheAddress cacheEntryAddress, Collection<BlackboardArtifact> associatedObjectArtifacts, Collection<BlackboardArtifact> webCacheArtifacts ) throws TskCoreException, IngestModuleException {
396 
397  List<DerivedFile> derivedFiles = new ArrayList<>();
398 
399  // get the path to the corresponding data_X file
400  String dataFileName = cacheEntryAddress.getFilename();
401  String cachePath = cacheEntryAddress.getCachePath();
402 
403 
404  Optional<CacheFileCopy> cacheEntryFile = this.getCacheFileCopy(dataFileName, cachePath);
405  if (!cacheEntryFile.isPresent()) {
406  String msg = String.format("Failed to get cache entry at address %s", cacheEntryAddress); //NON-NLS
407  throw new IngestModuleException(msg);
408  }
409 
410 
411  // Get the cache entry and its data segments
412  CacheEntry cacheEntry = new CacheEntry(cacheEntryAddress, cacheEntryFile.get() );
413 
414  List<CacheData> dataEntries = cacheEntry.getData();
415  // Only process the first payload data segment in each entry
416  // first data segement has the HTTP headers, 2nd is the payload
417  if (dataEntries.size() < 2) {
418  return derivedFiles;
419  }
420  CacheData dataSegment = dataEntries.get(1);
421 
422 
423  // name of the file that was downloaded and cached (or data_X if it was saved into there)
424  String cachedFileName = dataSegment.getAddress().getFilename();
425  Optional<AbstractFile> cachedFileAbstractFile = this.findCacheFile(cachedFileName, cachePath);
426  if (!cachedFileAbstractFile.isPresent()) {
427  logger.log(Level.WARNING, "Error finding file: " + cachePath + "/" + cachedFileName); //NON-NLS
428  return derivedFiles;
429  }
430 
431  boolean isBrotliCompressed = false;
432  if (dataSegment.getType() != CacheDataTypeEnum.HTTP_HEADER && cacheEntry.isBrotliCompressed() ) {
433  isBrotliCompressed = true;
434  }
435 
436  // setup some attributes for later use
437  BlackboardAttribute urlAttr = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
438  moduleName,
439  ((cacheEntry.getKey() != null) ? cacheEntry.getKey() : ""));
440  BlackboardAttribute createTimeAttr = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED,
441  moduleName,
442  cacheEntry.getCreationTime());
443  BlackboardAttribute httpHeaderAttr = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_HEADERS,
444  moduleName,
445  cacheEntry.getHTTPHeaders());
446 
447  Collection<BlackboardAttribute> webCacheAttributes = new ArrayList<>();
448  webCacheAttributes.add(urlAttr);
449  webCacheAttributes.add(createTimeAttr);
450  webCacheAttributes.add(httpHeaderAttr);
451 
452 
453  // add artifacts to the f_XXX file
454  if (dataSegment.isInExternalFile() ) {
455  try {
456 
457  BlackboardArtifact webCacheArtifact = cacheEntryFile.get().getAbstractFile().newArtifact(ARTIFACT_TYPE.TSK_WEB_CACHE);
458  if (webCacheArtifact != null) {
459  webCacheArtifact.addAttributes(webCacheAttributes);
460 
461  // Add path of f_* file as attribute
462  webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH,
463  moduleName,
464  cachedFileAbstractFile.get().getUniquePath()));
465 
466  webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID,
467  moduleName, cachedFileAbstractFile.get().getId()));
468 
469  webCacheArtifacts.add(webCacheArtifact);
470 
471  BlackboardArtifact associatedObjectArtifact = cachedFileAbstractFile.get().newArtifact(ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT);
472  if (associatedObjectArtifact != null) {
473  associatedObjectArtifact.addAttribute(
474  new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT,
475  moduleName, webCacheArtifact.getArtifactID()));
476  associatedObjectArtifacts.add(associatedObjectArtifact);
477  }
478  }
479 
480  if (isBrotliCompressed) {
481  cachedFileAbstractFile.get().setMIMEType(BROTLI_MIMETYPE);
482  cachedFileAbstractFile.get().save();
483  }
484  } catch (TskException ex) {
485  logger.log(Level.SEVERE, "Error while trying to add an artifact", ex); //NON-NLS
486  }
487  }
488  // extract the embedded data to a derived file and create artifacts
489  else {
490 
491  // Data segments in "data_x" files are saved in individual files and added as derived files
492  String filename = dataSegment.save();
493  String relPathname = getRelOutputFolderName() + dataSegment.getAddress().getCachePath() + filename;
494  try {
495  DerivedFile derivedFile = fileManager.addDerivedFile(filename, relPathname,
496  dataSegment.getDataLength(),
497  cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), // TBD
498  true,
499  cachedFileAbstractFile.get(),
500  "",
501  moduleName,
502  VERSION_NUMBER,
503  "",
504  TskData.EncodingType.NONE);
505 
506 
507  BlackboardArtifact webCacheArtifact = cacheEntryFile.get().getAbstractFile().newArtifact(ARTIFACT_TYPE.TSK_WEB_CACHE);
508  if (webCacheArtifact != null) {
509  webCacheArtifact.addAttributes(webCacheAttributes);
510 
511  // Add path of derived file as attribute
512  webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH,
513  moduleName,
514  derivedFile.getUniquePath()));
515 
516  webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID,
517  moduleName, derivedFile.getId()));
518 
519  webCacheArtifacts.add(webCacheArtifact);
520 
521  BlackboardArtifact associatedObjectArtifact = derivedFile.newArtifact(ARTIFACT_TYPE.TSK_ASSOCIATED_OBJECT);
522  if (associatedObjectArtifact != null) {
523  associatedObjectArtifact.addAttribute(
524  new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT,
525  moduleName, webCacheArtifact.getArtifactID()));
526  associatedObjectArtifacts.add(associatedObjectArtifact);
527  }
528  }
529 
530  if (isBrotliCompressed) {
531  derivedFile.setMIMEType(BROTLI_MIMETYPE);
532  derivedFile.save();
533  }
534 
535  derivedFiles.add(derivedFile);
536  } catch (TskException ex) {
537  logger.log(Level.SEVERE, "Error while trying to add an artifact", ex); //NON-NLS
538  }
539  }
540 
541  return derivedFiles;
542  }
543 
552  private void findExternalFiles(String cachePath) throws TskCoreException {
553 
554  List<AbstractFile> effFiles = fileManager.findFiles(dataSource, "f_%", cachePath); //NON-NLS
555  for (AbstractFile abstractFile : effFiles ) {
556  this.externalFilesTable.put(cachePath + abstractFile.getName(), abstractFile);
557  }
558  }
567  private Optional<AbstractFile> findCacheFile(String cacheFileName, String cachePath) throws TskCoreException {
568 
569  // see if it is cached
570  String fileTableKey = cachePath + cacheFileName;
571  if (cacheFileName.startsWith("f_") && externalFilesTable.containsKey(fileTableKey)) {
572  return Optional.of(externalFilesTable.get(fileTableKey));
573  }
574  if (fileCopyCache.containsKey(fileTableKey)) {
575  return Optional.of(fileCopyCache.get(fileTableKey).getAbstractFile());
576  }
577 
578 
579  List<AbstractFile> cacheFiles = fileManager.findFiles(dataSource, cacheFileName, cachePath); //NON-NLS
580  if (!cacheFiles.isEmpty()) {
581  for (AbstractFile abstractFile: cacheFiles ) {
582  if (abstractFile.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)) {
583  return Optional.of(abstractFile);
584  }
585  }
586  return Optional.of(cacheFiles.get(0));
587  }
588 
589  return Optional.empty();
590  }
591 
598  private List<AbstractFile> findCacheIndexFiles() throws TskCoreException {
599  return fileManager.findFiles(dataSource, "index", DEFAULT_CACHE_PATH_STR); //NON-NLS
600  }
601 
602 
612  private Optional<CacheFileCopy> getCacheFileCopy(String cacheFileName, String cachePath) throws TskCoreException, IngestModuleException {
613 
614  // Check if the file is already in the cache
615  String fileTableKey = cachePath + cacheFileName;
616  if (fileCopyCache.containsKey(fileTableKey)) {
617  return Optional.of(fileCopyCache.get(fileTableKey));
618  }
619 
620  return findAndCopyCacheFile(cacheFileName, cachePath);
621  }
622 
630  private Optional<CacheFileCopy> findAndCopyCacheFile(String cacheFileName, String cachePath) throws TskCoreException, IngestModuleException {
631 
632  Optional<AbstractFile> cacheFileOptional = findCacheFile(cacheFileName, cachePath);
633  if (!cacheFileOptional.isPresent()) {
634  return Optional.empty();
635  }
636 
637 
638  // write the file to disk so that we can have a memory-mapped ByteBuffer
639  // @@@ NOTE: I"m not sure this is needed. These files are small enough and we could probably just load them into
640  // a byte[] for ByteBuffer.
641  AbstractFile cacheFile = cacheFileOptional.get();
642  RandomAccessFile randomAccessFile = null;
643  String tempFilePathname = RAImageIngestModule.getRATempPath(currentCase, moduleName) + cachePath + cacheFile.getName(); //NON-NLS
644  try {
645  File newFile = new File(tempFilePathname);
646  ContentUtils.writeToFile(cacheFile, newFile, context::dataSourceIngestIsCancelled);
647 
648  randomAccessFile = new RandomAccessFile(tempFilePathname, "r");
649  FileChannel roChannel = randomAccessFile.getChannel();
650  ByteBuffer cacheFileROBuf = roChannel.map(FileChannel.MapMode.READ_ONLY, 0,
651  (int) roChannel.size());
652 
653  cacheFileROBuf.order(ByteOrder.nativeOrder());
654  CacheFileCopy cacheFileCopy = new CacheFileCopy(cacheFile, randomAccessFile, cacheFileROBuf );
655 
656  if (!cacheFileName.startsWith("f_")) {
657  fileCopyCache.put(cachePath + cacheFileName, cacheFileCopy);
658  }
659 
660  return Optional.of(cacheFileCopy);
661  }
662  catch (IOException ex) {
663 
664  try {
665  if (randomAccessFile != null) {
666  randomAccessFile.close();
667  }
668  }
669  catch (IOException ex2) {
670  logger.log(Level.SEVERE, "Error while trying to close temp file after exception.", ex2); //NON-NLS
671  }
672  String msg = String.format("Error reading/copying Chrome cache file '%s' (id=%d).", //NON-NLS
673  cacheFile.getName(), cacheFile.getId());
674  throw new IngestModuleException(msg, ex);
675  }
676  }
677 
681  final class IndexFileHeader {
682 
683  private final long magic;
684  private final int version;
685  private final int numEntries;
686  private final int numBytes;
687  private final int lastFile;
688  private final int tableLen;
689 
690  IndexFileHeader(ByteBuffer indexFileROBuf) {
691 
692  magic = indexFileROBuf.getInt() & UINT32_MASK;
693 
694  indexFileROBuf.position(indexFileROBuf.position()+2);
695 
696  version = indexFileROBuf.getShort();
697  numEntries = indexFileROBuf.getInt();
698  numBytes = indexFileROBuf.getInt();
699  lastFile = indexFileROBuf.getInt();
700 
701  indexFileROBuf.position(indexFileROBuf.position()+4); // this_id
702  indexFileROBuf.position(indexFileROBuf.position()+4); // stats cache address
703 
704  tableLen = indexFileROBuf.getInt();
705  }
706 
707  public long getMagic() {
708  return magic;
709  }
710 
711  public int getVersion() {
712  return version;
713  }
714 
715  public int getNumEntries() {
716  return numEntries;
717  }
718 
719  public int getNumBytes() {
720  return numBytes;
721  }
722 
723  public int getLastFile() {
724  return lastFile;
725  }
726 
727  public int getTableLen() {
728  return tableLen;
729  }
730 
731  @Override
732  public String toString() {
733  StringBuilder sb = new StringBuilder();
734 
735  sb.append(String.format("Index Header:"))
736  .append(String.format("\tMagic = %x" , getMagic()) )
737  .append(String.format("\tVersion = %x" , getVersion()) )
738  .append(String.format("\tNumEntries = %x" , getNumEntries()) )
739  .append(String.format("\tNumBytes = %x" , getNumBytes()) )
740  .append(String.format("\tLastFile = %x" , getLastFile()) )
741  .append(String.format("\tTableLen = %x" , getTableLen()) );
742 
743  return sb.toString();
744  }
745  }
746 
750  enum CacheFileTypeEnum {
751  EXTERNAL,
752  RANKINGS,
753  BLOCK_256,
754  BLOCK_1K,
755  BLOCK_4K,
756  BLOCK_FILES,
757  BLOCK_ENTRIES,
758  BLOCK_EVICTED
759  }
760 
761 
762 
782  final class CacheAddress {
783  // sundry constants to parse the bit fields in address
784  private static final long ADDR_INITIALIZED_MASK = 0x80000000l;
785  private static final long FILE_TYPE_MASK = 0x70000000;
786  private static final long FILE_TYPE_OFFSET = 28;
787  private static final long NUM_BLOCKS_MASK = 0x03000000;
788  private static final long NUM_BLOCKS_OFFSET = 24;
789  private static final long FILE_SELECTOR_MASK = 0x00ff0000;
790  private static final long FILE_SELECTOR_OFFSET = 16;
791  private static final long START_BLOCK_MASK = 0x0000FFFF;
792  private static final long EXTERNAL_FILE_NAME_MASK = 0x0FFFFFFF;
793 
794  private final long uint32CacheAddr;
795  private final CacheFileTypeEnum fileType;
796  private final int numBlocks;
797  private final int startBlock;
798  private final String fileName;
799  private final int fileNumber;
800 
801  private final String cachePath;
802 
803 
804  CacheAddress(long uint32, String cachePath) {
805 
806  uint32CacheAddr = uint32;
807  this.cachePath = cachePath;
808 
809  int fileTypeEnc = (int)(uint32CacheAddr & FILE_TYPE_MASK) >> FILE_TYPE_OFFSET;
810  fileType = CacheFileTypeEnum.values()[fileTypeEnc];
811 
812  if (isInitialized()) {
813  if (isInExternalFile()) {
814  fileNumber = (int)(uint32CacheAddr & EXTERNAL_FILE_NAME_MASK);
815  fileName = String.format("f_%06x", getFileNumber() );
816  numBlocks = 0;
817  startBlock = 0;
818  } else {
819  fileNumber = (int)((uint32CacheAddr & FILE_SELECTOR_MASK) >> FILE_SELECTOR_OFFSET);
820  fileName = String.format("data_%d", getFileNumber() );
821  numBlocks = (int)(uint32CacheAddr & NUM_BLOCKS_MASK >> NUM_BLOCKS_OFFSET);
822  startBlock = (int)(uint32CacheAddr & START_BLOCK_MASK);
823  }
824  }
825  else {
826  fileName = null;
827  fileNumber = 0;
828  numBlocks = 0;
829  startBlock = 0;
830  }
831  }
832 
833  boolean isInitialized() {
834  return ((uint32CacheAddr & ADDR_INITIALIZED_MASK) != 0);
835  }
836 
837  CacheFileTypeEnum getFileType() {
838  return fileType;
839  }
840 
845  String getFilename() {
846  return fileName;
847  }
848 
849  String getCachePath() {
850  return cachePath;
851  }
852 
853  boolean isInExternalFile() {
854  return (fileType == CacheFileTypeEnum.EXTERNAL);
855  }
856 
857  int getFileNumber() {
858  return fileNumber;
859  }
860 
861  int getStartBlock() {
862  return startBlock;
863  }
864 
865  int getNumBlocks() {
866  return numBlocks;
867  }
868 
869  int getBlockSize() {
870  switch (fileType) {
871  case RANKINGS:
872  return 36;
873  case BLOCK_256:
874  return 256;
875  case BLOCK_1K:
876  return 1024;
877  case BLOCK_4K:
878  return 4096;
879  case BLOCK_FILES:
880  return 8;
881  case BLOCK_ENTRIES:
882  return 104;
883  case BLOCK_EVICTED:
884  return 48;
885  default:
886  return 0;
887  }
888  }
889 
890  public long getUint32CacheAddr() {
891  return uint32CacheAddr;
892  }
893 
894  @Override
895  public String toString() {
896  StringBuilder sb = new StringBuilder();
897  sb.append(String.format("CacheAddr %08x : %s : filename %s",
898  uint32CacheAddr,
899  isInitialized() ? "Initialized" : "UnInitialized",
900  getFilename()));
901 
902  if ((fileType == CacheFileTypeEnum.BLOCK_256) ||
903  (fileType == CacheFileTypeEnum.BLOCK_1K) ||
904  (fileType == CacheFileTypeEnum.BLOCK_4K) ) {
905  sb.append(String.format(" (%d blocks starting at %08X)",
906  this.getNumBlocks(),
907  this.getStartBlock()
908  ));
909  }
910 
911  return sb.toString();
912  }
913 
914  }
915 
919  enum CacheDataTypeEnum {
920  HTTP_HEADER,
921  UNKNOWN,
922  };
923 
933  final class CacheData {
934 
935  private int length;
936  private final CacheAddress address;
937  private CacheDataTypeEnum type;
938 
939  private boolean isHTTPHeaderHint;
940 
941  private CacheFileCopy cacheFileCopy = null;
942  private byte[] data = null;
943 
944  private String httpResponse;
945  private final Map<String, String> httpHeaders = new HashMap<>();
946 
947  CacheData(CacheAddress cacheAdress, int len) {
948  this(cacheAdress, len, false);
949  }
950 
951  CacheData(CacheAddress cacheAdress, int len, boolean isHTTPHeader ) {
952  this.type = CacheDataTypeEnum.UNKNOWN;
953  this.length = len;
954  this.address = cacheAdress;
955  this.isHTTPHeaderHint = isHTTPHeader;
956  }
957 
958  boolean isInExternalFile() {
959  return address.isInExternalFile();
960  }
961 
962  boolean hasHTTPHeaders() {
963  return this.type == CacheDataTypeEnum.HTTP_HEADER;
964  }
965 
966  String getHTTPHeader(String key) {
967  return this.httpHeaders.get(key);
968  }
969 
975  String getHTTPHeaders() {
976  if (!hasHTTPHeaders()) {
977  return "";
978  }
979 
980  StringBuilder sb = new StringBuilder();
981  httpHeaders.entrySet().forEach((entry) -> {
982  if (sb.length() > 0) {
983  sb.append(" \n");
984  }
985  sb.append(String.format("%s : %s",
986  entry.getKey(), entry.getValue()));
987  });
988 
989  return sb.toString();
990  }
991 
992  String getHTTPRespone() {
993  return httpResponse;
994  }
995 
1001  void extract() throws TskCoreException, IngestModuleException {
1002 
1003  // do nothing if already extracted,
1004  if (data != null) {
1005  return;
1006  }
1007 
1008  // Don't extract data from external files.
1009  if (!address.isInExternalFile() ) {
1010 
1011  cacheFileCopy = getCacheFileCopy(address.getFilename(), address.getCachePath()).get();
1012 
1013  this.data = new byte [length];
1014  ByteBuffer buf = cacheFileCopy.getByteBuffer();
1015  int dataOffset = DATAFILE_HDR_SIZE + address.getStartBlock() * address.getBlockSize();
1016  buf.position(dataOffset);
1017  buf.get(data, 0, length);
1018 
1019  // if this might be a HTPP header, lets try to parse it as such
1020  if ((isHTTPHeaderHint)) {
1021  String strData = new String(data);
1022  if (strData.contains("HTTP")) {
1023 
1024  // Http headers if present, are usually in frst data segment in an entry
1025  // General Parsing algo:
1026  // - Find start of HTTP header by searching for string "HTTP"
1027  // - Skip to the first 0x00 to get to the end of the HTTP response line, this makrs start of headers section
1028  // - Find the end of the header by searching for 0x00 0x00 bytes
1029  // - Extract the headers section
1030  // - Parse the headers section - each null terminated string is a header
1031  // - Each header is of the format "name: value" e.g.
1032 
1033  type = CacheDataTypeEnum.HTTP_HEADER;
1034 
1035  int startOff = strData.indexOf("HTTP");
1036  Charset charset = Charset.forName("UTF-8");
1037  boolean done = false;
1038  int i = startOff;
1039  int hdrNum = 1;
1040 
1041  while (!done) {
1042  // each header is null terminated
1043  int start = i;
1044  while (i < data.length && data[i] != 0) {
1045  i++;
1046  }
1047 
1048  // http headers are terminated by 0x00 0x00
1049  if (i == data.length || data[i+1] == 0) {
1050  done = true;
1051  }
1052 
1053  int len = (i - start);
1054  String headerLine = new String(data, start, len, charset);
1055 
1056  // first line is the http response
1057  if (hdrNum == 1) {
1058  httpResponse = headerLine;
1059  } else {
1060  int nPos = headerLine.indexOf(':');
1061  if (nPos > 0 ) {
1062  String key = headerLine.substring(0, nPos);
1063  String val= headerLine.substring(nPos+1);
1064  httpHeaders.put(key.toLowerCase(), val);
1065  }
1066  }
1067 
1068  i++;
1069  hdrNum++;
1070  }
1071  }
1072  }
1073  }
1074  }
1075 
1076  String getDataString() throws TskCoreException, IngestModuleException {
1077  if (data == null) {
1078  extract();
1079  }
1080  return new String(data);
1081  }
1082 
1083  byte[] getDataBytes() throws TskCoreException, IngestModuleException {
1084  if (data == null) {
1085  extract();
1086  }
1087  return data.clone();
1088  }
1089 
1090  int getDataLength() {
1091  return this.length;
1092  }
1093 
1094  CacheDataTypeEnum getType() {
1095  return type;
1096  }
1097 
1098  CacheAddress getAddress() {
1099  return address;
1100  }
1101 
1102 
1111  String save() throws TskCoreException, IngestModuleException {
1112  String fileName;
1113 
1114  if (address.isInExternalFile()) {
1115  fileName = address.getFilename();
1116  } else {
1117  fileName = String.format("%s__%08x", address.getFilename(), address.getUint32CacheAddr());
1118  }
1119 
1120  String filePathName = getAbsOutputFolderName() + address.getCachePath() + fileName;
1121  save(filePathName);
1122 
1123  return fileName;
1124  }
1125 
1135  void save(String filePathName) throws TskCoreException, IngestModuleException {
1136 
1137  // Save the data to specified file
1138  if (data == null) {
1139  extract();
1140  }
1141 
1142  // Data in external files is not saved in local files
1143  if (!this.isInExternalFile()) {
1144  // write the
1145  try (FileOutputStream stream = new FileOutputStream(filePathName)) {
1146  stream.write(data);
1147  } catch (IOException ex) {
1148  throw new TskCoreException(String.format("Failed to write output file %s", filePathName), ex);
1149  }
1150  }
1151  }
1152 
1153  @Override
1154  public String toString() {
1155  StringBuilder strBuilder = new StringBuilder();
1156  strBuilder.append(String.format("\t\tData type = : %s, Data Len = %d ",
1157  this.type.toString(), this.length ));
1158 
1159  if (hasHTTPHeaders()) {
1160  String str = getHTTPHeader("content-encoding");
1161  if (str != null) {
1162  strBuilder.append(String.format("\t%s=%s", "content-encoding", str ));
1163  }
1164  }
1165 
1166  return strBuilder.toString();
1167  }
1168 
1169  }
1170 
1171 
1175  enum EntryStateEnum {
1176  ENTRY_NORMAL,
1177  ENTRY_EVICTED,
1178  ENTRY_DOOMED
1179  };
1180 
1181 
1182 // Main structure for an entry on the backing storage.
1183 //
1184 // Each entry has a key, identifying the URL the cache entry pertains to.
1185 // If the key is longer than
1186 // what can be stored on this structure, it will be extended on consecutive
1187 // blocks (adding 256 bytes each time), up to 4 blocks (1024 - 32 - 1 chars).
1188 // After that point, the whole key will be stored as a data block or external
1189 // file.
1190 //
1191 // Each entry can have upto 4 data segments
1192 //
1193 // struct EntryStore {
1194 // uint32 hash; // Full hash of the key.
1195 // CacheAddr next; // Next entry with the same hash or bucket.
1196 // CacheAddr rankings_node; // Rankings node for this entry.
1197 // int32 reuse_count; // How often is this entry used.
1198 // int32 refetch_count; // How often is this fetched from the net.
1199 // int32 state; // Current state.
1200 // uint64 creation_time;
1201 // int32 key_len;
1202 // CacheAddr long_key; // Optional address of a long key.
1203 // int32 data_size[4]; // We can store up to 4 data streams for each
1204 // CacheAddr data_addr[4]; // entry.
1205 // uint32 flags; // Any combination of EntryFlags.
1206 // int32 pad[4];
1207 // uint32 self_hash; // The hash of EntryStore up to this point.
1208 // char key[256 - 24 * 4]; // null terminated
1209 // };
1210 
1214  final class CacheEntry {
1215 
1216  // each entry is 256 bytes. The last section of the entry, after all the other fields is a null terminated key
1217  private static final int MAX_KEY_LEN = 256-24*4;
1218 
1219  private final CacheAddress selfAddress;
1220  private final CacheFileCopy cacheFileCopy;
1221 
1222  private final long hash;
1223  private final CacheAddress nextAddress;
1224  private final CacheAddress rankingsNodeAddress;
1225 
1226  private final int reuseCount;
1227  private final int refetchCount;
1228  private final EntryStateEnum state;
1229 
1230  private final long creationTime;
1231  private final int keyLen;
1232 
1233  private final CacheAddress longKeyAddresses; // address of the key, if the key is external to the entry
1234 
1235  private final int dataSizes[];
1236  private final CacheAddress dataAddresses[];
1237  private List<CacheData> dataList;
1238 
1239  private final long flags;
1240 
1241  private String key; // Key may be found within the entry or may be external
1242 
1243  CacheEntry(CacheAddress cacheAdress, CacheFileCopy cacheFileCopy ) {
1244  this.selfAddress = cacheAdress;
1245  this.cacheFileCopy = cacheFileCopy;
1246 
1247  ByteBuffer fileROBuf = cacheFileCopy.getByteBuffer();
1248 
1249  int entryOffset = DATAFILE_HDR_SIZE + cacheAdress.getStartBlock() * cacheAdress.getBlockSize();
1250 
1251  // reposition the buffer to the the correct offset
1252  fileROBuf.position(entryOffset);
1253 
1254  hash = fileROBuf.getInt() & UINT32_MASK;
1255 
1256  long uint32 = fileROBuf.getInt() & UINT32_MASK;
1257  nextAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1258 
1259  uint32 = fileROBuf.getInt() & UINT32_MASK;
1260  rankingsNodeAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1261 
1262  reuseCount = fileROBuf.getInt();
1263  refetchCount = fileROBuf.getInt();
1264 
1265  state = EntryStateEnum.values()[fileROBuf.getInt()];
1266  creationTime = (fileROBuf.getLong() / 1000000) - Long.valueOf("11644473600");
1267 
1268  keyLen = fileROBuf.getInt();
1269 
1270  uint32 = fileROBuf.getInt() & UINT32_MASK;
1271  longKeyAddresses = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1272 
1273  dataList = null;
1274  dataSizes= new int[4];
1275  for (int i = 0; i < 4; i++) {
1276  dataSizes[i] = fileROBuf.getInt();
1277  }
1278  dataAddresses = new CacheAddress[4];
1279  for (int i = 0; i < 4; i++) {
1280  dataAddresses[i] = new CacheAddress(fileROBuf.getInt() & UINT32_MASK, selfAddress.getCachePath());
1281  }
1282 
1283  flags = fileROBuf.getInt() & UINT32_MASK;
1284  // skip over pad
1285  for (int i = 0; i < 4; i++) {
1286  fileROBuf.getInt();
1287  }
1288 
1289  // skip over self hash
1290  fileROBuf.getInt();
1291 
1292  // get the key
1293  if (longKeyAddresses != null) {
1294  // Key is stored outside of the entry
1295  try {
1296  CacheData data = new CacheData(longKeyAddresses, this.keyLen, true);
1297  key = data.getDataString();
1298  } catch (TskCoreException | IngestModuleException ex) {
1299  logger.log(Level.WARNING, String.format("Failed to get external key from address %s", longKeyAddresses)); //NON-NLS
1300  }
1301  }
1302  else { // key stored within entry
1303  StringBuilder strBuilder = new StringBuilder(MAX_KEY_LEN);
1304  int keyLen = 0;
1305  while (fileROBuf.remaining() > 0 && keyLen < MAX_KEY_LEN) {
1306  char keyChar = (char)fileROBuf.get();
1307  if (keyChar == '\0') {
1308  break;
1309  }
1310  strBuilder.append(keyChar);
1311  keyLen++;
1312  }
1313 
1314  key = strBuilder.toString();
1315  }
1316  }
1317 
1318  public CacheAddress getAddress() {
1319  return selfAddress;
1320  }
1321 
1322  public long getHash() {
1323  return hash;
1324  }
1325 
1326  public CacheAddress getNextAddress() {
1327  return nextAddress;
1328  }
1329 
1330  public int getReuseCount() {
1331  return reuseCount;
1332  }
1333 
1334  public int getRefetchCount() {
1335  return refetchCount;
1336  }
1337 
1338  public EntryStateEnum getState() {
1339  return state;
1340  }
1341 
1342  public long getCreationTime() {
1343  return creationTime;
1344  }
1345 
1346  public long getFlags() {
1347  return flags;
1348  }
1349 
1350  public String getKey() {
1351  return key;
1352  }
1353 
1362  public List<CacheData> getData() throws TskCoreException, IngestModuleException {
1363 
1364  if (dataList == null) {
1365  dataList = new ArrayList<>();
1366  for (int i = 0; i < 4; i++) {
1367  if (dataSizes[i] > 0) {
1368  CacheData cacheData = new CacheData(dataAddresses[i], dataSizes[i], true );
1369 
1370  cacheData.extract();
1371  dataList.add(cacheData);
1372  }
1373  }
1374  }
1375  return dataList;
1376  }
1377 
1385  boolean hasHTTPHeaders() {
1386  if ((dataList == null) || dataList.isEmpty()) {
1387  return false;
1388  }
1389  return dataList.get(0).hasHTTPHeaders();
1390  }
1391 
1398  String getHTTPHeader(String key) {
1399  if ((dataList == null) || dataList.isEmpty()) {
1400  return null;
1401  }
1402  // First data segment has the HTTP headers, if any
1403  return dataList.get(0).getHTTPHeader(key);
1404  }
1405 
1411  String getHTTPHeaders() {
1412  if ((dataList == null) || dataList.isEmpty()) {
1413  return null;
1414  }
1415  // First data segment has the HTTP headers, if any
1416  return dataList.get(0).getHTTPHeaders();
1417  }
1418 
1427  boolean isBrotliCompressed() {
1428 
1429  if (hasHTTPHeaders() ) {
1430  String encodingHeader = getHTTPHeader("content-encoding");
1431  if (encodingHeader!= null) {
1432  return encodingHeader.trim().equalsIgnoreCase("br");
1433  }
1434  }
1435 
1436  return false;
1437  }
1438 
1439  @Override
1440  public String toString() {
1441  StringBuilder sb = new StringBuilder();
1442  sb.append(String.format("Entry = Hash: %08x, State: %s, ReuseCount: %d, RefetchCount: %d",
1443  this.hash, this.state.toString(), this.reuseCount, this.refetchCount ))
1444  .append(String.format("\n\tKey: %s, Keylen: %d",
1445  this.key, this.keyLen, this.reuseCount, this.refetchCount ))
1446  .append(String.format("\n\tCreationTime: %s",
1447  TimeUtilities.epochToTime(this.creationTime) ))
1448  .append(String.format("\n\tNext Address: %s",
1449  (nextAddress != null) ? nextAddress.toString() : "None"));
1450 
1451  for (int i = 0; i < 4; i++) {
1452  if (dataSizes[i] > 0) {
1453  sb.append(String.format("\n\tData %d: cache address = %s, Data = %s",
1454  i, dataAddresses[i].toString(),
1455  (dataList != null)
1456  ? dataList.get(i).toString()
1457  : "Data not retrived yet."));
1458  }
1459  }
1460 
1461  return sb.toString();
1462  }
1463  }
1464 }

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