Autopsy  4.11.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;
53 import org.sleuthkit.datamodel.AbstractFile;
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.SEVERE, 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.SEVERE, 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.SEVERE, 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.SEVERE, 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  services.fireModuleDataEvent(new ModuleDataEvent(moduleName, BlackboardArtifact.ARTIFACT_TYPE.TSK_DOWNLOAD_SOURCE, !sourceArtifacts.isEmpty() ? sourceArtifacts : null));
366  services.fireModuleDataEvent(new ModuleDataEvent(moduleName, BlackboardArtifact.ARTIFACT_TYPE.TSK_WEB_CACHE, !webCacheArtifacts.isEmpty() ? webCacheArtifacts : null));
367 
368  cleanup();
369  }
370 
382  private List<DerivedFile> processCacheEntry(CacheAddress cacheEntryAddress, Collection<BlackboardArtifact> sourceArtifacts, Collection<BlackboardArtifact> webCacheArtifacts ) throws TskCoreException, IngestModuleException {
383 
384  List<DerivedFile> derivedFiles = new ArrayList<>();
385 
386  // get the path to the corresponding data_X file
387  String dataFileName = cacheEntryAddress.getFilename();
388  String cachePath = cacheEntryAddress.getCachePath();
389 
390 
391  Optional<CacheFileCopy> cacheEntryFile = this.getCacheFileCopy(dataFileName, cachePath);
392  if (!cacheEntryFile.isPresent()) {
393  String msg = String.format("Failed to get cache entry at address %s", cacheEntryAddress); //NON-NLS
394  throw new IngestModuleException(msg);
395  }
396 
397 
398  // Get the cache entry and its data segments
399  CacheEntry cacheEntry = new CacheEntry(cacheEntryAddress, cacheEntryFile.get() );
400 
401  List<CacheData> dataEntries = cacheEntry.getData();
402  // Only process the first payload data segment in each entry
403  // first data segement has the HTTP headers, 2nd is the payload
404  if (dataEntries.size() < 2) {
405  return derivedFiles;
406  }
407  CacheData dataSegment = dataEntries.get(1);
408 
409 
410  // name of the file that was downloaded and cached (or data_X if it was saved into there)
411  String cachedFileName = dataSegment.getAddress().getFilename();
412  Optional<AbstractFile> cachedFileAbstractFile = this.findCacheFile(cachedFileName, cachePath);
413  if (!cachedFileAbstractFile.isPresent()) {
414  logger.log(Level.SEVERE, "Error finding file: " + cachePath + "/" + cachedFileName); //NON-NLS
415  return derivedFiles;
416  }
417 
418  boolean isBrotliCompressed = false;
419  if (dataSegment.getType() != CacheDataTypeEnum.HTTP_HEADER && cacheEntry.isBrotliCompressed() ) {
420  isBrotliCompressed = true;
421  }
422 
423  // setup some attributes for later use
424  BlackboardAttribute urlAttr = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL,
425  moduleName,
426  ((cacheEntry.getKey() != null) ? cacheEntry.getKey() : ""));
427  BlackboardAttribute createTimeAttr = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_CREATED,
428  moduleName,
429  cacheEntry.getCreationTime());
430  BlackboardAttribute httpHeaderAttr = new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_HEADERS,
431  moduleName,
432  cacheEntry.getHTTPHeaders());
433 
434  Collection<BlackboardAttribute> sourceArtifactAttributes = new ArrayList<>();
435  sourceArtifactAttributes.add(urlAttr);
436  sourceArtifactAttributes.add(createTimeAttr);
437 
438  Collection<BlackboardAttribute> webCacheAttributes = new ArrayList<>();
439  webCacheAttributes.add(urlAttr);
440  webCacheAttributes.add(createTimeAttr);
441  webCacheAttributes.add(httpHeaderAttr);
442 
443 
444  // add artifacts to the f_XXX file
445  if (dataSegment.isInExternalFile() ) {
446  try {
447  BlackboardArtifact sourceArtifact = cachedFileAbstractFile.get().newArtifact(ARTIFACT_TYPE.TSK_DOWNLOAD_SOURCE);
448  if (sourceArtifact != null) {
449  sourceArtifact.addAttributes(sourceArtifactAttributes);
450  sourceArtifacts.add(sourceArtifact);
451  }
452 
453  BlackboardArtifact webCacheArtifact = cacheEntryFile.get().getAbstractFile().newArtifact(ARTIFACT_TYPE.TSK_WEB_CACHE);
454  if (webCacheArtifact != null) {
455  webCacheArtifact.addAttributes(webCacheAttributes);
456 
457  // Add path of f_* file as attribute
458  webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH,
459  moduleName,
460  cachedFileAbstractFile.get().getUniquePath()));
461 
462  webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID,
463  moduleName, cachedFileAbstractFile.get().getId()));
464 
465  webCacheArtifacts.add(webCacheArtifact);
466  }
467 
468  if (isBrotliCompressed) {
469  cachedFileAbstractFile.get().setMIMEType(BROTLI_MIMETYPE);
470  cachedFileAbstractFile.get().save();
471  }
472  } catch (TskException ex) {
473  logger.log(Level.SEVERE, "Error while trying to add an artifact", ex); //NON-NLS
474  }
475  }
476  // extract the embedded data to a derived file and create artifacts
477  else {
478 
479  // Data segments in "data_x" files are saved in individual files and added as derived files
480  String filename = dataSegment.save();
481  String relPathname = getRelOutputFolderName() + dataSegment.getAddress().getCachePath() + filename;
482  try {
483  DerivedFile derivedFile = fileManager.addDerivedFile(filename, relPathname,
484  dataSegment.getDataLength(),
485  cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), cacheEntry.getCreationTime(), // TBD
486  true,
487  cachedFileAbstractFile.get(),
488  "",
489  moduleName,
490  VERSION_NUMBER,
491  "",
492  TskData.EncodingType.NONE);
493 
494  BlackboardArtifact sourceArtifact = derivedFile.newArtifact(ARTIFACT_TYPE.TSK_DOWNLOAD_SOURCE);
495  if (sourceArtifact != null) {
496  sourceArtifact.addAttributes(sourceArtifactAttributes);
497  sourceArtifacts.add(sourceArtifact);
498  }
499 
500  BlackboardArtifact webCacheArtifact = cacheEntryFile.get().getAbstractFile().newArtifact(ARTIFACT_TYPE.TSK_WEB_CACHE);
501  if (webCacheArtifact != null) {
502  webCacheArtifact.addAttributes(webCacheAttributes);
503 
504  // Add path of derived file as attribute
505  webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH,
506  moduleName,
507  derivedFile.getUniquePath()));
508 
509  webCacheArtifact.addAttribute(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PATH_ID,
510  moduleName, derivedFile.getId()));
511 
512  webCacheArtifacts.add(webCacheArtifact);
513  }
514 
515  if (isBrotliCompressed) {
516  derivedFile.setMIMEType(BROTLI_MIMETYPE);
517  derivedFile.save();
518  }
519 
520  derivedFiles.add(derivedFile);
521  } catch (TskException ex) {
522  logger.log(Level.SEVERE, "Error while trying to add an artifact", ex); //NON-NLS
523  }
524  }
525 
526  return derivedFiles;
527  }
528 
537  private void findExternalFiles(String cachePath) throws TskCoreException {
538 
539  List<AbstractFile> effFiles = fileManager.findFiles(dataSource, "f_%", cachePath); //NON-NLS
540  for (AbstractFile abstractFile : effFiles ) {
541  this.externalFilesTable.put(cachePath + abstractFile.getName(), abstractFile);
542  }
543  }
552  private Optional<AbstractFile> findCacheFile(String cacheFileName, String cachePath) throws TskCoreException {
553 
554  // see if it is cached
555  String fileTableKey = cachePath + cacheFileName;
556  if (cacheFileName.startsWith("f_") && externalFilesTable.containsKey(fileTableKey)) {
557  return Optional.of(externalFilesTable.get(fileTableKey));
558  }
559  if (fileCopyCache.containsKey(fileTableKey)) {
560  return Optional.of(fileCopyCache.get(fileTableKey).getAbstractFile());
561  }
562 
563 
564  List<AbstractFile> cacheFiles = fileManager.findFiles(dataSource, cacheFileName, cachePath); //NON-NLS
565  if (!cacheFiles.isEmpty()) {
566  for (AbstractFile abstractFile: cacheFiles ) {
567  if (abstractFile.getUniquePath().trim().endsWith(DEFAULT_CACHE_PATH_STR)) {
568  return Optional.of(abstractFile);
569  }
570  }
571  return Optional.of(cacheFiles.get(0));
572  }
573 
574  return Optional.empty();
575  }
576 
583  private List<AbstractFile> findCacheIndexFiles() throws TskCoreException {
584  return fileManager.findFiles(dataSource, "index", DEFAULT_CACHE_PATH_STR); //NON-NLS
585  }
586 
587 
597  private Optional<CacheFileCopy> getCacheFileCopy(String cacheFileName, String cachePath) throws TskCoreException, IngestModuleException {
598 
599  // Check if the file is already in the cache
600  String fileTableKey = cachePath + cacheFileName;
601  if (fileCopyCache.containsKey(fileTableKey)) {
602  return Optional.of(fileCopyCache.get(fileTableKey));
603  }
604 
605  return findAndCopyCacheFile(cacheFileName, cachePath);
606  }
607 
615  private Optional<CacheFileCopy> findAndCopyCacheFile(String cacheFileName, String cachePath) throws TskCoreException, IngestModuleException {
616 
617  Optional<AbstractFile> cacheFileOptional = findCacheFile(cacheFileName, cachePath);
618  if (!cacheFileOptional.isPresent()) {
619  return Optional.empty();
620  }
621 
622 
623  // write the file to disk so that we can have a memory-mapped ByteBuffer
624  // @@@ NOTE: I"m not sure this is needed. These files are small enough and we could probably just load them into
625  // a byte[] for ByteBuffer.
626  AbstractFile cacheFile = cacheFileOptional.get();
627  RandomAccessFile randomAccessFile = null;
628  String tempFilePathname = RAImageIngestModule.getRATempPath(currentCase, moduleName) + cachePath + cacheFile.getName(); //NON-NLS
629  try {
630  File newFile = new File(tempFilePathname);
631  ContentUtils.writeToFile(cacheFile, newFile, context::dataSourceIngestIsCancelled);
632 
633  randomAccessFile = new RandomAccessFile(tempFilePathname, "r");
634  FileChannel roChannel = randomAccessFile.getChannel();
635  ByteBuffer cacheFileROBuf = roChannel.map(FileChannel.MapMode.READ_ONLY, 0,
636  (int) roChannel.size());
637 
638  cacheFileROBuf.order(ByteOrder.nativeOrder());
639  CacheFileCopy cacheFileCopy = new CacheFileCopy(cacheFile, randomAccessFile, cacheFileROBuf );
640 
641  if (!cacheFileName.startsWith("f_")) {
642  fileCopyCache.put(cachePath + cacheFileName, cacheFileCopy);
643  }
644 
645  return Optional.of(cacheFileCopy);
646  }
647  catch (IOException ex) {
648 
649  try {
650  if (randomAccessFile != null) {
651  randomAccessFile.close();
652  }
653  }
654  catch (IOException ex2) {
655  logger.log(Level.SEVERE, "Error while trying to close temp file after exception.", ex2); //NON-NLS
656  }
657  String msg = String.format("Error reading/copying Chrome cache file '%s' (id=%d).", //NON-NLS
658  cacheFile.getName(), cacheFile.getId());
659  throw new IngestModuleException(msg, ex);
660  }
661  }
662 
666  final class IndexFileHeader {
667 
668  private final long magic;
669  private final int version;
670  private final int numEntries;
671  private final int numBytes;
672  private final int lastFile;
673  private final int tableLen;
674 
675  IndexFileHeader(ByteBuffer indexFileROBuf) {
676 
677  magic = indexFileROBuf.getInt() & UINT32_MASK;
678 
679  indexFileROBuf.position(indexFileROBuf.position()+2);
680 
681  version = indexFileROBuf.getShort();
682  numEntries = indexFileROBuf.getInt();
683  numBytes = indexFileROBuf.getInt();
684  lastFile = indexFileROBuf.getInt();
685 
686  indexFileROBuf.position(indexFileROBuf.position()+4); // this_id
687  indexFileROBuf.position(indexFileROBuf.position()+4); // stats cache address
688 
689  tableLen = indexFileROBuf.getInt();
690  }
691 
692  public long getMagic() {
693  return magic;
694  }
695 
696  public int getVersion() {
697  return version;
698  }
699 
700  public int getNumEntries() {
701  return numEntries;
702  }
703 
704  public int getNumBytes() {
705  return numBytes;
706  }
707 
708  public int getLastFile() {
709  return lastFile;
710  }
711 
712  public int getTableLen() {
713  return tableLen;
714  }
715 
716  @Override
717  public String toString() {
718  StringBuilder sb = new StringBuilder();
719 
720  sb.append(String.format("Index Header:"))
721  .append(String.format("\tMagic = %x" , getMagic()) )
722  .append(String.format("\tVersion = %x" , getVersion()) )
723  .append(String.format("\tNumEntries = %x" , getNumEntries()) )
724  .append(String.format("\tNumBytes = %x" , getNumBytes()) )
725  .append(String.format("\tLastFile = %x" , getLastFile()) )
726  .append(String.format("\tTableLen = %x" , getTableLen()) );
727 
728  return sb.toString();
729  }
730  }
731 
735  enum CacheFileTypeEnum {
736  EXTERNAL,
737  RANKINGS,
738  BLOCK_256,
739  BLOCK_1K,
740  BLOCK_4K,
741  BLOCK_FILES,
742  BLOCK_ENTRIES,
743  BLOCK_EVICTED
744  }
745 
746 
747 
767  final class CacheAddress {
768  // sundry constants to parse the bit fields in address
769  private static final long ADDR_INITIALIZED_MASK = 0x80000000l;
770  private static final long FILE_TYPE_MASK = 0x70000000;
771  private static final long FILE_TYPE_OFFSET = 28;
772  private static final long NUM_BLOCKS_MASK = 0x03000000;
773  private static final long NUM_BLOCKS_OFFSET = 24;
774  private static final long FILE_SELECTOR_MASK = 0x00ff0000;
775  private static final long FILE_SELECTOR_OFFSET = 16;
776  private static final long START_BLOCK_MASK = 0x0000FFFF;
777  private static final long EXTERNAL_FILE_NAME_MASK = 0x0FFFFFFF;
778 
779  private final long uint32CacheAddr;
780  private final CacheFileTypeEnum fileType;
781  private final int numBlocks;
782  private final int startBlock;
783  private final String fileName;
784  private final int fileNumber;
785 
786  private final String cachePath;
787 
788 
789  CacheAddress(long uint32, String cachePath) {
790 
791  uint32CacheAddr = uint32;
792  this.cachePath = cachePath;
793 
794  int fileTypeEnc = (int)(uint32CacheAddr & FILE_TYPE_MASK) >> FILE_TYPE_OFFSET;
795  fileType = CacheFileTypeEnum.values()[fileTypeEnc];
796 
797  if (isInitialized()) {
798  if (isInExternalFile()) {
799  fileNumber = (int)(uint32CacheAddr & EXTERNAL_FILE_NAME_MASK);
800  fileName = String.format("f_%06x", getFileNumber() );
801  numBlocks = 0;
802  startBlock = 0;
803  } else {
804  fileNumber = (int)((uint32CacheAddr & FILE_SELECTOR_MASK) >> FILE_SELECTOR_OFFSET);
805  fileName = String.format("data_%d", getFileNumber() );
806  numBlocks = (int)(uint32CacheAddr & NUM_BLOCKS_MASK >> NUM_BLOCKS_OFFSET);
807  startBlock = (int)(uint32CacheAddr & START_BLOCK_MASK);
808  }
809  }
810  else {
811  fileName = null;
812  fileNumber = 0;
813  numBlocks = 0;
814  startBlock = 0;
815  }
816  }
817 
818  boolean isInitialized() {
819  return ((uint32CacheAddr & ADDR_INITIALIZED_MASK) != 0);
820  }
821 
822  CacheFileTypeEnum getFileType() {
823  return fileType;
824  }
825 
830  String getFilename() {
831  return fileName;
832  }
833 
834  String getCachePath() {
835  return cachePath;
836  }
837 
838  boolean isInExternalFile() {
839  return (fileType == CacheFileTypeEnum.EXTERNAL);
840  }
841 
842  int getFileNumber() {
843  return fileNumber;
844  }
845 
846  int getStartBlock() {
847  return startBlock;
848  }
849 
850  int getNumBlocks() {
851  return numBlocks;
852  }
853 
854  int getBlockSize() {
855  switch (fileType) {
856  case RANKINGS:
857  return 36;
858  case BLOCK_256:
859  return 256;
860  case BLOCK_1K:
861  return 1024;
862  case BLOCK_4K:
863  return 4096;
864  case BLOCK_FILES:
865  return 8;
866  case BLOCK_ENTRIES:
867  return 104;
868  case BLOCK_EVICTED:
869  return 48;
870  default:
871  return 0;
872  }
873  }
874 
875  public long getUint32CacheAddr() {
876  return uint32CacheAddr;
877  }
878 
879  @Override
880  public String toString() {
881  StringBuilder sb = new StringBuilder();
882  sb.append(String.format("CacheAddr %08x : %s : filename %s",
883  uint32CacheAddr,
884  isInitialized() ? "Initialized" : "UnInitialized",
885  getFilename()));
886 
887  if ((fileType == CacheFileTypeEnum.BLOCK_256) ||
888  (fileType == CacheFileTypeEnum.BLOCK_1K) ||
889  (fileType == CacheFileTypeEnum.BLOCK_4K) ) {
890  sb.append(String.format(" (%d blocks starting at %08X)",
891  this.getNumBlocks(),
892  this.getStartBlock()
893  ));
894  }
895 
896  return sb.toString();
897  }
898 
899  }
900 
904  enum CacheDataTypeEnum {
905  HTTP_HEADER,
906  UNKNOWN,
907  };
908 
918  final class CacheData {
919 
920  private int length;
921  private final CacheAddress address;
922  private CacheDataTypeEnum type;
923 
924  private boolean isHTTPHeaderHint;
925 
926  private CacheFileCopy cacheFileCopy = null;
927  private byte[] data = null;
928 
929  private String httpResponse;
930  private final Map<String, String> httpHeaders = new HashMap<>();
931 
932  CacheData(CacheAddress cacheAdress, int len) {
933  this(cacheAdress, len, false);
934  }
935 
936  CacheData(CacheAddress cacheAdress, int len, boolean isHTTPHeader ) {
937  this.type = CacheDataTypeEnum.UNKNOWN;
938  this.length = len;
939  this.address = cacheAdress;
940  this.isHTTPHeaderHint = isHTTPHeader;
941  }
942 
943  boolean isInExternalFile() {
944  return address.isInExternalFile();
945  }
946 
947  boolean hasHTTPHeaders() {
948  return this.type == CacheDataTypeEnum.HTTP_HEADER;
949  }
950 
951  String getHTTPHeader(String key) {
952  return this.httpHeaders.get(key);
953  }
954 
960  String getHTTPHeaders() {
961  if (!hasHTTPHeaders()) {
962  return "";
963  }
964 
965  StringBuilder sb = new StringBuilder();
966  httpHeaders.entrySet().forEach((entry) -> {
967  if (sb.length() > 0) {
968  sb.append(" \n");
969  }
970  sb.append(String.format("%s : %s",
971  entry.getKey(), entry.getValue()));
972  });
973 
974  return sb.toString();
975  }
976 
977  String getHTTPRespone() {
978  return httpResponse;
979  }
980 
986  void extract() throws TskCoreException, IngestModuleException {
987 
988  // do nothing if already extracted,
989  if (data != null) {
990  return;
991  }
992 
993  // Don't extract data from external files.
994  if (!address.isInExternalFile() ) {
995 
996  cacheFileCopy = getCacheFileCopy(address.getFilename(), address.getCachePath()).get();
997 
998  this.data = new byte [length];
999  ByteBuffer buf = cacheFileCopy.getByteBuffer();
1000  int dataOffset = DATAFILE_HDR_SIZE + address.getStartBlock() * address.getBlockSize();
1001  buf.position(dataOffset);
1002  buf.get(data, 0, length);
1003 
1004  // if this might be a HTPP header, lets try to parse it as such
1005  if ((isHTTPHeaderHint)) {
1006  String strData = new String(data);
1007  if (strData.contains("HTTP")) {
1008 
1009  // Http headers if present, are usually in frst data segment in an entry
1010  // General Parsing algo:
1011  // - Find start of HTTP header by searching for string "HTTP"
1012  // - Skip to the first 0x00 to get to the end of the HTTP response line, this makrs start of headers section
1013  // - Find the end of the header by searching for 0x00 0x00 bytes
1014  // - Extract the headers section
1015  // - Parse the headers section - each null terminated string is a header
1016  // - Each header is of the format "name: value" e.g.
1017 
1018  type = CacheDataTypeEnum.HTTP_HEADER;
1019 
1020  int startOff = strData.indexOf("HTTP");
1021  Charset charset = Charset.forName("UTF-8");
1022  boolean done = false;
1023  int i = startOff;
1024  int hdrNum = 1;
1025 
1026  while (!done) {
1027  // each header is null terminated
1028  int start = i;
1029  while (i < data.length && data[i] != 0) {
1030  i++;
1031  }
1032 
1033  // http headers are terminated by 0x00 0x00
1034  if (i == data.length || data[i+1] == 0) {
1035  done = true;
1036  }
1037 
1038  int len = (i - start);
1039  String headerLine = new String(data, start, len, charset);
1040 
1041  // first line is the http response
1042  if (hdrNum == 1) {
1043  httpResponse = headerLine;
1044  } else {
1045  int nPos = headerLine.indexOf(':');
1046  if (nPos > 0 ) {
1047  String key = headerLine.substring(0, nPos);
1048  String val= headerLine.substring(nPos+1);
1049  httpHeaders.put(key.toLowerCase(), val);
1050  }
1051  }
1052 
1053  i++;
1054  hdrNum++;
1055  }
1056  }
1057  }
1058  }
1059  }
1060 
1061  String getDataString() throws TskCoreException, IngestModuleException {
1062  if (data == null) {
1063  extract();
1064  }
1065  return new String(data);
1066  }
1067 
1068  byte[] getDataBytes() throws TskCoreException, IngestModuleException {
1069  if (data == null) {
1070  extract();
1071  }
1072  return data.clone();
1073  }
1074 
1075  int getDataLength() {
1076  return this.length;
1077  }
1078 
1079  CacheDataTypeEnum getType() {
1080  return type;
1081  }
1082 
1083  CacheAddress getAddress() {
1084  return address;
1085  }
1086 
1087 
1096  String save() throws TskCoreException, IngestModuleException {
1097  String fileName;
1098 
1099  if (address.isInExternalFile()) {
1100  fileName = address.getFilename();
1101  } else {
1102  fileName = String.format("%s__%08x", address.getFilename(), address.getUint32CacheAddr());
1103  }
1104 
1105  String filePathName = getAbsOutputFolderName() + address.getCachePath() + fileName;
1106  save(filePathName);
1107 
1108  return fileName;
1109  }
1110 
1120  void save(String filePathName) throws TskCoreException, IngestModuleException {
1121 
1122  // Save the data to specified file
1123  if (data == null) {
1124  extract();
1125  }
1126 
1127  // Data in external files is not saved in local files
1128  if (!this.isInExternalFile()) {
1129  // write the
1130  try (FileOutputStream stream = new FileOutputStream(filePathName)) {
1131  stream.write(data);
1132  } catch (IOException ex) {
1133  throw new TskCoreException(String.format("Failed to write output file %s", filePathName), ex);
1134  }
1135  }
1136  }
1137 
1138  @Override
1139  public String toString() {
1140  StringBuilder strBuilder = new StringBuilder();
1141  strBuilder.append(String.format("\t\tData type = : %s, Data Len = %d ",
1142  this.type.toString(), this.length ));
1143 
1144  if (hasHTTPHeaders()) {
1145  String str = getHTTPHeader("content-encoding");
1146  if (str != null) {
1147  strBuilder.append(String.format("\t%s=%s", "content-encoding", str ));
1148  }
1149  }
1150 
1151  return strBuilder.toString();
1152  }
1153 
1154  }
1155 
1156 
1160  enum EntryStateEnum {
1161  ENTRY_NORMAL,
1162  ENTRY_EVICTED,
1163  ENTRY_DOOMED
1164  };
1165 
1166 
1167 // Main structure for an entry on the backing storage.
1168 //
1169 // Each entry has a key, identifying the URL the cache entry pertains to.
1170 // If the key is longer than
1171 // what can be stored on this structure, it will be extended on consecutive
1172 // blocks (adding 256 bytes each time), up to 4 blocks (1024 - 32 - 1 chars).
1173 // After that point, the whole key will be stored as a data block or external
1174 // file.
1175 //
1176 // Each entry can have upto 4 data segments
1177 //
1178 // struct EntryStore {
1179 // uint32 hash; // Full hash of the key.
1180 // CacheAddr next; // Next entry with the same hash or bucket.
1181 // CacheAddr rankings_node; // Rankings node for this entry.
1182 // int32 reuse_count; // How often is this entry used.
1183 // int32 refetch_count; // How often is this fetched from the net.
1184 // int32 state; // Current state.
1185 // uint64 creation_time;
1186 // int32 key_len;
1187 // CacheAddr long_key; // Optional address of a long key.
1188 // int32 data_size[4]; // We can store up to 4 data streams for each
1189 // CacheAddr data_addr[4]; // entry.
1190 // uint32 flags; // Any combination of EntryFlags.
1191 // int32 pad[4];
1192 // uint32 self_hash; // The hash of EntryStore up to this point.
1193 // char key[256 - 24 * 4]; // null terminated
1194 // };
1195 
1199  final class CacheEntry {
1200 
1201  // each entry is 256 bytes. The last section of the entry, after all the other fields is a null terminated key
1202  private static final int MAX_KEY_LEN = 256-24*4;
1203 
1204  private final CacheAddress selfAddress;
1205  private final CacheFileCopy cacheFileCopy;
1206 
1207  private final long hash;
1208  private final CacheAddress nextAddress;
1209  private final CacheAddress rankingsNodeAddress;
1210 
1211  private final int reuseCount;
1212  private final int refetchCount;
1213  private final EntryStateEnum state;
1214 
1215  private final long creationTime;
1216  private final int keyLen;
1217 
1218  private final CacheAddress longKeyAddresses; // address of the key, if the key is external to the entry
1219 
1220  private final int dataSizes[];
1221  private final CacheAddress dataAddresses[];
1222  private List<CacheData> dataList;
1223 
1224  private final long flags;
1225 
1226  private String key; // Key may be found within the entry or may be external
1227 
1228  CacheEntry(CacheAddress cacheAdress, CacheFileCopy cacheFileCopy ) {
1229  this.selfAddress = cacheAdress;
1230  this.cacheFileCopy = cacheFileCopy;
1231 
1232  ByteBuffer fileROBuf = cacheFileCopy.getByteBuffer();
1233 
1234  int entryOffset = DATAFILE_HDR_SIZE + cacheAdress.getStartBlock() * cacheAdress.getBlockSize();
1235 
1236  // reposition the buffer to the the correct offset
1237  fileROBuf.position(entryOffset);
1238 
1239  hash = fileROBuf.getInt() & UINT32_MASK;
1240 
1241  long uint32 = fileROBuf.getInt() & UINT32_MASK;
1242  nextAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1243 
1244  uint32 = fileROBuf.getInt() & UINT32_MASK;
1245  rankingsNodeAddress = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1246 
1247  reuseCount = fileROBuf.getInt();
1248  refetchCount = fileROBuf.getInt();
1249 
1250  state = EntryStateEnum.values()[fileROBuf.getInt()];
1251  creationTime = (fileROBuf.getLong() / 1000000) - Long.valueOf("11644473600");
1252 
1253  keyLen = fileROBuf.getInt();
1254 
1255  uint32 = fileROBuf.getInt() & UINT32_MASK;
1256  longKeyAddresses = (uint32 != 0) ? new CacheAddress(uint32, selfAddress.getCachePath()) : null;
1257 
1258  dataList = null;
1259  dataSizes= new int[4];
1260  for (int i = 0; i < 4; i++) {
1261  dataSizes[i] = fileROBuf.getInt();
1262  }
1263  dataAddresses = new CacheAddress[4];
1264  for (int i = 0; i < 4; i++) {
1265  dataAddresses[i] = new CacheAddress(fileROBuf.getInt() & UINT32_MASK, selfAddress.getCachePath());
1266  }
1267 
1268  flags = fileROBuf.getInt() & UINT32_MASK;
1269  // skip over pad
1270  for (int i = 0; i < 4; i++) {
1271  fileROBuf.getInt();
1272  }
1273 
1274  // skip over self hash
1275  fileROBuf.getInt();
1276 
1277  // get the key
1278  if (longKeyAddresses != null) {
1279  // Key is stored outside of the entry
1280  try {
1281  CacheData data = new CacheData(longKeyAddresses, this.keyLen, true);
1282  key = data.getDataString();
1283  } catch (TskCoreException | IngestModuleException ex) {
1284  logger.log(Level.SEVERE, String.format("Failed to get external key from address %s", longKeyAddresses)); //NON-NLS
1285  }
1286  }
1287  else { // key stored within entry
1288  StringBuilder strBuilder = new StringBuilder(MAX_KEY_LEN);
1289  int keyLen = 0;
1290  while (fileROBuf.remaining() > 0 && keyLen < MAX_KEY_LEN) {
1291  char keyChar = (char)fileROBuf.get();
1292  if (keyChar == '\0') {
1293  break;
1294  }
1295  strBuilder.append(keyChar);
1296  keyLen++;
1297  }
1298 
1299  key = strBuilder.toString();
1300  }
1301  }
1302 
1303  public CacheAddress getAddress() {
1304  return selfAddress;
1305  }
1306 
1307  public long getHash() {
1308  return hash;
1309  }
1310 
1311  public CacheAddress getNextAddress() {
1312  return nextAddress;
1313  }
1314 
1315  public int getReuseCount() {
1316  return reuseCount;
1317  }
1318 
1319  public int getRefetchCount() {
1320  return refetchCount;
1321  }
1322 
1323  public EntryStateEnum getState() {
1324  return state;
1325  }
1326 
1327  public long getCreationTime() {
1328  return creationTime;
1329  }
1330 
1331  public long getFlags() {
1332  return flags;
1333  }
1334 
1335  public String getKey() {
1336  return key;
1337  }
1338 
1347  public List<CacheData> getData() throws TskCoreException, IngestModuleException {
1348 
1349  if (dataList == null) {
1350  dataList = new ArrayList<>();
1351  for (int i = 0; i < 4; i++) {
1352  if (dataSizes[i] > 0) {
1353  CacheData cacheData = new CacheData(dataAddresses[i], dataSizes[i], true );
1354 
1355  cacheData.extract();
1356  dataList.add(cacheData);
1357  }
1358  }
1359  }
1360  return dataList;
1361  }
1362 
1370  boolean hasHTTPHeaders() {
1371  if ((dataList == null) || dataList.isEmpty()) {
1372  return false;
1373  }
1374  return dataList.get(0).hasHTTPHeaders();
1375  }
1376 
1383  String getHTTPHeader(String key) {
1384  if ((dataList == null) || dataList.isEmpty()) {
1385  return null;
1386  }
1387  // First data segment has the HTTP headers, if any
1388  return dataList.get(0).getHTTPHeader(key);
1389  }
1390 
1396  String getHTTPHeaders() {
1397  if ((dataList == null) || dataList.isEmpty()) {
1398  return null;
1399  }
1400  // First data segment has the HTTP headers, if any
1401  return dataList.get(0).getHTTPHeaders();
1402  }
1403 
1412  boolean isBrotliCompressed() {
1413 
1414  if (hasHTTPHeaders() ) {
1415  String encodingHeader = getHTTPHeader("content-encoding");
1416  if (encodingHeader!= null) {
1417  return encodingHeader.trim().equalsIgnoreCase("br");
1418  }
1419  }
1420 
1421  return false;
1422  }
1423 
1424  @Override
1425  public String toString() {
1426  StringBuilder sb = new StringBuilder();
1427  sb.append(String.format("Entry = Hash: %08x, State: %s, ReuseCount: %d, RefetchCount: %d",
1428  this.hash, this.state.toString(), this.reuseCount, this.refetchCount ))
1429  .append(String.format("\n\tKey: %s, Keylen: %d",
1430  this.key, this.keyLen, this.reuseCount, this.refetchCount ))
1431  .append(String.format("\n\tCreationTime: %s",
1432  TimeUtilities.epochToTime(this.creationTime) ))
1433  .append(String.format("\n\tNext Address: %s",
1434  (nextAddress != null) ? nextAddress.toString() : "None"));
1435 
1436  for (int i = 0; i < 4; i++) {
1437  if (dataSizes[i] > 0) {
1438  sb.append(String.format("\n\tData %d: cache address = %s, Data = %s",
1439  i, dataAddresses[i].toString(),
1440  (dataList != null)
1441  ? dataList.get(i).toString()
1442  : "Data not retrived yet."));
1443  }
1444  }
1445 
1446  return sb.toString();
1447  }
1448  }
1449 }

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