Task 339: .KEY File Format

Task 339: .KEY File Format

The .KEY file format, used by Apple Keynote presentations, is a proprietary compressed ZIP archive. Based on available specifications and reverse-engineered details, the properties intrinsic to its file system include:

  • File signature (magic number): 50 4B 03 04 (PK\003\004, indicating a ZIP archive).
  • MIME type: application/vnd.apple.keynote.
  • Internal ZIP structure with specific directories and files, including:
  • Data/ (contains media resources like images, videos, and audio).
  • Index/ (contains .iwa files or Index.zip with presentation data; .iwa files are Snappy-compressed Protobuf-based files holding metadata, text, slide definitions, transitions, and animations).
  • Metadata/ (contains plist files such as buildVersionHistory.plist for version history, DocumentIdentifier for a unique UUID, and Properties.plist for document properties like author, title, language, and company).
  • QuickLook/ (contains Thumbnail.jpg and sometimes Preview.pdf for macOS Quick Look previews).
  • Preview files: preview.jpg, preview-micro.jpg, preview-web.jpg (JPEG previews of the presentation).
  • Version compatibility: Tied to Keynote versions (e.g., Protobuf definitions change across versions like 10.2 to 11.2, affecting backwards compatibility).
  • Compression: Overall ZIP compression, with internal .iwa files using Snappy compression.
  • No public official specification; format is proprietary but can be unpacked as ZIP.

Two direct download links for .KEY files:

Here's an HTML/JavaScript snippet that can be embedded in a Ghost blog post. It allows users to drag and drop a .KEY file, treats it as a ZIP, extracts the key components, and dumps the properties (e.g., file listings, metadata from plists, preview info) to the screen. It uses JSZip for ZIP handling and plist.js for parsing plists (include those libraries via CDN).

Drag and drop a .KEY file here
  1. Python class for handling .KEY files. It uses zipfile for unpacking, plistlib for plists, and prints properties. For write, it can modify and re-zip (simple example).
import zipfile
import plistlib
import os
from io import BytesIO

class KeyFileHandler:
    def __init__(self, filepath):
        self.filepath = filepath
        self.zip = None
        self.properties = {}

    def open(self):
        self.zip = zipfile.ZipFile(self.filepath, 'r')
        self._decode_properties()

    def _decode_properties(self):
        # Extract file structure
        self.properties['file_signature'] = self.zip.read(self.zip.namelist()[0])[:4].hex().upper()
        self.properties['structure'] = self.zip.namelist()

        # Parse Metadata plists
        plists = ['Metadata/buildVersionHistory.plist', 'Metadata/Properties.plist']
        for plist_path in plists:
            if plist_path in self.zip.namelist():
                with self.zip.open(plist_path) as f:
                    self.properties[plist_path] = plistlib.loads(f.read())

        # DocumentIdentifier is text
        doc_id_path = 'Metadata/DocumentIdentifier'
        if doc_id_path in self.zip.namelist():
            with self.zip.open(doc_id_path) as f:
                self.properties[doc_id_path] = f.read().decode('utf-8').strip()

        # Check previews and index
        self.properties['previews'] = [f for f in self.zip.namelist() if 'preview' in f or 'Thumbnail' in f]
        self.properties['has_index'] = any('Index' in f for f in self.zip.namelist())

    def print_properties(self):
        for key, value in self.properties.items():
            print(f"{key}: {value}")

    def write(self, new_filepath, modifications=None):
        # Simple write: copy and apply mods (e.g., dict of path: content)
        with zipfile.ZipFile(new_filepath, 'w') as new_zip:
            for item in self.zip.infolist():
                data = self.zip.read(item.filename)
                if modifications and item.filename in modifications:
                    data = modifications[item.filename]
                new_zip.writestr(item, data)

    def close(self):
        if self.zip:
            self.zip.close()

# Usage example:
# handler = KeyFileHandler('example.key')
# handler.open()
# handler.print_properties()
# handler.write('modified.key', {'Metadata/Properties.plist': plistlib.dumps({'new_key': 'value'})})
# handler.close()
  1. Java class for handling .KEY files. Uses java.util.zip and external plist parser (assume com.dd.plist).
import java.io.*;
import java.util.*;
import java.util.zip.*;
import com.dd.plist.*;  // External library for plist parsing, e.g., dd-plist

public class KeyFileHandler {
    private String filepath;
    private ZipFile zip;
    private Map<String, Object> properties = new HashMap<>();

    public KeyFileHandler(String filepath) {
        this.filepath = filepath;
    }

    public void open() throws IOException {
        zip = new ZipFile(filepath);
        decodeProperties();
    }

    private void decodeProperties() throws IOException {
        // File signature (first 4 bytes)
        ZipEntry firstEntry = zip.entries().nextElement();
        byte[] buffer = new byte[4];
        try (InputStream is = zip.getInputStream(firstEntry)) {
            is.read(buffer);
        }
        properties.put("file_signature", bytesToHex(buffer));

        // Structure
        List<String> structure = new ArrayList<>();
        Enumeration<? extends ZipEntry> entries = zip.entries();
        while (entries.hasMoreElements()) {
            structure.add(entries.nextElement().getName());
        }
        properties.put("structure", structure);

        // Parse plists
        String[] plists = {"Metadata/buildVersionHistory.plist", "Metadata/Properties.plist"};
        for (String plistPath : plists) {
            ZipEntry entry = zip.getEntry(plistPath);
            if (entry != null) {
                try (InputStream is = zip.getInputStream(entry)) {
                    NSObject obj = PropertyListParser.parse(is);
                    properties.put(plistPath, obj);
                } catch (Exception e) {
                    // Handle error
                }
            }
        }

        // DocumentIdentifier
        ZipEntry docIdEntry = zip.getEntry("Metadata/DocumentIdentifier");
        if (docIdEntry != null) {
            try (BufferedReader br = new BufferedReader(new InputStreamReader(zip.getInputStream(docIdEntry)))) {
                properties.put("Metadata/DocumentIdentifier", br.readLine());
            }
        }

        // Previews and index
        List<String> previews = new ArrayList<>();
        boolean hasIndex = false;
        entries = zip.entries();
        while (entries.hasMoreElements()) {
            String name = entries.nextElement().getName();
            if (name.contains("preview") || name.contains("Thumbnail")) {
                previews.add(name);
            }
            if (name.contains("Index")) {
                hasIndex = true;
            }
        }
        properties.put("previews", previews);
        properties.put("has_index", hasIndex);
    }

    public void printProperties() {
        for (Map.Entry<String, Object> entry : properties.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }

    public void write(String newFilepath, Map<String, byte[]> modifications) throws IOException {
        try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(newFilepath))) {
            Enumeration<? extends ZipEntry> entries = zip.entries();
            while (entries.hasMoreElements()) {
                ZipEntry entry = entries.nextElement();
                byte[] data = inputStreamToBytes(zip.getInputStream(entry));
                if (modifications != null && modifications.containsKey(entry.getName())) {
                    data = modifications.get(entry.getName());
                }
                ZipEntry newEntry = new ZipEntry(entry.getName());
                zos.putNextEntry(newEntry);
                zos.write(data);
                zos.closeEntry();
            }
        }
    }

    public void close() throws IOException {
        if (zip != null) {
            zip.close();
        }
    }

    private static String bytesToHex(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte b : bytes) {
            sb.append(String.format("%02X ", b));
        }
        return sb.toString().trim();
    }

    private byte[] inputStreamToBytes(InputStream is) throws IOException {
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int nRead;
        byte[] data = new byte[16384];
        while ((nRead = is.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, nRead);
        }
        return buffer.toByteArray();
    }

    // Usage example in main method...
}
  1. JavaScript class for handling .KEY files (Node.js, uses jszip and plist).
const fs = require('fs');
const JSZip = require('jszip');
const plist = require('plist');

class KeyFileHandler {
  constructor(filepath) {
    this.filepath = filepath;
    this.zip = null;
    this.properties = {};
  }

  async open() {
    const data = fs.readFileSync(this.filepath);
    this.zip = await JSZip.loadAsync(data);
    await this.decodeProperties();
  }

  async decodeProperties() {
    // File signature
    const firstFile = Object.keys(this.zip.files)[0];
    const firstContent = await this.zip.file(firstFile).async('arraybuffer');
    const sig = Buffer.from(firstContent.slice(0, 4)).toString('hex').toUpperCase();
    this.properties.file_signature = sig;

    // Structure
    this.properties.structure = Object.keys(this.zip.files);

    // Parse plists
    const plists = ['Metadata/buildVersionHistory.plist', 'Metadata/Properties.plist'];
    for (const plistPath of plists) {
      if (this.zip.file(plistPath)) {
        const content = await this.zip.file(plistPath).async('text');
        this.properties[plistPath] = plist.parse(content);
      }
    }

    // DocumentIdentifier
    const docIdPath = 'Metadata/DocumentIdentifier';
    if (this.zip.file(docIdPath)) {
      const content = await this.zip.file(docIdPath).async('text');
      this.properties[docIdPath] = content.trim();
    }

    // Previews and index
    this.properties.previews = this.properties.structure.filter(f => f.includes('preview') || f.includes('Thumbnail'));
    this.properties.has_index = this.properties.structure.some(f => f.includes('Index'));
  }

  printProperties() {
    console.log(this.properties);
  }

  async write(newFilepath, modifications = {}) {
    for (const [path, content] of Object.entries(modifications)) {
      this.zip.file(path, content);
    }
    const buffer = await this.zip.generateAsync({type: 'nodebuffer'});
    fs.writeFileSync(newFilepath, buffer);
  }

  close() {
    // No close needed for JSZip
  }
}

// Usage:
// const handler = new KeyFileHandler('example.key');
// await handler.open();
// handler.printProperties();
// await handler.write('modified.key', {'Metadata/Properties.plist': plist.build({new_key: 'value'})});

  1. For C (using C++ for class support), a class to handle .KEY files. Uses miniz for ZIP (assume included), and a simple plist parser (simplified, as full plist parsing in C++ is complex; use external if needed).
#include <iostream>
#include <fstream>
#include <vector>
#include <map>
#include <string>
#include <sstream>
#include "miniz.h"  // Assume miniz for ZIP handling
// For plist, simplified parser or use external like tinyxml for XML plists

class KeyFileHandler {
private:
    std::string filepath;
    mz_zip_archive zip;
    std::map<std::string, std::string> properties;  // Simplified to string values

public:
    KeyFileHandler(const std::string& fp) : filepath(fp) {
        memset(&zip, 0, sizeof(zip));
    }

    ~KeyFileHandler() {
        close();
    }

    bool open() {
        if (!mz_zip_reader_init_file(&zip, filepath.c_str(), 0)) {
            return false;
        }
        decodeProperties();
        return true;
    }

    void decodeProperties() {
        // File signature
        void* firstData;
        size_t firstSize;
        std::string firstFile = mz_zip_reader_get_filename(&zip, 0);
        mz_zip_reader_extract_to_mem(&zip, 0, &firstData, &firstSize, 0);
        std::string sig;
        for (size_t i = 0; i < 4 && i < firstSize; ++i) {
            char buf[3];
            sprintf(buf, "%02X ", ((unsigned char*)firstData)[i]);
            sig += buf;
        }
        properties["file_signature"] = sig.substr(0, sig.size() - 1);
        mz_free(firstData);

        // Structure
        std::stringstream structure;
        for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&zip); ++i) {
            structure << mz_zip_reader_get_filename(&zip, i) << " ";
        }
        properties["structure"] = structure.str();

        // Simplified plist parsing (text dump)
        std::vector<std::string> plists = {"Metadata/buildVersionHistory.plist", "Metadata/Properties.plist", "Metadata/DocumentIdentifier"};
        for (const auto& p : plists) {
            mz_uint index;
            if (mz_zip_reader_locate_file(&zip, p.c_str(), nullptr, 0, &index)) {
                void* data;
                size_t size;
                mz_zip_reader_extract_to_mem(&zip, index, &data, &size, 0);
                properties[p] = std::string((char*)data, size);
                mz_free(data);
            }
        }

        // Previews and index
        std::stringstream previews;
        bool hasIndex = false;
        for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&zip); ++i) {
            std::string name = mz_zip_reader_get_filename(&zip, i);
            if (name.find("preview") != std::string::npos || name.find("Thumbnail") != std::string::npos) {
                previews << name << " ";
            }
            if (name.find("Index") != std::string::npos) {
                hasIndex = true;
            }
        }
        properties["previews"] = previews.str();
        properties["has_index"] = hasIndex ? "true" : "false";
    }

    void printProperties() {
        for (const auto& prop : properties) {
            std::cout << prop.first << ": " << prop.second << std::endl;
        }
    }

    bool write(const std::string& newFilepath, const std::map<std::string, std::string>& modifications) {
        mz_zip_archive newZip;
        memset(&newZip, 0, sizeof(newZip));
        if (!mz_zip_writer_init_file(&newZip, newFilepath.c_str(), 0)) {
            return false;
        }

        for (mz_uint i = 0; i < mz_zip_reader_get_num_files(&zip); ++i) {
            std::string name = mz_zip_reader_get_filename(&zip, i);
            void* data;
            size_t size;
            mz_zip_reader_extract_to_mem(&zip, i, &data, &size, 0);

            auto it = modifications.find(name);
            if (it != modifications.end()) {
                data = (void*)it->second.data();
                size = it->second.size();
            }

            mz_zip_writer_add_mem(&newZip, name.c_str(), data, size, MZ_DEFAULT_COMPRESSION);
            mz_free(data);
        }

        mz_zip_writer_finalize_archive(&newZip);
        mz_zip_writer_end(&newZip);
        return true;
    }

    void close() {
        mz_zip_reader_end(&zip);
    }
};

// Usage:
// KeyFileHandler handler("example.key");
// if (handler.open()) {
//     handler.printProperties();
//     std::map<std::string, std::string> mods = {{"Metadata/Properties.plist", "<plist><dict><key>new</key><string>value</string></dict></plist>"}};
//     handler.write("modified.key", mods);
// }