Vitrina del código
Sin repositorio — 17 extractos del código fuente real, sin alterar
En la página de inicio se lee: «Cada afirmación aquí se puede comprobar línea por línea en el código.» Eso es verificable o es solo una frase — esto es la verificación. 17 puntos del código fuente real de HIY (Android, iOS, Relay), originales y con los comentarios originales, tal como están en el repositorio. Sin recortar, sin pulir.
Lo que se muestra es la criptografía: generación de claves, cifrado, derivación de claves, formato de firma, lógica de alcance. Esa es la parte que debe poder verificarse — es segura porque es conocida. Lo que deliberadamente no se muestra está al final de esta página.
Cifrado de extremo a extremo — el formato en el cable
Afirmación: «chats E2EE con X25519 + AES-256-GCM.»
/**
* Ende-zu-Ende-Verschlüsselung für "Nur Freunde"-Posts, siehe
* `docs/E2E_ENCRYPTION.md`. Nutzt das separate X25519-Schlüsselpaar aus
* `EncryptionIdentity` (NICHT den Signier-Schlüssel — getrennte Schlüssel für
* getrennte Zwecke). Pendant zu `E2E.swift` — X25519 ist ein einziger,
* unzweideutiger Standard (RFC 7748), Tinks Primitiv hier liefert exakt
* dieselben Bytes wie CryptoKit dort; HKDF-SHA256 mit leerem Salt ist wegen
* HMACs eigenem Zero-Padding kurzer Schlüssel ebenfalls plattformunabhängig
* identisch (ein 0-Byte- und ein 32-Byte-Null-Schlüssel ergeben nach dem
* Padding auf die Blockgrösse dasselbe Ergebnis).
*
* Wire-Format pro versiegeltem Blob (`encText`, und jeder Wert in `encKeys`):
* base64(Nonce[12] || Ciphertext || Tag[16]) — identisch zu CryptoKit's
* `AES.GCM.SealedBox.combined`.
*/
object E2E {
private val HKDF_INFO = "hiy.e2e.v1".toByteArray(Charsets.UTF_8)
private const val NONCE_LEN = 12
data class Sealed(val encText: String, val encKeys: Map<String, String>, val encBild: String? = null)
El formato en el cable está ahí, escrito en el comentario (en alemán — son las notas reales, sin traducir, del desarrollador). Quien lo conoce puede recalcular a mano un blob interceptado — eso es exactamente lo que hace verificable una afirmación. Curioso de paso: claves separadas para firmar y cifrar, con el motivo indicado.
PIN de respaldo — la derivación de clave
Afirmación: «solo se desbloquea con el PIN (600.000 rondas de PBKDF2).»
/**
* Verschlüsseltes Backup der Freundesliste — überlebt eine Neuinstallation/
* einen Gerätewechsel, anders als FriendsStore selbst (rein lokal,
* SharedPreferences). Der Relay sieht nie Klartext: der AES-Schlüssel kommt
* aus einer selbst gewählten PIN, nie vom Server — nur wer die PIN kennt,
* kann den Blob entschlüsseln, der Relay-Betreiber selbst nicht. Pendant zu
* FriendsBackup.swift.
*/
object FriendsBackup {
// OWASP-Empfehlung (2023+) für PBKDF2-HMAC-SHA256 ist 600k Runden — vorher
// 200k, zusammen mit der Mindestlänge von 10 Zeichen über die volle
// Tastatur (s. BackupPinDialog.kt) macht das Offline-Brute-Force gegen
// einen durchgesickerten Backup-Blob (liegt verschlüsselt auf dem Relay)
// praktisch aussichtslos.
private const val ITERATIONS = 600_000
private const val KEY_LENGTH_BITS = 256
600.000 rondas de PBKDF2-HMAC-SHA256, AES-256.
…y la longitud mínima, sin la cual el número de rondas no dice nada
// MINDESTENS ZEHN ZEICHEN, UND BUCHSTABEN SIND ERLAUBT.
//
// Der verschluesselte Block liegt auf dem Relay, und darin steht die
// Freundesliste: Namen, Codes, Schluessel. Nicht der Inhalt von
// Nachrichten, aber das soziale Netz. Faellt er je jemandem in die Haende,
// zaehlt allein, wie teuer das Durchprobieren ist.
//
// Acht Ziffern waeren 100 Millionen Moeglichkeiten — trotz 600 000
// PBKDF2-Runden auf OWASP-Stand auf einer guten Grafikkarte in wenigen
// Stunden durch. Genau deshalb sind es zehn: zwei Stellen mehr
// verhundertfachen den Aufwand, Buchstaben vertausendfachen ihn.
// Erzwungen werden sie nicht — wer bei Ziffern bleibt, soll das koennen;
// die Tastatur bietet beides an.
//
// VERLAUF, damit die Zahlen unten nicht verwirren: 4 -> 6 -> 8 -> 10, die
// PBKDF2-Runden parallel von 200k auf 600k (s. FriendsBackup). Jede Stufe
// kam aus demselben Grund, nur mit besserer Rechnung.
//
// GILT NUR FUER NEUE PINs. Beim Wiederherstellen wird keine Laenge
// geprueft, sonst sperrten wir Bestandsnutzer:innen aus ihrem eigenen
// Backup aus. Die Kehrseite: Blobs, die noch mit einer kurzen PIN von
// frueher verschluesselt sind, bleiben schwach — dagegen hilft nur, nach
// erfolgreicher Wiederherstellung zur Neuvergabe aufzufordern.
val isValid = if (isRestore) pin.isNotEmpty() else pin.length >= BACKUP_PIN_MIN && pin == confirmPin
Incluso 600.000 rondas no valdrían nada frente a un PIN numérico de cuatro dígitos. Solo los dos números juntos dan una afirmación verificable. La progresión 4 → 6 → 8 → 10 se anota deliberadamente al lado.
…y la derivación en sí, hoy todavía PBKDF2
/** Salt aus sub (nicht geheim — Salts müssen es nicht sein) statt fest
* verdrahtet, damit zwei verschiedene Konten mit zufällig gleicher PIN
* nicht denselben Schlüssel ableiten. */
fun deriveKey(pin: String, sub: String, verfahren: Verfahren = Verfahren.ARGON2ID): SecretKeySpec {
val salt = MessageDigest.getInstance("SHA-256").digest(sub.toByteArray(Charsets.UTF_8))
return when (verfahren) {
Verfahren.PBKDF2 -> {
val spec: KeySpec = PBEKeySpec(pin.toCharArray(), salt, ITERATIONS, KEY_LENGTH_BITS)
SecretKeySpec(
SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256").generateSecret(spec).encoded,
"AES",
)
}
La firma de la función delata el próximo paso: un parámetro verfahren («método»), con ARGON2ID como valor por defecto. Sin embargo, hoy los respaldos nuevos siguen pasando por la rama PBKDF2 — un interruptor en otra parte del mismo archivo los mantiene ahí, hasta que la app de iOS pueda leer Argon2id. Cambiarlo antes de tiempo dejaría ilegibles los respaldos que deben abrirse en el otro sistema operativo. Incluso un trabajo aún sin terminar tiene su lugar en esta página, siempre que se nombre honestamente como tal.
Campos firmados — qué cubre una firma
Afirmación: «nada puede sustituirse sin que se note.»
const SIGNING_FIELDS = {
post: ['id', 'author', 'handle', 'text', 'createdAt', 'originPeerId', 'audience', 'imageHasCameraExif', 'reachBudget', 'imageHash', 'avatarUrl', 'quotedPostId', 'encText', 'encKeys', 'encPubKey'],
// avatarUrl bei comment/friendreq nachgezogen (Sicherheits-Review vor
// Beta): vorher NICHT Teil der Signatur — ein Relay/MITM konnte das
// angezeigte Profilbild eines sonst gültig signierten, "✅ verifizierten"
// Kommentars/einer Freundschaftsanfrage unbemerkt austauschen. Gleicher
// Fix, der für WirePost schon früher gemacht wurde (s. dortiger Kommentar).
comment: ['id', 'postId', 'author', 'handle', 'text', 'createdAt', 'originPeerId', 'encPubKey', 'avatarUrl'],
report: ['id', 'targetId', 'targetType', 'reporterId', 'createdAt'],
friendreq: ['id', 'kind', 'fromId', 'fromName', 'toId', 'createdAt', 'encPubKey', 'avatarUrl'],
// Direktnachricht: anders als "post" gibt es hier NIE eine Klartext-Variante
// — kein `text`-Feld, nur der bestehende E2E-Mechanismus (encText/encKeys/
// encPubKey, exakt wie bei "Nur Freunde"-Posts). `toId` (Fingerprint/sub der
// Empfängerin) ist signiert mitgeführt, damit ein MITM nicht den Empfänger
// austauschen kann, ohne die Signatur ungültig zu machen — der Relay selbst
// braucht `toId` inhaltlich nicht (er matcht über encKeys), es ist reine
// Ziel-Absicherung.
dm: ['id', 'toId', 'author', 'createdAt', 'originPeerId', 'encPubKey', 'avatarUrl', 'encText', 'encKeys'],
// …
gm: ['id', 'gruppenId', 'author', 'createdAt', 'originPeerId', 'encPubKey', 'avatarUrl', 'encText', 'encKeys'],
};
El comentario documenta un fallo ENCONTRADO Y CERRADO: avatarUrl no estaba firmado, un relay podía sustituir la foto de perfil de un comentario aun así válidamente firmado y «✅ verificado». Mostrar el propio fallo abiertamente es más creíble que cualquier promesa de seguridad. La línea más reciente (gm, desde el 31.08.2026) muestra que el patrón crece con el producto: los mensajes de grupo reciben la misma protección que los mensajes directos, no una propia y más débil.
El segundo hallazgo en la misma función, meses después
/** Felder, in denen ein senkrechter Strich erlaubt bleiben MUSS, je Typ.
* Genau eins pro Typ, und nur dort, wo Menschen frei schreiben. */
const TRENNZEICHEN_ERLAUBT = { post: 'text', comment: 'text' };
/**
* WARUM EIN SENKRECHTER STRICH IN FAST KEINEM FELD STEHEN DARF.
*
* signingString() haengt die signierten Felder mit '|' aneinander und
* escaped nichts. Enthaelt ein Feldwert selbst ein '|', sind die Feldgrenzen
* nicht mehr eindeutig: Aus author="Anna", handle="anna", text="hallo|welt"
* wird die Zeichenkette "…|Anna|anna|hallo|welt|…" — und dieselbe entsteht
* aus handle="anna|hallo", text="welt". Beide Belegungen ergeben BYTEWEISE
* dieselbe signierte Zeichenkette, also gilt dieselbe Signatur fuer beide.
*
* Wer eine Nachricht unterwegs veraendern kann — der Relay selbst, oder wer
* den Transportweg bricht —, koennte damit Inhalt ueber eine Feldgrenze
* schieben, ohne die Signatur zu brechen. Erfinden kann er nichts, nur
* vorhandene Bytes verruecken. Aber genau davor soll die Signatur schuetzen.
*
* DIESE PRUEFUNG MACHT DIE ZERLEGUNG WIEDER EINDEUTIG. Darf ausser dem
* Textfeld kein Feld ein '|' enthalten, dann muss jede abweichende Zerlegung
* ein '|' in ein anderes Feld schieben — und scheitert hier. Nachrechnen:
* Wer die Grenze VOR dem Text nach rechts schiebt, bekommt eins ins
* vorangehende Feld; wer sie danach nach links schiebt, eins ins folgende.
* Beides abgewiesen. Typen ohne Textfeld (dm, friendreq, report) sind damit
* vollstaendig eindeutig.
*
* DIE SAUBERE LOESUNG WAERE EIN LAENGENPRAEFIX statt eines Trennzeichens.
* Sie aendert aber JEDE signierte Zeichenkette und damit jede Signatur — jede
* installierte App wuerde ab dem Umstieg alles als "Signatur ungueltig"
* verwerfen. Das braucht eine versionierte Umstellung mit Uebergangszeit, wie
* bei der Attest-Pflicht. Diese Pruefung hier kostet nichts und schliesst die
* Luecke bis dahin.
*
* PREIS, bewusst bezahlt: Wer einen senkrechten Strich im Anzeigenamen fuehrt,
* wird abgewiesen. Der Client zeigt den Grund seit RelayAbgelehnt an.
*/
function trennzeichenProblem(type, p) {
Dos hallazgos documentados en la misma función — y la admisión honesta de por qué la solución limpia (un prefijo de longitud) todavía no ha llegado. Esta transparencia sobre un resto sin terminar resulta más creíble que una página donde todo parece acabado.
Presupuesto de alcance — aritmética, no un algoritmo
Afirmación: «ningún algoritmo, solo un presupuesto de alcance.»
function zustellGrenze(p) {
return budgetVon(p) * BROADCAST_K;
}
function darfNochReisen(p) {
if (!REICHWEITE_AKTIV) return true;
if ((p.t || 'post') !== 'post') return true;
if ((p.audience || 'all') !== 'all') return true;
return (zustellungen.get(p.id) || 0) < zustellGrenze(p);
}
Cuatro líneas. Ningún modelo, ninguna ponderación, ninguna telemetría — una multiplicación y una comparación. Este es el camino por el que pasa prácticamente toda la entrega: cuenta cuántos dispositivos recogen una publicación en el feed «Todos», no cuántos la reenvían. Y la tercera línea es la más importante desde el punto de vista humano — audience !== 'all' deja pasar todo lo demás sin obstáculos: solo «Todos» tiene límite. «Amistades», «Local» y «Solo yo» están dirigidos, no difundidos, y llegan a todos los destinatarios previstos. Elegir un alcance pequeño no silencia a las amistades propias — solo limita cuánto se propaga una publicación entre desconocidos.
// Resonanzreichweite: 1 Hop verbraucht, bei lokaler Resonanz (z. B.
// Autor ist Freund) einen kleinen, rein lokalen Rabatt gewähren.
// Diese Entscheidung wird nirgends gemeldet oder gezählt.
val discount = if (isResonantAuthor(post)) RESONANCE_DISCOUNT else 0
val forwarded = post.copy(hopCount = (post.hopCount + 1 - discount).coerceAtLeast(0))
if (forwarded.hopCount < forwarded.reachBudget) {
broadcast(forwarded.toJson().toString(), exclude = key)
} // sonst: Reichweite erschöpft, verebbt hier — keine Weitergabe
Solo en la red local el cálculo se ve así: un contador de saltos que sube en 1 con cada reenvío de dispositivo a dispositivo (y baja en vez de subir para las amistades del autor). Ese es el camino en el mismo Wi-Fi — la excepción, no la regla. Ambos caminos son aritmética, no un algoritmo, solo distinta: uno cuenta reenvíos, el otro cuenta recogidas.
Identidad en el chip — Android
Afirmación: «la clave privada nunca sale del chip.»
private fun ensureKeyPair() {
if (keyStore.containsAlias(ALIAS)) return
// StrongBox bevorzugen (API 28+), bei Fehler auf normalen Keystore zurückfallen.
val strongBoxSupported = android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P
if (strongBoxSupported) {
try { generate(strongBox = true); Log.i(TAG, "Identität in StrongBox erzeugt"); return }
catch (e: Exception) { Log.w(TAG, "StrongBox nicht verfügbar: ${e.message}") }
}
generate(strongBox = false)
Log.i(TAG, "Identität im Keystore erzeugt")
}
private fun generate(strongBox: Boolean) {
val builder = KeyGenParameterSpec.Builder(
ALIAS,
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY,
)
.setAlgorithmParameterSpec(java.security.spec.ECGenParameterSpec("secp256r1"))
.setDigests(KeyProperties.DIGEST_SHA256)
if (strongBox && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
builder.setIsStrongBoxBacked(true)
}
Se prefiere StrongBox, con reserva en el Keystore normal. Esa reserva tiene su lugar — no todos los dispositivos tienen StrongBox, y ocultarlo sería exactamente el tipo de media verdad que esta página reprocha a otros.
Identidad en el chip — iPhone
let attributes: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeECSECPrimeRandom,
kSecAttrKeySizeInBits as String: 256,
kSecAttrTokenID as String: kSecAttrTokenIDSecureEnclave,
kSecPrivateKeyAttrs as String: [
kSecAttrIsPermanent as String: true,
kSecAttrApplicationTag as String: tag,
kSecAttrAccessControl as String: access,
],
]
var error: Unmanaged<CFError>?
guard let key = SecKeyCreateRandomKey(attributes as CFDictionary, &error) else {
// Secure Enclave nicht verfügbar (z. B. manche Simulatoren) — Fallback
// auf einen software-gestützten Schlüssel im Keychain, weiterhin
// secp256r1 und damit drahtkompatibel, nur ohne Hardware-Schutz.
return generateSoftwareKey(tag: tag)
kSecAttrTokenIDSecureEnclave es la única línea que realmente importa. Que dos implementaciones independientes produzcan el mismo formato ya es en sí un argumento — y aquí también la reserva se declara abiertamente justo al lado.
Lo que NO hay en una notificación
// KEIN NAME UND KEIN KLARTEXT IN DER NUTZLAST — auch nicht dort, wo der
// Relay beides kennt.
//
// Bis hierher stand der Absendername im Titel und der Kommentartext im Body.
// Beides ist inhaltlich verzichtbar und reist an Apple und Google vorbei: aus
// "diese Person hat jener geschrieben, um 21:04" laesst sich der halbe
// Freundes-Graph rekonstruieren, ohne eine einzige Nachricht zu lesen. Genau
// dieselbe Korrektur wurde beim Freundschaftsanfrage-Push schon gemacht (s.
// dortiger Kommentar) — hier fehlten nur die drei anderen Faelle.
//
// Was der Relay stattdessen schickt, ist eine Kennung: `t` und `fromId`. Den
// Namen loest der Client aus der EIGENEN Freundesliste auf — dieselbe Quelle,
// aus der die iOS-Extension ihn schon fuer Direktnachrichten holt, und der
// einzigen, der man glauben darf.
//
// Die Texte hier bleiben trotzdem stehen und bleiben deutsch. Sie sind der
// Rueckfallschirm: installierte Apps aelterer Fassungen lesen `title`/`body`
// unbesehen, und auf iOS erscheinen sie, wenn die Extension nicht laeuft.
// Weglassen hiesse "HIY" mit leerer Zeile auf jedem Geraet, das noch nicht
// aktualisiert hat. Neue Clients ueberschreiben sie ohnehin — samt der
// richtigen Sprache, was die alte Loesung nie konnte.
Trata de un límite, no de una capacidad: una notificación push tiene que pasar forzosamente por Apple o Google. La única pregunta es qué hay dentro. Google y Apple ven QUE algo llegó y CUÁNDO — no qué, y no de quién.
Módulos de grupo — una sola respuesta para todos
/**
* DIE EINE ANTWORT FUER ALLE MODULE.
*
* Vier Module fragen etwas, und der naheliegende Weg waere, jedem seine
* eigene Antwortart zu geben: `stimme`, `zusage`, `verfuegbarkeit`,
* `haken`. Vier Formate auf der Leitung, vier Auswertungen, vier Stellen,
* an denen sich ein Fehler einnisten kann.
*
* Stattdessen EINE: `{bezug, art, wahl:[Zahlen]}`. Bei der Umfrage sind
* die Zahlen die angekreuzten Optionen, beim Termin genau eine (ja /
* vielleicht / nein), beim Doodle die passenden Vorschlaege, bei der Liste
* die erledigten Punkte. Die Auswertung steht damit an EINER Stelle
* (`GruppenModule.werte`), und ein fuenftes fragendes Modul braucht sie
* gar nicht mehr.
*
* WARUM DAS OHNE ABSPRACHE FUNKTIONIERT: Jede Antwort ist eine eigene
* Nachricht mit eigenem Zeitstempel. Wer zweimal antwortet, hat zweimal
* geantwortet — es gilt die spaetere. Niemand muss etwas zurueckziehen,
* niemand muss eine Nachricht aendern (was verschluesselt ohnehin nicht
* ginge), und ein Geraet, das eine Woche aus war, holt die Antworten in
* beliebiger Reihenfolge nach und kommt aufs selbe Ergebnis.
*/
const val MODUL_ANTWORT = "antwort"
Cuatro módulos que preguntan algo — encuesta, evento, doodle, lista — y una única forma de respuesta para todos. Eso es una decisión de diseño, no una línea de código: los otros tres caminos (un formato propio por módulo) se consideraron y se descartaron. El párrafo «WARUM DAS OHNE ABSPRACHE FUNKTIONIERT» («por qué esto funciona sin coordinación») resuelve de paso un verdadero acertijo — cómo puede «cambiar» la respuesta a una encuesta si nadie puede alterar un mensaje cifrado ya enviado: no se cambia nada, se dice otra vez, y vale la última.
Quien escanea, llama a la puerta
/**
* "Ich habe euren QR-Code gescannt und moechte dazu."
*
* DER EINZIGE WEG, WIE JEMAND VON AUSSEN AN EINE GRUPPE HERANKOMMT — und
* er endet nicht im Beitritt, sondern in einer Frage. Wer scannt, ist
* damit noch kein Mitglied; die Nachricht geht ausschliesslich an die
* Person, deren Code im QR steht, und die entscheidet.
*
* WARUM DAS OHNE FREUNDSCHAFT FUNKTIONIERT: Der QR traegt den
* Verschluesselungsschluessel der Einladenden. Damit laesst sich eine
* ganz gewoehnliche Gruppennachricht bauen, deren `encKeys` genau EINEN
* Eintrag hat — den fuer sie. Der Relay stellt sie zu, ohne etwas ueber
* Gruppen zu wissen; alle anderen koennen sie nicht einmal oeffnen.
*
* WARUM DAS NICHT ZUM EINFALLSTOR WIRD: Es traegt sich niemand selbst
* ein. Die Nachricht landet als Frage im Verlauf, und erst ein Tippen auf
* "Aufnehmen" schreibt die Mitgliederliste fort — von einem Geraet, das
* ohnehin schon Mitglied ist.
*/
const val MODUL_BEITRITT = "beitritt"
Un código QR parece una llave. Aquí es un timbre. La diferencia no es menor: reenviar un enlace de invitación no abre un grupo de par en par — el mensaje va a exactamente una persona, que decide, y nadie se apunta a sí mismo.
Lo que un servidor no puede distinguir
} else if (type === 'gm' && stored.gruppenId && stored.encKeys && !stored.silent) {
// `silent` wie bei friendreq (s. oben): Eine Gruppennachricht ist nicht
// immer eine Nachricht. Nach jedem Geraetewechsel geht die aktualisierte
// Mitgliederliste an alle — Verwaltung, kein Gespraech. Ohne diese
// Unterdrueckung klingelte bei jeder Wiederherstellung "Neue
// Gruppennachricht" bei allen Mitgliedern, ohne dass jemand etwas
// geschrieben hatte (gemeldet von Benjamin am 31.08.2026).
//
// WARUM DER RELAY DAS NICHT SELBST SIEHT: Das Modul steckt im
// verschluesselten `encText`. Er sieht "eine gm" und sonst nichts — das
// ist der Sinn der Sache. Also entscheidet es die Absenderin.
// An jedes Mitglied ausser die Absenderin selbst.
El extracto más honesto de toda la colección. Muestra un inconveniente del cifrado en lugar de una ventaja: como el servidor no conoce el contenido, tampoco puede decidir si algo merece una notificación. La solución no es una excepción al principio, es trasladar la decisión a donde está el texto plano — al dispositivo. Quien quiera saber si un cifrado es real debe fijarse en puntos exactamente así: donde el servidor sabe evidentemente menos de lo que necesitaría saber para ser cómodo.
También el lado del relay
Hasta ahora casi todos los extractos venían de las apps. Pero el relay importa más para la credibilidad, porque ahí es donde vive la sospecha — «¿qué recopila el servidor?» Cuatro puntos que no revelan nada crítico.
Lo que el relay sabe de un grupo — y que está escrito ahí
/**
* Gruppennachrichten abholen. Zugestellt wird, wofuer ein Eintrag in `encKeys`
* vorliegt — es gibt kein Empfaengerfeld wie `toId` bei der Direktnachricht.
*
* DAMIT SIEHT DER RELAY, WER MIT WEM IN EINER GRUPPE IST. Das laesst sich nicht
* vermeiden: Irgendwoher muss er wissen, an wen er ausliefern soll. Neu ist es
* nicht — bei einem "Nur Freunde"-Beitrag erfaehrt er aus denselben `encKeys`
* schon heute, wer mit wem befreundet ist. Den INHALT sieht er in keinem der
* beiden Faelle. Das gehoert auf die Technologie-Seite, nicht unter den Tisch.
*/
app.post('/gm/fetch', (req, res) => {
La última frase del comentario es lo esencial. Una debilidad nombrada en el propio código fuente es un tipo de afirmación distinto a una encontrada desde fuera. Y no es nueva: en una publicación «Solo amistades», la misma información ya está en encKeys.
Retención: siete días, y está escrito en el código
const RETENTION_DAYS = parseInt(process.env.RETENTION_DAYS || '7', 10);
const MAX_POSTS = parseInt(process.env.MAX_POSTS || '50000', 10);
// …
function prune() {
const cutoff = nowMs() - RETENTION_DAYS * 86400000;
let changed = false;
for (const [id, p] of posts) if ((p.serverTs || 0) < cutoff) { posts.delete(id); changed = true; }
// Obergrenze: älteste zuerst entfernen
if (posts.size > MAX_POSTS) {
const sorted = [...posts.values()].sort((a, b) => a.serverTs - b.serverTs);
// Die Zahl VOR der Schleife festhalten: `posts.size` schrumpft mit jedem
// Loeschen, die Bedingung wanderte also mit und es wurde nur etwa die
// Haelfte des Ueberhangs entfernt. MAX_POSTS wurde dadurch nie erreicht.
const zuViel = posts.size - MAX_POSTS;
for (let i = 0; i < zuViel; i++) { posts.delete(sorted[i].id); changed = true; }
}
if (changed) {
rewriteDisk();
// Die Reichweiten-Buchhaltung mit aufraeumen — sonst waechst sie ewig
// weiter, obwohl die Beitraege laengst weg sind.
for (const id of [...zustellungen.keys()]) if (!posts.has(id)) { zustellungen.delete(id); zustellGeaendert = true; }
for (const id of [...zustellHerkunft.keys()]) if (!posts.has(id)) zustellHerkunft.delete(id);
sichereZustellungen();
}
}
Los plazos de retención suelen figurar en una política de privacidad. Aquí figuran en el archivo que los ejecuta, y ambos pueden ponerse uno junto al otro. El comentario del medio muestra un bug real, encontrado y corregido: el límite nunca se alcanzaba, porque el conteo se desplazaba durante el borrado.
Un mensaje directo solo se obtiene con prueba
const data = `${praefix}|${since}|${createdAt}|${typeof id === 'string' ? id : ''}`;
if (!verifyRawSignature(data, pubKey, signature)) {
// Fingerprint UND IP mitloggen: ohne die war eine abgelehnte Anfrage nicht
// zuzuordnen. Eine leere Signatur bekommt einen eigenen Hinweis, das ist
// der haeufigste Fall (gesperrtes iPhone, s. SecureEnclaveIdentity.sign).
const who = fingerprintOf(pubKey) || 'unbekannt';
const why = !signature ? 'Signatur LEER (Geraet gesperrt?)' : 'Signatur ungueltig';
console.warn(`[relay] /${praefix} abgelehnt: ${why} (fp=${who}, ip=${req.ip})`);
res.status(401).json({ error: 'Signatur ungueltig' }); return null;
}
El token de acceso de la app está integrado en el código y, por tanto, es de facto público. No basta para los mensajes directos: para recuperarlos hay que firmar con la clave privada que nunca sale del chip de hardware. La diferencia entre «con sesión iniciada» y «en posesión del dispositivo» — en un punto donde de verdad importa.
Los errores también están aquí
// Direktnachrichten gehen NIE an alle. `withinBudget` wird nur im
// post-Zweig oben angefasst und blieb fuer "dm" auf true — jede
// Direktnachricht ging damit live an jeden offenen Client, mit Empfaenger,
// Absender-Schluessel und Zeitpunkt. Der HTTP-Weg schliesst das laengst aus
// (GET /posts filtert dm heraus, s. dort), der WebSocket-Weg nicht. Wer
// wann wem schreibt, lag damit fuer jeden offen, der eine Verbindung
// offenhaelt — genau das Metadaten-Leck, das die DM-Auslieferung ueber
// POST /dm/fetch verhindern sollte.
if (stored.t === 'dm') withinBudget = false;
El extracto más contundente de toda la colección. Aquí hay un bug real — encontrado, corregido, y descrito sin adornos: cada mensaje directo salía en directo, por WebSocket, a cada cliente conectado, con destinatario, clave del remitente y hora exacta. Dejar algo así visible públicamente significa no tener nada que ocultar, o ser más valiente de lo necesario. Ambas cosas convencen más que cualquier promesa de seguridad.
Lo que deliberadamente no se muestra: lo que se muestra es la criptografía — generación de claves, cifrado, derivación de claves, formato de firma, lógica de alcance. Esa es la parte que debe poder verificarse; es segura porque es conocida.
No se muestra la defensa contra abusos: patrones de detección de bots, umbrales de denuncia, limitación de frecuencia. Ahí la apertura solo ayuda al atacante, que solo tiene que quedarse por debajo del umbral. Esta distinción no es una excusa, es la diferencia entre una cerradura y una alarma.
Nada del panel de moderación: muestra bloqueos, denuncias y expedientes, y hasta un extracto inofensivo delataría la estructura de la moderación.
Y ningún parámetro de derivación de clave usado como afirmación de «así de seguro es»: los números en sí (rondas, memoria) pueden quedarse, pero sin un cálculo comparativo del tipo «X años para romperlo». Ese tipo de números envejece mal e invita a la contradicción.
Estos extractos son una mirada curada, no un repositorio completo ni una invitación a construir un cliente propio. Preguntas sobre un punto concreto: info@hiy.ch.