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

Copyright © 2012-2018 Basis Technology. Generated on: Wed Sep 18 2019
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.