惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

S
Schneier on Security
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research
WordPress大学
WordPress大学
博客园 - Franky
V
V2EX
爱范儿
爱范儿
J
Java Code Geeks
小众软件
小众软件
Last Week in AI
Last Week in AI
The Cloudflare Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Hugging Face - Blog
Hugging Face - Blog
T
The Blog of Author Tim Ferriss
酷 壳 – CoolShell
酷 壳 – CoolShell
The Register - Security
The Register - Security
GbyAI
GbyAI
Vercel News
Vercel News
Y
Y Combinator Blog
腾讯CDC
F
Fortinet All Blogs
I
InfoQ
N
Netflix TechBlog - Medium
B
Blog RSS Feed
D
Docker
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
量子位
博客园 - 司徒正美
阮一峰的网络日志
阮一峰的网络日志
The GitHub Blog
The GitHub Blog
Microsoft Security Blog
Microsoft Security Blog
V
Visual Studio Blog
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
Blog — PlanetScale
Blog — PlanetScale
H
Help Net Security
云风的 BLOG
云风的 BLOG
A
About on SuperTechFans
Scott Helme
Scott Helme
T
Tor Project blog
U
Unit 42
Google Online Security Blog
Google Online Security Blog
PCI Perspectives
PCI Perspectives
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
NISL@THU
NISL@THU
D
Darknet – Hacking Tools, Hacker News & Cyber Security
aimingoo的专栏
aimingoo的专栏
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Stack Overflow Blog
Stack Overflow Blog
Security Archives - TechRepublic
Security Archives - TechRepublic

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Я открыл боевую базу своего clipboard-sync, чтобы показать, что он знает о вашем пароле. Ответ: ничего
Sergey Razzhivin · 2026-06-07 · via Все публикации подряд на Хабре

Средний

9 мин

3.6K

Скопировали пароль от прода и синхронизировали его между ноутбуком и телефоном. Где он теперь лежит и кто может его прочитать? Я сделал сервис, где честный ответ — «нигде в открытом виде и никто, включая меня». И сейчас покажу строку из живой базы, чтобы это доказать.

Это первая статья про Copy Sync — приватный кроссплатформенный обмен буфером обмена. Я не собираюсь его вам продавать. Я хочу разобрать одну инженерную задачу: как построить сервер, которому физически нечего у вас украсть, даже если им завладеет кто‑то злой — включая меня самого. Весь крипто‑код открыт, и проверить меня можно по ~70 строкам, а не по обещаниям.

TL;DR. Сервер хранит только зашифрованный blob и метаданные маршрутизации (кому доставить, когда протухнет). Шифрование и расшифровка живут на клиенте, в Web Crypto API: X25519 (ECDH) → HKDF‑SHA256 (соль = IV клипа) → AES-256-GCM. Сервер никогда не видит ни приватного ключа, ни байта открытого текста. Realtime — на SSE, хранения нет: клипы удаляются по TTL раз в минуту. Чего пока нет — подписи отправителя и сверки отпечатков ключей; честно разбираю это в конце.

Почему я вообще полез это писать

История банальная. Я скопировал на телефоне токен из СМС, хотел вставить в терминал на Маке — и в очередной раз отправил его себе в Telegram «Избранное». Скопировал ссылку на ноутбуке, хочу открыть на телефоне — снова «Избранное». Это раздражало, и я пошёл искать готовый clipboard‑sync.

Их хватает: Pushbullet, разные облачные менеджеры буфера. Я поставил один, попользовался неделю — и поймал себя на неприятной мысли: я только что прогнал через этот буфер пароль от продакшен‑базы, и он улетел на чей‑то сервер. Дальше я полез читать, как эти штуки устроены, и почти везде увидел одну архитектуру: клиент шлёт содержимое буфера на сервер, сервер хранит и пересылает его другим вашим устройствам. TLS‑шифрование «в транзите» есть почти у всех — но это шифрование канала. На самом сервере данные лежат открытыми.

То есть вопрос «читает ли провайдер мой буфер» сводится к «верю ли я провайдеру». А я не хотел верить на слово — даже себе. Я хотел инструмент, про который можно сказать строго: сервер не читает мои данные не потому, что обещал, а потому, что у него нет ключа. Не нашёл такого под свои требования — и сел писать свой.

Сначала — доказательство, а не слова

Зайдём с конца. Вот что реально оказывается в базе, когда одно моё устройство отправляет клип другому. Это не упрощённая схема для статьи — это определение таблицы clips из исходника, как есть:

@Entity('clips')
export class Clip {
  @PrimaryColumn('uuid') id: string
  @Column({ name: 'sender_device_id', type: 'uuid' })    senderDeviceId: string
  @Column({ name: 'recipient_device_id', type: 'uuid' }) recipientDeviceId: string
  @Column({ name: 'recipient_user_id', type: 'uuid' })   recipientUserId: string
  
  // AES-256-GCM ciphertext + 16-байтный auth-тег, склеенные вместе.
  @Column({ type: 'bytea' }) ciphertext: Buffer

  // GCM-nonce, ровно 12 байт (проверяется на уровне DTO).
  @Column({ type: 'bytea' }) iv: Buffer

  @CreateDateColumn() createdAt: Date
  @Column({ name: 'ttl_expires_at', type: 'timestamptz' }) ttlExpiresAt: Date
}

Прочитайте этот список колонок глазами того, кто захватил сервер и сделал SELECT * FROM clips. Что он получит?

ciphertext — шифротекст AES-256-GCM. Без ключа это случайный шум.

iv — публичный одноразовый nonce. Сам по себе бесполезен.

— три *_device_id / user_id — метаданные маршрутизации: кому и от кого доставить.

ttl_expires_at — когда удалить.

Здесь нет колонки plaintext. И нет ни одной колонки с ключом. Не потому что я её «забыл показать» — её нет в схеме. Сервер по своей конструкции не может записать сюда открытый текст, потому что он его никогда не видит: к моменту, когда данные доходят до сервера, они уже зашифрованы на клиенте. Всё остальное в статье — объяснение, как так вышло.

Главное решение: сервер с нулевым знанием

Copy Sync — это pnpm‑монорепозиторий: сервер на NestJS + PostgreSQL, веб‑клиент на Vue 3, и набор общих пакетов. Дальше будут расширение для браузера, десктоп и мобайл — но сердце всего одно: крошечный крипто‑пакет @copy-sync/crypto, вокруг которого выстроено остальное.

Вся идея держится на одном: сервер не должен ничего знать о содержимом. Остальное — следствия:

Ключи генерируются на устройстве и не уезжают с него. У каждого устройства своя пара ключей. Сервер получает только публичные.

Шифрование адресное: один отправитель → один получатель. Отправка «на все мои устройства» — это N независимых шифрований и N строк в таблице. Сервер видит N разных blob'ов и не знает, что это «одно и то же».

Сервер — труба, а не архив. Доставка идёт через SSE, у клипов есть TTL, протухшие удаляет крон. Долговременного хранилища ваших данных просто нет.

Разберём по очереди — с кодом.

Как клиенты договариваются о ключе, ни разу его не передав

При первом запуске устройство генерирует пару X25519. Публичный ключ уходит на сервер (он на то и публичный), приватный остаётся на устройстве. Вот реальная генерация из keypair.ts:

export async function generateDeviceKeyPair(): Promise<SerializedKeyPair> {

  const kp = (await crypto.subtle.generateKey({ name: 'X25519' }, true, [
    'deriveBits',
  ])) as CryptoKeyPair

  const pubRaw = new Uint8Array(await crypto.subtle.exportKey('raw', kp.publicKey))
  const privPkcs8 = new Uint8Array(await crypto.subtle.exportKey('pkcs8', kp.privateKey))

  return {
    privateKeyPkcs8: bytesToBase64(privPkcs8), // лежит в keystore устройства
    publicKeyHex: bytesToHex(pubRaw),          // уходит на сервер
  }

}

X25519 — это Diffie‑Hellman на кривой Curve25519. Его единственная задача — дать двум устройствам вычислить общий секрет, ни разу не передав его по сети. Магия ECDH в симметрии: ECDH(priv_A, pub_B) == ECDH(priv_B, pub_A). Каждая сторона берёт свой приватный ключ и чужой публичный — и обе получают один и тот же секрет. Сервер, у которого есть только публичные ключи, вычислить его не может: для этого нужен хотя бы один приватный, а приватных у сервера нет.

Из общего секрета — отдельный ключ на каждый клип

Использовать сырой результат ECDH прямо как ключ шифрования — плохая практика. Правильно прогнать его через KDF. Я беру HKDF‑SHA256 — и вот тут есть приём, которым я доволен: солью для HKDF служит IV конкретного клипа. Вот целиком функция вывода ключа из clip.ts:

const HKDF_INFO = new TextEncoder().encode('copy-sync v1 clip')

async function deriveAesKey(myPrivate, peerPubHex, iv, usage) {

  const peerPub = await crypto.subtle.importKey('raw', hexToBytes(peerPubHex), { name: 'X25519' }, false, [])
  const sharedBits = await crypto.subtle.deriveBits({ name: 'X25519', public: peerPub }, myPrivate, 256)
  const hkdfKey = await crypto.subtle.importKey('raw', sharedBits, 'HKDF', false, ['deriveKey'])

  return crypto.subtle.deriveKey(
    { name: 'HKDF', hash: 'SHA-256', salt: iv, info: HKDF_INFO },
    hkdfKey, { name: 'AES-GCM', length: 256 }, false, [usage],
  )

}

IV у каждого клипа случайный — 12 байт из crypto.getRandomValues. Значит, и AES‑ключ получается уникальным на каждое сообщение, хотя общий ECDH‑секрет между парой устройств один. Из этого выпадает мелочь, которая мне нравится: один и тот же текст, отправленный одному и тому же получателю дважды, даёт два совершенно разных шифротекста. Сервер не может даже понять, что вы скопировали одно и то же.

Дальше — симметричное шифрование. AES-256-GCM выбран потому, что это AEAD: он даёт не только конфиденциальность, но и аутентификацию. При расшифровке проверяется 16-байтный тег целостности — подменённый или битый шифротекст просто не расшифруется, а не вернёт мусор.

export async function encryptClip(plaintext, recipientPubHex, myPrivateKey) {

  const iv = crypto.getRandomValues(new Uint8Array(12))
  const aesKey = await deriveAesKey(myPrivateKey, recipientPubHex, iv, 'encrypt')
  const ct = new Uint8Array(await crypto.subtle.encrypt(
    { name: 'AES-GCM', iv }, aesKey, new TextEncoder().encode(plaintext)))

  return { ciphertext: bytesToBase64(ct), iv: bytesToBase64(iv) }

}

На сервер уходит ровно { ciphertext, iv }. Получатель делает зеркальную операцию: тот же ECDH со своей стороны, тот же HKDF с тем же IV, AES-GCM-decrypt. Всё.

Сервер как труба: realtime без хранения

Раз сервер ничего не понимает в данных, логично, чтобы он их и не копил. Доставка идёт через Server‑Sent Events — по одному потоку на устройство, с heartbeat'ом, чтобы прокси не рвал соединение clips.sse.controller.ts:

@Sse()

stream(@CurrentDevice() device: Device): Observable<SseMessage> {

  return merge(
    this.events.subscribe(device.id).pipe(map((ev) => ({ type: ev.type, data: ev }))),
    interval(25_000).pipe(map(() => ({ type: 'ping', data: { t: Date.now() } }))),
  )

}

А у каждого клипа есть ttl_expires_at, и крон раз в минуту выносит протухшее, попутно уведомляя получателя, что клип исчез clips.ttl.task.ts:

@Cron(CronExpression.EVERY_MINUTE)

async cleanup(): Promise<void> {

  const deleted = await this.clips.deleteExpired()

  for (const { id, recipientDeviceId } of deleted) {
    this.events.publish(recipientDeviceId, { type: 'clip.expired', clipId: id })
  }

}

Смысл простой: даже зашифрованный blob не должен лежать вечно. Доставили — забыли.

Что это значит на практике

Если убрать крипто‑жаргон, всё сводится к трём бытовым гарантиям:

1. Дамп базы бесполезен. Утечёт хранилище — наружу уйдут шифротексты без ключей. Расшифровать нечем: приватные ключи только на ваших устройствах.

2. Запрос «отдайте данные пользователя X» нечем удовлетворить. У сервера нет открытого текста ни сейчас, ни в истории — он его не видел никогда.

3. Сервер не видит даже повторов и долго ничего не хранит. Случайный IV делает каждый шифротекст уникальным, а TTL удаляет клипы вскоре после доставки.

Это не «мы обещаем не читать». Это «нам физически нечем прочитать».

Честно о границах

Я обещал разбирать, а не продавать, поэтому про слабые места — прямо, до того как их найдут в комментариях:

1. Это v1. Метка copy-sync v1 clip в info HKDF — задел на версионирование протокола, а не справка о зрелости. Код молодой.

2. Подписи отправителя пока нет. Отдельной Ed25519-подписи в текущей версии нет: аутентичность держится на том, что AES‑GCM‑тег проверяется на ключе, выведенном из ECDH именно с публичным ключом конкретного отправителя. Полноценная защита от активного MITM на этапе обмена ключами (verification fingerprints) — в бэклоге.

3. Доверие к публичным ключам. Сейчас вы доверяете тому, что сервер отдал правильные публичные ключи ваших устройств. Сравнение отпечатков ключей между устройствами — следующий приоритет.

4. Приватный ключ хранится сериализованным в keystore устройства (в вебе это localStorage). In‑memory CryptoKey после реимпорта помечен неэкспортируемым, но сам PKCS8-байт лежит в платформенном хранилище — значит, в веб‑модели угроз есть XSS. Это честная цена браузерного клиента, и расширение здесь будет строже веба.

Если я где‑то ошибаюсь в крипто — это лучшее, что может случиться со статьёй. Код открыт, поправим вместе.

Как проверить каждое моё слово

Главное свойство zero‑knowledge архитектуры — её проверяемость: вам не нужно мне верить. Крипто‑ядро — это три маленьких файла, на которые уйдёт минут пятнадцать чтения, не больше:

Что я предлагаю проверить ревьюеру в первую очередь:

1. Что из encryptClip на сервер уходит только { ciphertext, iv } — и ничего больше.

2. Что IV случайный на каждый вызов и используется как соль HKDF.

3. Что приватный ключ реимпортируется неэкспортируемым (importDevicePrivateKey).

4. Что в серверном коде нет ни одного пути, где появлялся бы приватный ключ или открытый текст (начните с entities/clips.entity.ts — там просто негде).

Репозиторий открыт: [gitlab.com/razgiva/copy‑sync](https://gitlab.com/razgiva/copy‑sync), смотреть packages/crypto Нашли дыру — это ценнее любого лайка.

Попробовать и что дальше

Веб‑клиент уже работает: [copysync.ru/app](https://copysync.ru/app). Заводите устройства, копируйте между ними — и помните, что сервер‑посредник не может прочитать ни одного вашего символа.

Дальше по плану — расширение для браузера (MV3, фоновый SSE), синхронизация ключей между устройствами и сверка отпечатков, потом десктоп и мобайл. Про каждый этап буду писать так же: с кодом и с честным разделом про то, чего пока нет.

Если вы безопасник — буду рад разносу: код для того и открыт. И мне правда интересно — где, по‑вашему, в этой схеме самое слабое место?

Это первая статья серии про разработку Copy Sync. Дальше — про MV3-расширение, сверку ключей между устройствами и про то, почему realtime здесь на SSE, а не на WebSocket.