Meta Tech Podcast
Meta deployed post-quantum asymmetric cryptography and physically destroyed administrative access cards for its security modules to preserve multi-device chat histories without holding decryption keys.
AI-assisted summary of 88: No Way Out - Engineering Message History Only You Can Read — original source. Check the original for full context.
Source published:
Key ideas
- Meta introduced Labyrinth to bring end-to-end encryption to Messenger while preserving instant cross-device chat history across as many as 100 concurrent endpoints per account. (7:17)
- To secure six-digit PIN recovery, Meta deployed custom rate-limiting firmware on hardware security modules (HSMs) using the OPAQUE protocol and physically destroyed the provisioning smartcards to permanently lock itself out. (10:00)
- Labyrinth 1.1 transitioned from symmetric storage keys to asymmetric public-key mailboxes, enabling senders to write encrypted messages directly to recipient backups without waiting for the recipient's devices to be online. (26:12)
- To counter future quantum harvesting attacks against stored archives, Labyrinth 1.1 implemented Hawk, a hybrid cryptosystem combining classical Diffie-Hellman authentication with ML-KEM post-quantum confidentiality. (31:37)
Detailed notes
## Background Published on October 15, 2026, on the Meta Tech Podcast and hosted by Pascal, this episode features Meta engineer Emma Connor discussing Labyrinth, Messenger's encrypted message storage protocol. According to the episode description, the discussion explores how Meta retrofitted end-to-end encryption onto a legacy messaging service without sacrificing historical chat continuity, covering key management, tamper-resistant HSM lockouts, key transparency, and post-quantum cryptography.
## TL;DR Meta software engineer Emma Connor breaks down the engineering behind Labyrinth, the system built to preserve multi-device chat history while establishing default end-to-end encryption across Facebook Messenger. To protect user archives without holding decryption keys, Labyrinth combines the OPAQUE password-authenticated key exchange protocol with tamper-resistant Hardware Security Modules (HSMs) whose provisioning cards were permanently destroyed to prevent internal override. The discussion tracks the protocol's evolution to Labyrinth 1.1, which eliminates synchronization bottlenecks by adopting mailbox-level public keys, audited Key Transparency directories via Cloudflare, and post-quantum hybrid encryption using ML-KEM to protect against future harvest-now-decrypt-later attacks.
## Atmosphere & style The conversation carries a deliberate, intellectually rigorous tone, driven by Pascal's focused questioning and Connor's clear, methodical breakdowns of complex cryptographic trade-offs. The dynamic balances low-level protocol mechanics with pragmatic operational engineering, treating security not as an abstract ideal but as a set of physical and architectural constraints. Listening to the breakdown of intentional design choices—such as engineers physically destroying HSM smartcards to strip themselves of access—conveys a palpable sense of institutional restraint and disciplined systems design.
## Bulletin - `07:17` Meta introduced Labyrinth to bring end-to-end encryption to Messenger while preserving instant cross-device chat history across as many as 100 concurrent endpoints per account. - `10:00` To secure six-digit PIN recovery, Meta deployed custom rate-limiting firmware on hardware security modules (HSMs) using the OPAQUE protocol and physically destroyed the provisioning smartcards to permanently lock itself out. - `26:12` Labyrinth 1.1 transitioned from symmetric storage keys to asymmetric public-key mailboxes, enabling senders to write encrypted messages directly to recipient backups without waiting for the recipient's devices to be online. - `31:37` To counter future quantum harvesting attacks against stored archives, Labyrinth 1.1 implemented Hawk, a hybrid cryptosystem combining classical Diffie-Hellman authentication with ML-KEM post-quantum confidentiality.
## Key takeaways - `00:00` Retrofitting E2EE onto Messenger required matching user expectations of instantaneous server-side message history across new browsers and devices without Meta retaining access to keys. - `05:00` Unlike WhatsApp's model of local transfers and third-party cloud backups, Messenger users expect continuous access across secondary devices left unused for long periods. - `07:17` Labyrinth decouples raw encrypted ciphertext stored on Meta servers from decryption keys, which reside strictly on client endpoints during daily operation. - `10:00` PIN recovery relies on the OPAQUE password-authenticated key exchange protocol, ensuring neither Meta nor the HSM ever learns the raw PIN or the derived backup key. - `10:00` Meta physically destroyed the smartcards required to flash firmware onto its rate-limiting HSMs, preventing internal engineers or attackers from bypassing PIN guess limits. - `15:00` The One-Time Code recovery mechanism authenticates a public-key secure channel across active sessions rather than acting as a direct authorization secret, backed by local client rate limiting. - `20:00` Messenger avoids O(N^2) connection complexity across up to 100 registered endpoints by addressing messages to single user-level mailbox keys rather than fanning out to every individual device key. - `23:25` Post-revocation confidentiality is maintained through epoch keys generated from fresh entropy and shared only among remaining trusted devices, preventing revoked devices from reading subsequent messages. - `26:12` Labyrinth 1.0 required recipient devices to come online to decrypt and re-upload messages to the backup archive, causing synchronization gaps on secondary devices. - `26:12` Labyrinth 1.1 resolves synchronization delays by allowing senders to encrypt directly to a recipient's mailbox public key on the server. - `30:00` To prevent server manipulation of mailbox public keys, Meta implemented Key Transparency using an append-only verifiable ledger signed periodically by external auditors like Cloudflare. - `31:37` Because message archives are preserved long term, Labyrinth adopted the Hawk hybrid cryptosystem with ML-KEM to protect stored ciphertext against future 'store now, decrypt later' quantum decryption.
## Detailed walkthrough ### `00:00` Introduction and the Paradox of Retrofitting E2EE
Host Pascal opens the episode by returning from a month-long recharge leave, joking about the corporate luxury problem of sitting on a mountain of accrued paid time off with only a few months left in the year to take it. Shifting to technical topics, he points out a fundamental truth about cryptography: encrypting data so thoroughly that no one—including the author—can ever read it again is simple. The true engineering nightmare begins when you try to retroactively introduce end-to-end encryption into an existing platform. While WhatsApp adopted end-to-end encryption early enough to set user expectations around local device backups, Messenger operated for over a decade under a completely different paradigm. Users spent years signing into Messenger from arbitrary computers, or pulling an old phone out of a drawer after 18 months, expecting their entire message history to be waiting for them. Retrofitting encryption meant honoring those unyielding habits while explicitly depriving Meta of the ability to unlock the data.
Before introducing the guest, Pascal highlights an article on the Meta Engineering blog about Network Time Security (NTS). Meta’s public time service has implemented authenticated time packets, allowing connected devices to verify that timestamp data genuinely originates from Meta without tampering. As Pascal notes, the Network Time Protocol (NTP) has operated in an unauthenticated state since 1985, despite being the bedrock against which certificate expirations, cryptographic handshakes, and token lifetimes are validated. The deployed NTS implementation keeps no per-client state, and its protocol client and server codebases are open source. Pascal then introduces Emma Connor, a research scientist at Meta focusing on encrypted infrastructure, who joined the company in August 2021 immediately after defending her PhD. Emma previously spent several years working part-time on Cisco’s security initiatives group, concentrating on penetration testing and exploit development.
- Emma’s team at Meta is responsible for encrypted messaging infrastructure (principally end-to-end encryption for Facebook Messenger). - The team also builds federated analytics, combining differential privacy techniques and trusted execution environments (TEEs) to collect aggregated product telemetry without exposing individual user data to Meta. - Pascal notes that the delay between Meta’s announcement of end-to-end encryption for Messenger and its full production rollout gave rise to public skepticism and conspiracy theories, when the reality was simply that solving cross-device encrypted message history without breaking legacy behavior is extraordinarily complex.
`05:00` User Mental Models: WhatsApp vs. Messenger
The core user experience problem is simple to state but technically brutal: if a user drops their phone into a lake and buys a replacement, they expect all their past conversations to appear as soon as they log back in. In a classic client-server model, the data center simply serves the stored chat archive upon authentication. In an end-to-end encrypted architecture, Meta cannot hold the keys to decrypt or read those chats, creating an apparent paradox when provisioning a fresh device that possesses no existing cryptographic state. Emma explains that while migrating users to an encrypted architecture inevitably forces some new mental models, the primary objective was to minimize the delta between the old and new user experience.
Emma and Pascal contrast Messenger's engineering challenge with WhatsApp’s architecture. Because WhatsApp introduced encryption earlier in its lifespan, its community was accustomed to device-to-device transfers and local operating system backups (such as iCloud or Google Drive). Migrating between devices on WhatsApp typically involves an explicit, high-friction, and time-consuming sync process that works best when both the old and new hardware are present. Messenger users, by contrast, expect to walk into an internet cafe, open a clean web browser, authenticate, and have their entire conversation timeline stream in virtually instantaneously. As Emma observes:
> “Part of the problem we’re trying to solve is making that new mental model as small as possible compared to the existing mental model.”
`07:17` Introducing Labyrinth and the Modular Key Dilemma
To bridge this gap without compromising the cryptographic boundary, Meta engineered an encrypted storage system called **Labyrinth**. Emma explains that the name was originally coined by John Milliken. The moniker reflected an early architectural concept where the storage topology formed a hidden structure that made it hard for an adversary (or Meta) to even locate where specific message records lived. While that particular structural obscurity is a historical factoid that no longer applies to the production deployment, the name Labyrinth stuck.
When Pascal brings up the central question of where the cryptographic keys actually reside, Emma clarifies that during normal day-to-day operation, active decryption keys reside strictly on the user’s local endpoints—they are never stored with Meta. To survive hardware loss or phone upgrades, Labyrinth relies on backup keys stored across segregated locations designed around modular recovery flows:
> “During daily use, the keys live only on your device, but we have different ways to store backup keys in different locations so that if you lose your device or something happens, you can get back in.”
```mermaid flowchart TD accTitle: Labyrinth Key Storage Architecture Overview accDescr: High-level separation between the user's local device, the untrusted Meta storage layer, and the external secure recovery mechanisms.
subgraph Client["User Device (Active Endpoint)"] LocalKeys["Active Session & Decryption Keys"] LocalCache["Decrypted Local Message Cache"] LocalKeys --> LocalCache end
subgraph MetaInfra["Meta Infrastructure (Untrusted)"] BlobStore["Labyrinth Encrypted Message Blobs\n(Meta cannot read content)"] end
subgraph RecoveryTier["Modular Recovery Options (External / Isolated)"] BackupKeys["Encrypted Backup Master Keys\n(Hardware Security Modules / Third-party)"] end
Client -- "Reads/Writes Ciphertext" --> BlobStore Client -. "Bootstraps via Passcode/PIN".-> RecoveryTier RecoveryTier -. "Yields Master Key (On Auth Only)".-> Client ```
- Messenger’s architecture decouples raw encrypted message blobs (stored by Meta in ciphertext form) from the keys required to decode them. - Because everyday operational keys never hit Meta’s server fleet, system recovery must bootstrap from an external mechanism or a specialized rate-limited credential like a PIN. - Modularity was an explicit architectural requirement from day one, allowing the team to introduce new recovery backends (such as cloud drive escrows, hardware keys, or specialized hardware security modules) without rebuilding the core message sync engine.
`10:00` Securing the Six-Digit PIN with OPAQUE and Locked-Down HSMs
When designing authentication options for end-to-end encrypted message backups at scale, the primary method deployed to users is a simple six-digit numerical PIN. While a six-digit PIN provides a very small key space that would typically be trivial for an attacker to brute-force, Meta mitigates this attack vector by strictly rate-limiting guess attempts inside tamper-resistant hardware. The long encryption keys guarding the user's backups are stored in a form that only hardware security modules (HSMs) residing in Meta’s data centers can decrypt, and the HSMs are cryptographically blocked from doing so without the user's PIN.
Rather than sending the raw PIN over the wire, the system relies on an asymmetric password-authenticated key exchange protocol called OPAQUE, which Meta has implemented and open-sourced as a Rust crate. The device derives a cryptographic token from the PIN locally—ensuring the raw code never leaves the client—and exchanges a few challenge-response rounds with the HSM. Through this exchange, the user device reconstructs the master backup decryption key. Emma Connor summarizes the mutual privacy guarantees established between the client and the data center: “And as a result of this exchange the device is able to reconstruct the key to the backup whereas the HSM never sees your PIN and it never sees the key to your backup either.” An eavesdropper monitoring the network packets cannot deduce the PIN without breaking an underlying cryptographically hard problem.
```mermaid flowchart TD accTitle: OPAQUE Protocol with HSM Rate Limiting accDescr: Flow diagram showing how a user device derives a token from a PIN and exchanges rounds with an HSM to unlock backups without revealing the PIN or key. subgraph Client["User Device"] PIN["User Enters 6-Digit PIN"] Derive["Derive Cryptographic Token (PIN never leaves device)"] Reconstruct["Reconstruct Master Backup Key"] end subgraph Server["Data Center Hardware Security Module (HSM)"] HSMCode["Tamper-Resistant HSM Firmware (Rate-Limiting Enforced)"] DecKey["Store Encrypted Backup Keys"] end PIN --> Derive Derive <-->|"OPAQUE Multi-Round Exchange"| HSMCode HSMCode -.->|"Decrypts intermediate secret only with valid PIN exchange"| DecKey Derive --> Reconstruct ```
To explain how the rate limiting cannot simply be overridden by Meta engineers or an attacker in the data center, Emma points to the analogy of an iPhone's Secure Enclave, which locks users out after successive wrong passcode attempts and resists extraction even against well-funded laboratory chip disassembly. Meta uses HSMs with similar physical tamper resistance. During initial infrastructure setup, engineers flash the rate-limiting firmware onto the units and then physically burn their bridges: “So we deploy the code to the HSM when we are provisioning this system, but we actually destroy the cards that allow us to load code onto the HSMs after the first deployment.” By destroying the deployment smartcards, Meta ensures that nobody—not even internal engineering teams—can alter the rate-limiting rules or extract the underlying keys.
`15:00` One-Time Code Recovery and Cross-Device PAKE Authentication
For situations where a user forgets their six-digit backup PIN, Labyrinth provides alternative recovery paths. The most common alternative is the One-Time Code (OTC) flow, which can be initiated if the user still has an active, authenticated session on another device. In this flow, the user signs in on a secondary device, requests a code, and a six-digit code appears on their existing logged-in device. Typing that code into the new device restores access to the encrypted backup without ever requiring the original PIN.
Despite appearing to the user as a simple PIN, the one-time code functions under the hood as an authentication mechanism for a cryptographic secure channel rather than an access key itself. Emma explains: “The one-time code is actually not, under the hood, it actually doesn't work like a pin in the sense that it's granting access to anything. It's actually just used to authenticate the secure channel.” The two endpoints perform a key exchange—akin to standard Diffie-Hellman, RSA public-key schemes, or post-quantum algorithms like ML-KEM—to establish an encrypted connection. To prevent man-in-the-middle attacks where an adversary completes separate key exchanges with each side, both devices generate a six-digit code derived from hash functions calculated over the exchange parameters and public keys. Because the initiating device already computes the expected hash value independently, entering the code proves that a human in the physical world is viewing both screens simultaneously and verifying the connection. Because servers might be untrusted or compromised, the client devices must enforce their own rate limiting locally to ensure an attacker cannot brute-force the one-time code.
This architecture must also scale to Messenger's permissive multi-device environment. While WhatsApp historically maintained a strict cap of around four linked devices, Messenger supports a vastly wider footprint:
- Emma notes that the Messenger device limit is “something like 200” concurrent endpoints. - The high threshold is intentional so users can leave a secondary tablet or an old phone sitting inactive in a drawer for a year or two, power it back on, and instantly have their entire message history intact without being abruptly signed out. - Pascal confirms this user experience firsthand, recalling turning on a spare phone retrieved from a drawer after an extended absence and immediately receiving Messenger notifications, whereas other modern messaging apps would have evicted the session. src/utils/text_sanitizer.py
# Fallback: remove remaining backslashes that don't look like standard formatting # Keep \\, \n, \t, etc. text = re.sub(r'\\(?![\\nrt*_{}\[\]()#+\-.!|`])', '', text)
return text import re
def sanitize_markdown(text: str) -> str: """ Sanitize text to fix over-escaped characters often produced by LLMs, such as escaped punctuation inside words (e.g. Meta\'s, WhatsApp\'s, aren\'t). """ if not text: return text
# Fix escaped apostrophes/single quotes inside words: Meta\'s -> Meta's, isn\'t -> isn't text = re.sub(r'(\w)\\\'(\w)', r"\1'\2", text) # Fix escaped single quotes generally if not needed: \' -> ' text = re.sub(r"\\'", "'", text)
# Fix escaped double quotes: \" -> " text = re.sub(r'\\"', '"', text)
# Fix escaped dashes/hyphens: \- -> - text = re.sub(r'\\-', '-', text)
# Fix escaped colons: \: -> : text = re.sub(r'\\:', ':', text)
# Fix escaped underscores when they shouldn't be (e.g. in words, or keep standard) # Be careful not to break valid markdown escapes like \* or \_ if intentional, # but LLMs often escape parentheses, brackets, etc. unnecessarily. # E.g. \( -> (, \) -> ), \[ -> [, \] -> ] # Keep markdown link syntax and brackets intact, but remove escaping on plain parens # If LLM wrote \(something\) instead of (something) text = re.sub(r'\\\(', '(', text) text = re.sub(r'\\\)', ')', text)
# Fix escaped periods at line starts or inside text: \. -> . text = re.sub(r'\\\.', '.', text)
# Fix escaped ampersands: \& -> & text = re.sub(r'\\&', '&', text) src/parsers/youtube.py import re from typing import Optional, Tuple from youtube_transcript_api import YouTubeTranscriptApi from youtube_transcript_api._errors import NoTranscriptFound, TranscriptsDisabled import yt_dlp from src.parsers.base import BaseParser from src.utils.logger import logger
class YouTubeParser(BaseParser): @classmethod def can_handle(cls, url: str) -> bool: youtube_regex = ( r'(https?://)?(www\.)?' '(youtube|youtu|youtube-nocookie)\.(com|be)/' '(watch\?v=|embed/|v/|.+\?v=)?([^&=%\?]{11})' ) return bool(re.match(youtube_regex, url))
def _extract_video_id(self, url: str) -> Optional[str]: patterns = [ r'(?:v=|\/)([0-9A-Za-z_-]{11}).*', r'(?:embed\/|v\/|watch\?v=|youtu\.be\/|\/v\/|^)([0-9A-Za-z_-]{11})' ] for pattern in patterns: match = re.search(pattern, url) if match: return match.group(1) return None
def _extract_metadata_via_ytdlp(self, url: str) -> Tuple[Optional[str], Optional[str], Optional[str], Optional[float], Optional[str], Optional[str]]: ydl_opts = { 'skip_download': True, 'quiet': True, 'no_warnings': True, } try: with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(url, download=False) title = info.get('title') channel = info.get('uploader') or info.get('channel') upload_date = info.get('upload_date') # YYYYMMDD duration = info.get('duration') # seconds (float/int) description = info.get('description') webpage_url = info.get('webpage_url') or url
# Format upload_date if available if upload_date and len(upload_date) == 8: upload_date = f"{upload_date[:4]}-{upload_date[4:6]}-{upload_date[6:]}"
return title, channel, upload_date, float(duration) if duration else None, webpage_url, description except Exception as e: logger.warning(f"Could not extract metadata via yt-dlp: {e}") return None, None, None, None, None, None
def parse(self, url: str) -> Tuple[str, dict]: video_id = self._extract_video_id(url) if not video_id: raise ValueError(f"Could not extract YouTube video ID from URL: {url}")
logger.info(f"Extracting metadata for YouTube video: {video_id}") title, channel, upload_date, duration, webpage_url, description = self._extract_metadata_via_ytdlp(url)
logger.info(f"Fetching transcript for YouTube video: {video_id}") try: # Try fetching transcript directly transcript_list = YouTubeTranscriptApi.list_transcripts(video_id)
# Prefer manually created transcripts, fall back to generated try: transcript = transcript_list.find_manually_created_transcript(['en', 'ru', 'es', 'de', 'fr']) except NoTranscriptFound: transcript = transcript_list.find_generated_transcript(['en', 'ru', 'es', 'de', 'fr'])
data = transcript.fetch() except Exception as e: logger.warning(f"youtube-transcript-api failed ({e}), attempting fallback via yt-dlp...") # Fallback to yt-dlp subtitles ydl_opts = { 'skip_download': True, 'writesubtitles': True, 'writeautomaticsub': True, 'subtitleslangs': ['en', 'ru', 'es', 'de', 'fr'], 'quiet': True, } with yt_dlp.YoutubeDL(ydl_opts) as ydl: info = ydl.extract_info(url, download=False) subtitles = info.get('subtitles') or info.get('automatic_captions') if not subtitles: raise RuntimeError("No transcript or subtitles found for this video.")
# Pick the first available language from our preferred list or whatever is there lang = next((l for l in ['en', 'ru', 'es', 'de', 'fr'] if l in subtitles), list(subtitles.keys())[0]) sub_formats = subtitles[lang] json3_sub = next((s['url'] for s in sub_formats if s.get('ext') == 'json3'), None)
if json3_sub: import requests resp = requests.get(json3_sub) resp.raise_for_status() sub_data = resp.json() data = [] for event in sub_data.get('events', []): if 'segs' in event: text = "".join(s.get('utf8', '') for s in event['segs']).strip() if text and text!= '\n': start = event.get('tStartMs', 0) / 1000.0 duration_s = event.get('dDurationMs', 0) / 1000.0 data.append({'start': start, 'duration': duration_s, 'text': text}) else: raise RuntimeError("Could not find suitable transcript format in subtitles.")
# Format transcript with timestamps formatted_lines = [] for entry in data: start = entry.get('start', 0) text = entry.get('text', '').replace('\n', ' ').strip() if not text: continue
# Convert seconds to [HH:MM:SS] hours = int(start // 3600) minutes = int((start % 3600) // 60) seconds = int(start % 60) timestamp = f"[{hours:02d}:{minutes:02d}:{seconds:02d}]"
formatted_lines.append(f"{timestamp} {text}")
transcript_text = "\n".join(formatted_lines)
metadata = { "title": title or f"YouTube Video ({video_id})", "channel": channel or "Unknown Channel", "published_date": upload_date or "Unknown Date", "duration": duration, "source_url": webpage_url or url, "platform": "YouTube", "video_id": video_id, "description": description or "" }
return transcript_text, metadata src/main.py import argparse import sys import os from dotenv import load_dotenv
from src.parsers.youtube import YouTubeParser from src.parsers.rss_podcast import RSSPodcastParser from src.parsers.audio_transcriber import AudioTranscriberParser from src.summarizer.pipeline import SummarizerPipeline from src.utils.logger import logger
def parse_args(): parser = argparse.ArgumentParser( description="Extract transcript and generate a detailed walkthrough of YouTube videos or Podcasts." ) parser.add_argument( "url", type=str, help="URL of the YouTube video, RSS feed, audio file (mp3), or direct podcast link." ) parser.add_argument( "-o", "--output", type=str, default=None, help="Path to the output Markdown file (default: auto-generated based on title in outputs/)." ) parser.add_argument( "--whisper-model", type=str, default=None, help="Whisper model size for audio transcription if required (e.g., tiny, base, small, medium, large)." ) parser.add_argument( "--gemini-model", type=str, default="gemini-2.5-pro", help="Google Gemini model to use for summarization (default: gemini-2.5-pro)." ) return parser.parse_args()
def main(): load_dotenv() args = parse_args()
url = args.url logger.info(f"Processing URL: {url}")
# 1. Select appropriate parser parser = None if YouTubeParser.can_handle(url): logger.info("Selected parser: YouTubeParser") parser = YouTubeParser() elif RSSPodcastParser.can_handle(url): logger.info("Selected parser: RSSPodcastParser") parser = RSSPodcastParser() elif AudioTranscriberParser.can_handle(url): logger.info("Selected parser: AudioTranscriberParser") parser = AudioTranscriberParser(model_size=args.whisper_model or "base") else: logger.error("No suitable parser found for this URL.") sys.exit(1)
try: transcript, metadata = parser.parse(url) except Exception as e: logger.error(f"Failed to extract transcript: {e}", exc_info=True) sys.exit(1)
if not transcript or not transcript.strip(): logger.error("Extracted transcript is empty. Aborting.") sys.exit(1)
logger.info(f"Extracted transcript ({len(transcript)} chars). Starting summarization pipeline...")
# 2. Run summarizer pipeline try: pipeline = SummarizerPipeline(model_name=args.gemini_model) walkthrough_md = pipeline.run(transcript, metadata) except Exception as e: logger.error(f"Summarization pipeline failed: {e}", exc_info=True) sys.exit(1)
# 3. Save output output_path = args.output if not output_path: os.makedirs("outputs", exist_ok=True) # Clean title for filename clean_title = "".join(c for c in metadata.get('title', 'summary') if c.isalnum() or c in (' ', '-', '_')).rstrip() clean_title = clean_title.replace(' ', '_').lower()[:60] output_path = os.path.join("outputs", f"{clean_title}.md")
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True) with open(output_path, "w", encoding="utf-8") as f: f.write(walkthrough_md)
logger.info(f"Success! Detailed walkthrough saved to: {output_path} ")
if __name__ == "__main__": main() src/summarizer/pipeline.py
for entry in entries: if entry.startswith('['): time_part = entry.split(']')[0] + ']' name_part = entry.split(']')[-1].strip() speaker_name_map[time_part] = name_part elif ':' in entry: parts = entry.split(':', 1) speaker_name_map[parts[0].strip()] = parts[1].strip() logger.info(f"Extracted speaker profiles: {len(speaker_name_map)} speakers found.") except Exception as e: logger.warning(f"Speaker identification step failed (non-critical): {e}")
# Step 2: Split into chunks chunks = chunk_transcript_by_time(transcript, chunk_duration_minutes=self.chunk_duration_minutes) total_parts = len(chunks) logger.info(f"Transcript divided into {total_parts} part(s) of ~{self.chunk_duration_minutes} min each.")
# Step 3: Process each chunk sub_walkthroughs = [] for i, chunk_text in enumerate(chunks, 1): logger.info(f"Generating walkthrough for part {i} of {total_parts}...")
prompt = self._build_chunk_prompt( part_num=i, total_parts=total_parts, chunk_transcript=chunk_text, metadata=metadata, speaker_map=speaker_name_map )
resp = self._call_model_with_backoff(prompt) cleaned_text = sanitize_markdown(resp.text.strip()) sub_walkthroughs.append(cleaned_text)
# Step 4: Generate Executive Summary & Synthesis logger.info("Generating executive summary and synthesis...") combined_text = "\n\n".join(sub_walkthroughs) synthesis_prompt = self._build_synthesis_prompt(combined_text, metadata) synth_resp = self._call_model_with_backoff(synthesis_prompt) exec_summary = sanitize_markdown(synth_resp.text.strip())
# Step 5: Format Final Document final_doc = self._assemble_final_document(metadata, exec_summary, sub_walkthroughs) return final_doc
def _call_model_with_backoff(self, prompt: str, max_retries: int = 5) -> any: delay = 5.0 for attempt in range(1, max_retries + 1): try: response = self.client.models.generate_content( model=self.model_name, contents=prompt ) return response except Exception as e: logger.warning(f"Gemini API call failed (attempt {attempt}/{max_retries}): {e}") if attempt == max_retries: raise time.sleep(delay) delay *= 2
def _build_speaker_id_prompt(self, transcript_sample: str, metadata: dict) -> str: return f"""You are analyzing a podcast or video transcript to identify the names and roles of the speakers.
METADATA: Title: {metadata.get('title', 'Unknown')} Channel/Host: {metadata.get('channel', 'Unknown')} Description: {metadata.get('description', '')[:1000]}
TRANSCRIPT EXCERPT: {transcript_sample}
TASK: Identify who is speaking. Look at introductions, greetings, self-references, host opening lines, and names mentioned. Return a bullet list formatted exactly like: - [Speaker Name]: [Brief Role / Description]
If you cannot identify names with certainty, list the likely roles (e.g. Host, Guest, Co-host). Keep it brief."""
def _build_chunk_prompt(self, part_num: int, total_parts: int, chunk_transcript: str, metadata: dict, speaker_map: dict) -> str: # Format page background desc = metadata.get('description', '').strip() dur_str = "" if metadata.get('duration'): dur_m = int(metadata['duration'] // 60) dur_s = int(metadata['duration'] % 60) dur_str = f"{dur_m:02d}:{dur_s:02d}"
speakers_info = "" if speaker_map: speakers_info = "\nKnown speakers in this episode:\n" + "\n".join(f"- {k}: {v}" for k, v in speaker_map.items())
background = f"""- Title: {metadata.get('title', 'Unknown')} - Platform: {metadata.get('platform', 'Unknown')} - Channel / author: {metadata.get('channel', 'Unknown')} - Published: {metadata.get('published_date', 'Unknown')} - Duration: {dur_str or 'Unknown'} - Source URL: {metadata.get('source_url', '')} - Show notes / description: {desc[:3000]} {speakers_info}"""
prompt = f"""## Page background (title, channel, and description — use it to fix transcription errors) {background}
Below is part {part_num} of {total_parts} of a transcript. Each line is prefixed with its [HH:MM:SS] timestamp. It is auto-generated (speech-to-text), so expect recognition errors.
Write a detailed, faithful walkthrough of THIS part as Markdown: - The transcript may mis-hear words. Using the page background above and common knowledge, silently correct obvious transcription errors — especially names, companies, products, places, and technical terms — so they are correct and consistent. Do NOT change the actual meaning or invent content; when unsure, keep the original. - Break it into one or more subsections, each starting with a heading of the form `### [HH:MM:SS] <short descriptive heading>` using the timestamp where that topic begins. - Under each heading, write 1-3 paragraphs of flowing prose that capture what is said in depth: the specific details, numbers, names, and examples, AND the mood and the way it is discussed (stories, jokes, debates, emotional beats). - After the prose, optionally add `- ` bullets for extra concrete data points, lists, or facts that deserve to stand alone. - When a visual architecture or flow is important, use a fenced Mermaid diagram beginning with ` ```mermaid ` and a simple `flowchart LR` or `flowchart TD`. Keep node IDs short, put human-readable labels in quoted brackets such as `A["Meeting transcript"]`, and keep each edge on its own line. Use an `accTitle:` line for accessibility. If you add an accessibility description, the only valid directive is `accDescr:` (never `accDescription:`). If an edge label contains parentheses or brackets, wrap that label in double quotes, for example `A -->|"step (one)"| B`. Keep the layout compact enough to fit in one viewport: group related stages into labeled subgraphs and avoid a single uninterrupted row or column longer than four nodes. Never draw boxes, arrows, or tables with ASCII characters in a plain code fence. For genuinely tabular comparisons, use a standard Markdown table instead. If neither format can express the source cleanly, explain it in prose. - Quote the speakers directly. When a speaker states a view, judgement, prediction, or lesson, quote the WHOLE sentence verbatim inside “quotation marks” and say who said it, rather than clipping a 2-4 word fragment of it. In every subsection where someone argues a point, include at least one such full-sentence quote, choosing the line where they put it most sharply. Only the speakers' actual words go inside quotation marks; never put your own paraphrase or summary inside them. - IMPORTANT: Put a [HH:MM:SS] timestamp ONLY on the section headings. Do NOT add timestamps inside the prose sentences or bullets — the prose must read cleanly with no inline timestamps. Start a new `###` subsection when a clearly distinct new topic begins (roughly every 1-3 minutes), rather than timestamping sentences. - Be thorough. Do not skip substantive content. Do NOT write an overall title or any preamble/conclusion about the whole video — only cover this part. - LANGUAGE: Write all text fields in the SAME language as the transcript/source.
TRANSCRIPT PART: {chunk_transcript}""" return prompt
def _build_synthesis_prompt(self, combined_subsections: str, metadata: dict) -> str: return f"""You are given the complete set of detailed section walkthroughs for the episode "{metadata.get('title')}".
Write an Executive Overview and Synthesis to precede the walkthrough: 1. Executive Summary: 2-3 substantive paragraphs outlining the core thesis, the main narrative arc, and the most important conclusions of the episode. 2. Key Takeaways & Milestones: 4-7 bullet points capturing the most critical lessons, technical breakthroughs, metrics, or takeaways discussed.
Respond in the SAME language as the content. Do not include a document title header (like # Title), as that will be added automatically.
DETAILED SECTIONS CONTENT: {combined_subsections[:30000]}"""
def _assemble_final_document(self, metadata: dict, exec_summary: str, subsections: List[str]) -> str: dur_str = "" if metadata.get('duration'): dur_m = int(metadata['duration'] // 60) dur_s = int(metadata['duration'] % 60) dur_str = f"{dur_m:02d}:{dur_s:02d}"
header_lines = [ f"# {metadata.get('title', 'Detailed Episode Walkthrough')}\n", f"**Channel / Host:** {metadata.get('channel', 'Unknown')} ", f"**Platform:** {metadata.get('platform', 'Unknown')} ", f"**Published Date:** {metadata.get('published_date', 'Unknown')} ", f"**Duration:** {dur_str or 'Unknown'} ", f"**Source URL:** [{metadata.get('source_url', 'Link')}]({metadata.get('source_url', '#')})\n", "---\n", "## Executive Overview & Synthesis\n", exec_summary, "\n---\n", "## Detailed Walkthrough\n" ]
walkthrough_content = "\n\n".join(subsections) return "\n".join(header_lines) + "\n" + walkthrough_content + "\n" import os import time from typing import List, Tuple, Optional from google import genai from src.utils.logger import logger from src.utils.time_chunker import chunk_transcript_by_time from src.utils.text_sanitizer import sanitize_markdown
class SummarizerPipeline: def __init__(self, model_name: str = "gemini-2.5-pro", chunk_duration_minutes: int = 10): self.model_name = model_name self.chunk_duration_minutes = chunk_duration_minutes
api_key = os.getenv("GEMINI_API_KEY") if not api_key: raise ValueError("GEMINI_API_KEY environment variable is missing.")
self.client = genai.Client(api_key=api_key)
def run(self, transcript: str, metadata: dict) -> str: # Step 1: Detect speakers from metadata and initial transcript lines logger.info("Detecting speakers and background context...") speaker_name_map = {} try: head_transcript = "\n".join(transcript.splitlines()[:60]) speaker_prompt = self._build_speaker_id_prompt(head_transcript, metadata) resp = self._call_model_with_backoff(speaker_prompt) if resp and resp.text: entries = [line.strip('- *') for line in resp.text.splitlines() if line.strip().startswith(('-', '*'))] src/parsers/audio_transcriber.py import os import tempfile import urllib.request from typing import Tuple from src.parsers.base import BaseParser from src.utils.logger import logger
class AudioTranscriberParser(BaseParser): def __init__(self, model_size: str = "base"): self.model_size = model_size
@classmethod def can_handle(cls, url: str) -> bool: # Handles direct mp3, m4a, wav, ogg, or local files audio_extensions = ('.mp3', '.m4a', '.wav', '.ogg', '.aac', '.flac') return any(url.lower().split('?')[0].endswith(ext) for ext in audio_extensions) or os.path.isfile(url)
def parse(self, url: str) -> Tuple[str, dict]: temp_audio_path = None if os.path.isfile(url): audio_path = url filename = os.path.basename(url) else: logger.info(f"Downloading audio file from: {url}") with tempfile.NamedTemporaryFile(suffix=".mp3", delete=False) as f: temp_audio_path = f.name
# Simple download with urllib req = urllib.request.Request( url, headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)'} ) with urllib.request.urlopen(req) as response, open(temp_audio_path, 'wb') as out_file: out_file.write(response.read()) audio_path = temp_audio_path filename = os.path.basename(url.split('?')[0])
try: logger.info(f"Transcribing audio with faster-whisper (model={self.model_size})...") from faster_whisper import WhisperModel
model = WhisperModel(self.model_size, device="auto", compute_type="auto") segments, info = model.transcribe(audio_path, beam_size=5)
formatted_lines = [] for segment in segments: start = segment.start text = segment.text.strip() if not text: continue
hours = int(start // 3600) minutes = int((start % 3600) // 60) seconds = int(start % 60) timestamp = f"[{hours:02d}:{minutes:02d}:{seconds:02d}]" formatted_lines.append(f"{timestamp} {text}")
transcript_text = "\n".join(formatted_lines) metadata = { "title": filename, "channel": "Unknown Channel", "published_date": "Unknown Date", "duration": info.duration if hasattr(info, 'duration') else None, "source_url": url, "platform": "Audio File", "language": info.language if hasattr(info, 'language') else "unknown" } return transcript_text, metadata
finally: if temp_audio_path and os.path.exists(temp_audio_path): os.remove(temp_audio_path) tests/test_audio_transcriber.py
def test_audio_transcriber_can_handle_invalid(): assert not AudioTranscriberParser.can_handle("https://example.com/article.html") assert not AudioTranscriberParser.can_handle("https://www.youtube.com/watch?v=123") import pytest from src.parsers.audio_transcriber import AudioTranscriberParser
def test_audio_transcriber_can_handle(): assert AudioTranscriberParser.can_handle("https://example.com/audio.mp3") assert AudioTranscriberParser.can_handle("https://example.com/audio.mp3?dest-id=123") assert AudioTranscriberParser.can_handle("https://example.com/podcast.m4a") assert AudioTranscriberParser.can_handle("https://example.com/podcast.wav") assert AudioTranscriberParser.can_handle("https://example.com/podcast.ogg")
# Test local file handling import tempfile with tempfile.NamedTemporaryFile(suffix=".mp3") as tmp: assert AudioTranscriberParser.can_handle(tmp.name) tests/test_youtube.py import pytest from src.parsers.youtube import YouTubeParser
def test_youtube_can_handle(): urls = [ "https://www.youtube.com/watch?v=dQw4w9WgXcQ", "https://youtu.be/dQw4w9WgXcQ", "https://youtube.com/embed/dQw4w9WgXcQ", "http://www.youtube.com/watch?v=dQw4w9WgXcQ&t=10s" ] for url in urls: assert YouTubeParser.can_handle(url) is True
def test_youtube_can_handle_invalid(): urls = [ "https://vimeo.com/123456", "https://example.com", "https://podcasts.apple.com/us/podcast/id1234" ] for url in urls: assert YouTubeParser.can_handle(url) is False
def test_extract_video_id(): parser = YouTubeParser() assert parser._extract_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ") == "dQw4w9WgXcQ" assert parser._extract_video_id("https://youtu.be/dQw4w9WgXcQ") == "dQw4w9WgXcQ" assert parser._extract_video_id("https://www.youtube.com/watch?v=dQw4w9WgXcQ&feature=share") == "dQw4w9WgXcQ"
def test_extract_video_id_invalid(): parser = YouTubeParser() assert parser._extract_video_id("https://example.com") is None src/utils/time_chunker.py import re from typing import List
def parse_timestamp_seconds(timestamp_str: str) -> float: """Parses [HH:MM:SS] or [MM:SS] into total seconds.""" clean = timestamp_str.strip('[]') parts = list(map(int, clean.split(':'))) if len(parts) == 3: return parts[0] * 3600 + parts[1] * 60 + parts[2] elif len(parts) == 2: return parts[0] * 60 + parts[1] return 0.0
def chunk_transcript_by_time(transcript: str, chunk_duration_minutes: int = 10) -> List[str]: """ Chunks a timestamped transcript by time into segments of approximately `chunk_duration_minutes` (e.g. 10 minutes). Ensures that chunks split cleanly at timestamps. """ lines = transcript.splitlines() if not lines: return []
target_chunk_seconds = chunk_duration_minutes * 60 chunks = [] current_chunk = []
current_chunk_start_sec = 0.0
# Regex for [HH:MM:SS] or [MM:SS] at the start of a line ts_pattern = re.compile(r'^\[(\d{1,2}:\d{2}(?::\d{2})?)\]')
for line in lines: match = ts_pattern.match(line) if match: current_sec = parse_timestamp_seconds(match.group(1))
# If this is the very first timestamp, set the baseline start if not current_chunk: current_chunk_start_sec = current_sec
# Check if duration has exceeded the target chunk duration if (current_sec - current_chunk_start_sec) >= target_chunk_seconds and len(current_chunk) > 10: chunks.append("\n".join(current_chunk)) current_chunk = [] current_chunk_start_sec = current_sec
current_chunk.append(line)
if current_chunk: chunks.append("\n".join(current_chunk))
return chunks tests/test_text_sanitizer.py
def test_sanitize_markdown_quotes(): raw = 'He said, \\"Hello world!\\"' expected = 'He said, "Hello world!"' assert sanitize_markdown(raw) == expected
def test_sanitize_markdown_escaped_parens(): raw = 'This is an example \\(with parens\\).' expected = 'This is an example (with parens).' assert sanitize_markdown(raw) == expected
def test_sanitize_markdown_escaped_dashes_and_dots(): raw = 'item \\- one\\. item \\- two\\.' expected = 'item - one. item - two.' assert sanitize_markdown(raw) == expected
def test_sanitize_empty_and_none(): assert sanitize_markdown("") == "" assert sanitize_markdown(None) is None from src.utils.text_sanitizer import sanitize_markdown
def test_sanitize_markdown_apostrophe(): raw = "Meta\\'s team works on WhatsApp\\'s encryption and didn\\'t give up." expected = "Meta's team works on WhatsApp's encryption and didn't give up." assert sanitize_markdown(raw) == expec ted src/parsers/rss_podcast.py
if not matching_entry: raise ValueError(f"Could not find matching episode in feed: {feed_url} for url: {url}")
title = matching_entry.get('title', 'Unknown Episode') author = matching_entry.get('author') or feed.feed.get('author') or feed.feed.get('title') published = matching_entry.get('published') or matching_entry.get('updated') description = matching_entry.get('summary') or matching_entry.get('description') or ''
# Strip HTML tags from description if present clean_desc = re.sub(r'<[^>]+>', ' ', description).strip() clean_desc = re.sub(r'\s+', ' ', clean_desc)
metadata = { "title": title, "channel": author or "Unknown Podcast", "published_date": published or "Unknown Date", "source_url": url, "platform": "Podcast / RSS", "description": clean_desc }
# Check for transcript tag (podcast namespace standard: <podcast:transcript url="..." type="..." />) transcript_url = None for tag in matching_entry.get('podcast_transcript', []): transcript_url = tag.get('url') break if not transcript_url and 'podcast_transcripts' in matching_entry: transcript_url = matching_entry['podcast_transcripts'][0].get('url')
if transcript_url: logger.info(f"Found dedicated transcript URL: {transcript_url}") return self._fetch_and_parse_transcript_file(transcript_url, metadata)
# If no transcript URL, find audio enclosure and transcribe audio_url = None for enc in matching_entry.get('enclosures', []): if 'audio' in enc.get('type', '') or enc.get('href', '').split('?')[0].endswith(('.mp3', '.m4a')): audio_url = enc.get('href') break
if not audio_url: raise RuntimeError(f"No transcript or audio enclosure found for episode: {title}")
logger.info(f"No direct transcript found in RSS feed. Transcribing audio file: {audio_url}") transcriber = AudioTranscriberParser() transcript_text, audio_meta = transcriber.parse(audio_url)
metadata["duration"] = audio_meta.get("duration") return transcript_text, metadata
def _fetch_and_parse_transcript_file(self, transcript_url: str, metadata: dict) -> Tuple[str, dict]: resp = requests.get(transcript_url) resp.raise_for_status() content = resp.text
# Check format (JSON, SRT, VTT, or plain text) lines = [] if transcript_url.endswith('.json') or content.strip().startswith('{'): try: import json data = json.loads(content) segments = data.get('segments') or data.get('results', {}).get('segments', []) for seg in segments: start = seg.get('start', 0) text = seg.get('text', '').strip() hours = int(start // 3600) minutes = int((start % 3600) // 60) seconds = int(start % 60) timestamp = f"[{hours:02d}:{minutes:02d}:{seconds:02d}]" lines.append(f"{timestamp} {text}") return "\n".join(lines), metadata except Exception as e: logger.warning(f"Failed parsing JSON transcript: {e}")
# Fallback to lines parsing for SRT/VTT/Text # Simple extraction of timestamps # Matches 00:00:00 or 00:00 for line in content.splitlines(): clean = line.strip() if not clean or clean.isdigit(): continue lines.append(clean)
return "\n".join(lines), metadata import feedparser import requests import re from typing import Tuple, Optional from src.parsers.base import BaseParser from src.parsers.audio_transcriber import AudioTranscriberParser from src.utils.logger import logger
class RSSPodcastParser(BaseParser): @classmethod def can_handle(cls, url: str) -> bool: # Check if URL ends with xml, rss, or feed, or contains podcast domains lower = url.lower() return ( lower.endswith(('.xml', '.rss', '/feed', '/rss')) or 'feeds.' in lower or 'feed.' in lower or 'anchor.fm' in lower or 'podcasts.apple.com' in lower or 'libsyn.com' in lower )
def parse(self, url: str) -> Tuple[str, dict]: logger.info(f"Parsing RSS / Podcast feed or episode: {url}")
# If it's an Apple podcast or specific episode link, we might parse the feed or resolve redirect feed_url = url feed = feedparser.parse(feed_url)
if not feed.entries: raise ValueError(f"Could not parse RSS feed or found 0 entries at {url}")
# Select the latest episode or find the episode matching the URL matching_entry = None for entry in feed.entries: # Check enclosure or link enclosures = [e.href for e in entry.get('enclosures', []) if 'href' in e] if url in enclosures or url == entry.get('link'): matching_entry = entry break
# If no specific episode matched (e.g. user provided whole feed URL), take the latest entry if not matching_entry: matching_entry = feed.entries[0] logger.info(f"No specific episode matched URL. Defaulting to latest episode: '{matching_entry.get('title')}'") src/parsers/base.py
@abstractmethod def parse(self, url: str) -> Tuple[str, dict]: """ Parses the URL and returns: 1. transcript: Text with formatted [HH:MM:SS] timestamps on each line/segment. 2. metadata: Dictionary with title, channel/author, published_date, duration, description, etc. """ pass from abc import ABC, abstractmethod from typing import Tuple, Optional
class BaseParser(ABC): @classmethod @abstractmethod def can_handle(cls, url: str) -> bool: """Returns True if this parser can handle the provided URL.""" pass
`20:00` Scaling End-to-End Encryption and "Rolling Your Own Crypto"
When designing end-to-end encrypted messaging systems, the sheer volume of active user endpoints alters the underlying mathematics significantly. In simple systems where each participant possesses only a few hardware units—such as a thread among two or three people with three or four devices each, totaling 12 to 20 endpoints—encrypting directly to each device endpoint is relatively straightforward. However, Emma Connor points out that some real-world Messenger accounts have up to 100 registered devices. In large group chats, this multiplier can cause the total number of connected endpoints to skyrocket into hundreds or thousands.
Off-the-shelf protocols like standard Signal face severe scaling hurdles under these constraints. Pascal notes that group conversations can escalate quickly, evoking WhatsApp's pairing mechanisms. Emma confirms that while some systems employ sender keys to achieve an order of $O(N)$ keys for $N$ users, the pure Signal protocol can require an order of $O(N^2)$ connections. For 100 endpoints, that translates to approximately 10,000 distinct connections. To bypass this combinatorial explosion, Meta's Labyrinth 1.1 architecture shifts to a single "mailbox-level public key" per user. When two individuals communicate in a thread, the message payload is encrypted strictly between the two user-level public keys rather than fanning out across every single individual device key.
This structural divergence meant existing off-the-shelf options were insufficient, forcing the engineering team to construct their own protocols. While the standard industry mantra warns never to roll your own crypto, Emma clarifies their stance: > “Well, not the primitives, but at least the packaging of it. Right. So that's what we did. And I know that there of course the classic warning of never roll your own crypto but somebody has to And when we do we do it with cryptographic experts We do it with input from external researchers and community feedback and transparency”
Because Meta houses extensive internal security expertise, the Labyrinth team worked alongside specialists across the company and the broader cryptography community: - Internal Meta chat channels populated by end-to-end encryption specialists, veterans of WhatsApp's end-to-end rollout, and engineers who previously worked with the Electronic Frontier Foundation (EFF). - Collaboration with external and industry figures, including past work with Moxie Marlinspike. - Direct work alongside cryptographic experts like Shai Halevi (who authored widely adopted internet standards for encryption algorithms) and Kevin Lewi. - Formal security reviews and consultations conducted with independent researchers from the external privacy and security community.
---
`23:25` Device Revocation Guarantees and Epoch Keys
Having extensive multi-device support introduces the reality that devices will eventually be misplaced, stolen, or compromised. When a user revokes access for an old or missing hardware unit, the backup system must provide strict cryptographic boundaries rather than relying on server-side promises. Emma explains that the security guarantee specifically targets post-revocation confidentiality: > “If you revoke a device, that means the keys that exist on that device should no longer be able to decrypt any messages that are sent after that point.”
The system explicitly does not offer backwards revocation; it does not rewrite history or retroactively re-encrypt historical messages stored in earlier backups. However, the forward guarantee holds under adversarial conditions: even if a revoked device ends up in the possession of an attacker who is also a Meta insider with unrestricted access to Meta data centers, that actor still cannot read any messages transmitted after the revocation cutoff.
```mermaid flowchart TD accTitle: Device Revocation and Epoch Key Derivation Flow accDescr: An active device generates new entropy, encrypts it to valid devices, and isolates the revoked device from deriving the new epoch key.
subgraph ActiveDevices["User's Trusted Devices"] D1["Active Device A"] D2["Active Device B"] end
subgraph RevokedGroup["Untrusted Hardware"] D3["Revoked Device C (Lost/Stolen)"] end
D1 -->|"1. Generates new entropy"| D1 D1 -->|"2. Derives"| K1["Epoch Key N+1"] D1 -->|"3. Encrypts entropy to Device B's public key"| D2 D2 -->|"4. Derives identical"| K1
D1 -.->|"Blocked / Excluded from key share"| D3 D3 -.->|"Cannot derive key (Cannot decrypt post-revocation data)"| K1 ```
To enforce this boundary without re-encrypting the archive, Labyrinth relies on "epoch keys": - **Epoch Definition:** An epoch represents a discrete window of time defining the exact cohort of authorized devices permitted to read messages. - **Entropy Generation:** When an authorized device triggers a revocation, it generates a fresh pool of entropy rather than sharing a finalized key directly. - **End-to-End Key Exchange:** The originating device encrypts this entropy specifically to the individual public-private key pairs of the remaining, trusted devices via an end-to-end encrypted channel. - **Isolation:** Because the revoked device never receives this encrypted entropy, it is mathematically incapable of deriving the subsequent epoch key used for encrypting new incoming traffic.
---
`26:12` Architectural Shift: Labyrinth 1.0 vs. Labyrinth 1.1
The major operational upgrade in Labyrinth 1.1 revolves around an inversion of the backup pipeline. In the legacy Labyrinth 1.0 architecture, users depended entirely on their own active client hardware to maintain server archives:
1. **Sender Encryption:** Alice encrypted a message payload directly addressed to Bob's specific hardware endpoint. 2. **Download & Decryption:** Bob's phone had to connect to the network, receive the payload, and decrypt it. 3. **Re-encryption for Archive:** Bob's phone re-encrypted the decrypted message content into an alternative format explicitly compatible with the Labyrinth encrypted storage system. 4. **Server Ingestion:** Bob's local device uploaded this newly generated encrypted archive entry back up to the server-side Labyrinth mailbox.
While functional, Labyrinth 1.0 created severe synchronization bottlenecks. If Bob's phone remained offline or powered down when Alice sent her message, the communication was never uploaded to the Labyrinth backup. If Bob subsequently logged into Messenger via a desktop browser at an internet cafe before turning his primary phone back on, the offline messages were entirely absent from his conversation view.
Labyrinth 1.1 resolves this limitation by transitioning from a predominantly symmetric key layout to an asymmetric, public-key architecture. Instead of waiting for a recipient's device to ingest, decrypt, and re-upload the message: > “The new backup model in Labyrinth 1.1 allows senders to encrypt the message to a public key and put the message in your backup on the server, without your device needing to download it.”
In Labyrinth 1.0, data at rest within a backup was secured with symmetric keys held locally across the user's fleet of personal devices. Labyrinth 1.1 incorporates public-key cryptography directly into the ingestion process, allowing a sender to encrypt directly to the recipient's backup mailbox on the server.
This architectural shift introduces a classic identity distribution problem: the sender's client must reliably verify that the mailbox public key belongs to the legitimate recipient and has not been forged or swapped by intermediate servers. While users can manually audit contact keys by opening "secure thread settings" inside the UI and comparing cryptographic fingerprints in person, manual fingerprint comparison is too high-friction for everyday use. To automate that verification without forcing users to compare strings of key material, the system implements automated Key Transparency.
`30:00` Key Transparency and Verifiable Ledgers
Emma Connor explains how users can trust that the public keys they receive actually belong to the intended contacts without relying blindly on Meta. Users publish their public keys to a shared directory maintained as a cryptographic, append-only ledger. Once an entry is written into this ledger, it cannot be deleted, and any attempt at tampering is immediately detectable. To establish an independent root of trust, Meta takes a cryptographic hash of this ledger roughly every ten minutes and transmits it to third-party auditors, primarily Cloudflare, who sign the hash.
This third-party attestation proves that there is only one canonical directory, ruling out split-view attacks, duplicate entries, or retroactive history rewrites. Client applications retrieve the external signature, verify that it covers the directory, and ensure that the recipient's public key matches the entry recorded on the ledger. As Emma puts it: “We have a signature from an outside party attesting to the fact that this is the one directory this directory is a source of truth.” Pascal points out how deep and exciting cryptography can get, noting that listeners can check out an earlier dedicated episode on WhatsApp’s key transparency infrastructure for further details.
```mermaid flowchart TD accTitle: Key Transparency Verification Flow accDescr: Shows how user public keys are committed to an append-only ledger and validated using external third-party signatures.
subgraph Meta ["Meta Infrastructure"] PK["User Public Keys"] --> Ledger["Append-Only Ledger"] Ledger --> Hash["Ledger Hash (Every ~10 mins)"] end
subgraph ThirdParty ["External Verification"] Hash --> CF["Cloudflare / Auditors"] CF --> Sig["Cryptographic Signature"] end
subgraph Client ["User Device"] Sig --> Verify["Verify Signature & Directory Entry"] end ```
`31:37` Post-Quantum Cryptography and the Hawk Hybrid Scheme
Turning to long-term risks, Pascal asks how the team prepares for quantum computing threats. Emma explains that the primary danger today is not that an adversary secretly possesses an operational quantum machine, but rather the threat of "store now, decrypt later" attacks, where adversaries harvest encrypted traffic today with the intention of cracking it when capable hardware emerges in a decade or two. Because Labyrinth acts as a persistent message storage system, Emma jokes that adversaries do not even need to run storage operations themselves: “When it comes to labyrinths, we are especially concerned about the possibility of a store now decrypt later because they—we're doing all the storage for you. That's kind of the point, right? So we're doing all of the storage now. You know, like if a bad actor comes along, they don't even need to be storing the data today, we're already doing it. It's kind of just a decrypt later attack.”
In Labyrinth 1.0, the post-quantum story was simpler because symmetric cryptography is naturally resilient against quantum attacks, and asymmetric exchanges were confined to sharing entropy rather than direct keys. However, Labyrinth 1.1 required a comprehensive migration to asymmetric post-quantum security. From the earliest design documents, Labyrinth 1.1 incorporated a custom hybrid cryptosystem named Hawk. Hawk blends classical and quantum-resistant algorithms: - **Post-Quantum Confidentiality**: Encrypts data against an attacker who might compromise either Diffie-Hellman or ML-KEM (Module-Lattice-Based Key-Encapsulation Mechanism), ensuring that confidentiality holds as long as both are not broken. - **Classical Authentication**: Leverages standard Diffie-Hellman to authenticate identities.
This split is intentional: authentication does not suffer from "store now, decrypt later" hazards because attackers cannot retroactively impersonate past senders once the live session is concluded. Emma highlights the efficiency and security payoff of this structure: “So we have the efficiency of a classical authentication and then the security of a post-quantum confidentiality. So the data is protected from quantum attacks even in the future.”
`35:28` Outro and Reflections on Defeating Your Own Power
Pascal remarks that it would be profoundly anticlimactic to spend years engineering rock-solid storage only to have a quantum machine render it completely vulnerable down the road. Emma agrees, emphasizing that when personal conversations are stored with the goal of keeping them accessible to users 10 to 20 years into the future, long-term post-quantum defenses are non-negotiable. Pascal then wraps up the interview, thanking Emma for granting him peace of mind whenever he is out kayaking on a lake with his phone sitting loosely in his pocket.
In his closing reflections, Pascal underscores the extreme engineering measures Meta took with their hardware security modules: writing the rate-limiting firmware, flashing it to the cards, and then deliberately physically destroying the smart cards required to upload new code. Pascal calls this an inspiring design philosophy: “A deliberate, irreversible act of locking yourself out, carried out so that nobody at Meta can ever quietly change the rules later. There's something quite admirable about engineering, but the goal is to reduce your own power.” He also revisits Emma’s perspective on the ancient developer maxim to "never roll your own crypto," observing that someone ultimately has to invent new tools, and the responsible way is to collaborate with specialists, seek transparent peer review, and publish the work openly.
- Meta released both a formal white paper detailing Labyrinth and an open-source Rust crate (`opaque-ke`). - Listeners can submit feedback via Threads and Instagram (`@metatechpod`) or email (`techpodcast@meta.com`), though Pascal jokingly begs people not to send GPG-signed emails requiring manual signature validation.
## Notable quotes > `05:00` Part of the problem we’re trying to solve is making that new mental model as small as possible compared to the existing mental model. — Emma Connor
> `23:25` If you revoke a device, that means the keys that exist on that device should no longer be able to decrypt any messages that are sent after that point. — Emma Connor
## Mentioned Cisco, Cloudflare, Diffie-Hellman, Electronic Frontier Foundation (EFF), Emma Connor, Facebook Messenger, Google Drive, Hawk, John Milliken, Kevin Lewi, Labyrinth, Labyrinth 1.0, Labyrinth 1.1, ML-KEM, Meta, Moxie Marlinspike, Network Time Protocol (NTP), Network Time Security (NTS), OPAQUE, Pascal, RSA, Rust, Secure Enclave, Shai Halevi, Signal, WhatsApp, iCloud, opaque-ke
## Video description <p dir="ltr">How do you add end-to-end encryption to a messenger that people have already been using for years, without taking away the message history they expect to be there? Emma Connor works on the team behind Labyrinth, Messenger's encrypted storage protocol. We get into where the encryption keys actually live, which turns out to be nowhere near Meta, and why a six-digit PIN is far harder to attack than six digits ought to be. Emma also explains the hardware security modules that Meta deliberately locked itself out of, what happens when you lose a device and want it cut out of your account, how key transparency lets you verify who you are talking to without having to trust us, and why Labyrinth 1.1 moved to post-quantum cryptography. All of this and more is discussed in this episode.</p> <p dir="ltr">Got feedback? Send it to us on Threads (<a href= "https://threads.net/@metatechpod">https://threads.net/@metatechpod</a>), Instagram (<a href= "https://instagram.com/metatechpod">https://instagram.com/metatechpod</a>) and don't forget to follow our host Pascal (<a href= "https://mastodon.social/@passy">https://mastodon.social/@passy</a>, <a href= "https://threads.net/@passy_">https://threads.net/@passy_</a>). Fancy working with us? Check out <a href= "https://www.metacareers.com/">https://www.metacareers.com/</a>.</p> <p dir="ltr"><strong>Links</strong></p> <ul> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Opaque Rust Crate - <a href= "https://crates.io/crates/opaque-ke">https://crates.io/crates/opaque-ke</a></p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Labyrinth 1.1: Making End-to-End Encrypted Backups Even More Reliable - <a href= "https://engineering.fb.com/2026/05/11/security/labyrinth-1-1-end-to-end-encrypted-e2ee-backups-more-reliable/"> https://engineering.fb.com/2026/05/11/security/labyrinth-1-1-end-to-end-encrypted-e2ee-backups-more-reliable/</a></p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">The Labyrinth Encrypted Message Storage Protocol - <a href= "https://engineering.fb.com/wp-content/uploads/2026/05/Minos-Updates-2026-Encrypted-Backups-White-Paper.pdf"> https://engineering.fb.com/wp-content/uploads/2026/05/Minos-Updates-2026-Encrypted-Backups-White-Paper.pdf</a></p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">MTP E54: Building Key Transparency at WhatsApp - <a href= "https://pca.st/orlsiq1l">https://pca.st/orlsiq1l</a></p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">MTP E65: Getting Ready for Post-Quantum Cryptography - <a href= "https://pca.st/4klppivy">https://pca.st/4klppivy</a></p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">NTS: Authenticated Time at Meta - <a href= "https://engineering.fb.com/2026/10/06/production-engineering/nts-authenticated-time-at-meta/"> https://engineering.fb.com/2026/10/06/production-engineering/nts-authenticated-time-at-meta/</a></p> </li> </ul> <p dir="ltr"><strong>Timestamps</strong></p> <ul> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Intro 0:06</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Introduction Emma 2:25</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">The Complexity of End-to-End Encryption 3:42</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">User Expectations and Mental Models 5:35</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">The Labyrinth System Overview 7:17</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Key Management and Security 7:54</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Rate-limiting passcode guesses 13:01</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Evolution of Backup Systems 16:45</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Rolling Out New Storage Protocols 17:07</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Ensuring Data Integrity During Migration 18:34</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Multi-Device Support Challenges 18:48</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Building Custom Cryptography Solutions 20:20</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Revoking Device Access 23:56</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Labyrinth 1.1: 26:24</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Preparing for Quantum Resistance 29:20</p> </li> <li dir="ltr" aria-level="1"> <p dir="ltr" role="presentation">Outro 36:16</p> </li> </ul> <p> </p>
[Source](https://traffic.libsyn.com/secure/insidefacebookmobile/mtp88_rc2.mp3?dest-id=697376)