USB-свисток для отправки текста из телефона
USB-свисток для отправки текста из телефона

Идея устройства возникла у меня давно. Захотелось аппаратного менеджера паролей. Надоело перепечатывать 20-символьные пароли из телефонного KeyPass, а на каждый компьютер, где нужно что‑то вводить, устанешь устанавливать. Посмотрел, что предлагают, посмотрел, что делают. В основном, не понравились размеры. Иногда цена. С другой стороны, телефон с паролями всегда с собой. А тут ещё бесплатный DeepSeek.

TL;DR
Просто история мучений. В конце прошивка для контроллера + Android‑приложение, работает. Хотя и на стадии «концепт».

Однажды я вспомнил, что есть такие ESP32, которые умеют работать с USB напрямую и эмулировать HID-устройства. У них есть bluetooth, то есть можно воткнуть в компьютер, прикинуться клавиатурой, и с телефона отправить текст. Размер контроллера совсем маленький, повесил на ключи, — и он всегда с тобой.

Купил у китайцев ESP32-C3. Пока оно ехало, узнал, что USB HID там нет. Купил ESP32-S3 zero mini micro nano (их по-разному называют) — это для тех, кто решит повторить мой путь.

В общем, из железа это всё. Дальше прошивка и приложение.

Вайб-кодинг это называется, когда сказать можешь, а программировать — нет. Удобно, что не пришлось изучать контроллер, BLE (я с ним до этого не сталкивался), Kotlin — ни разу не писал приложений для телефона, и до сих пор не умею. Я не настоящий программист.
Я почему‑то решил, что написав в чат DeepSeek «Напиши программу для ESP32-S3 для эмуляции USB HID клавиатуры. ESP32-S3 получает текст через BLE и отправляет в USB. Напиши Android-приложение, которое отправляет текст по BLE из буфера обмена» получу всё и сразу. Сразу не заработало ничего :(

Пока ехал контроллер, я занялся приложением для телефона.

Вот перечень некоторых ошибок:

Unresolved reference ‘R'
unable to instantiate activity componentinfo didnt find class mainactivivty
resource style/AppTheme not found
Attempt to invoke virtual method 'java.lang.String android.provider.MiuiSettings$SettingsCloudData$CloudData.getString(java.lang.String, java.lang.String)' on a null object reference

По большей части, я просто копировал текст ошибки из IDE в чат DeepSeek. Сначала добавлял «исправь ошибку» или «найди решение», потом забил даже на это. Нейросеть рассказывала, как исправить ошибки, которые она же и сделала. Странное ощущение. В итоге приложение заработало, я добавил тёмную тему и передвинул кнопочку:

Скрытый текст
MainActivity.kt
package com.mc.bleapp

import android.annotation.SuppressLint
import android.bluetooth.*
import android.bluetooth.le.ScanCallback
import android.bluetooth.le.ScanResult
import android.content.ClipboardManager
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.Button
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import java.util.*

class MainActivity : AppCompatActivity() {

    private lateinit var statusText: TextView
    private lateinit var logText: TextView
    private lateinit var sendButton: Button
    private lateinit var reconnectButton: Button

    private val DEVICE_NAME = "Secure BLE Keyboard"
    private val SERVICE_UUID = UUID.fromString("4fafc201-1fb5-459e-8fcc-c5c9c331914b")
    private val INPUT_CHAR_UUID = UUID.fromString("beb5483e-36e1-4688-b7f5-ea07361b26a8")

    private var bluetoothAdapter: BluetoothAdapter? = null
    private var bluetoothGatt: BluetoothGatt? = null
    private var inputCharacteristic: BluetoothGattCharacteristic? = null
    private var isConnected = false

    private val handler = Handler(Looper.getMainLooper())
    private val PERMISSIONS = arrayOf(
        android.Manifest.permission.ACCESS_FINE_LOCATION,
        android.Manifest.permission.BLUETOOTH,
        android.Manifest.permission.BLUETOOTH_ADMIN,
        android.Manifest.permission.BLUETOOTH_CONNECT,
        android.Manifest.permission.BLUETOOTH_SCAN
    )

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        statusText = findViewById(R.id.statusText)
        logText = findViewById(R.id.logText)
        sendButton = findViewById(R.id.sendButton)
        reconnectButton = findViewById(R.id.reconnectButton)

        reconnectButton.setOnClickListener { reconnect() }
        sendButton.setOnClickListener { sendClipboardContent() }
        checkPermissions()
    }

    private fun checkPermissions() {
        if (PERMISSIONS.any { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }) {
            ActivityCompat.requestPermissions(this, PERMISSIONS, 0)
        } else {
            initializeBluetooth()
        }
    }

    @SuppressLint("MissingPermission")
    private fun initializeBluetooth() {
        bluetoothAdapter = BluetoothAdapter.getDefaultAdapter().takeIf { it.isEnabled }
            ?: run {
                Toast.makeText(this, "Enable Bluetooth", Toast.LENGTH_LONG).show()
                return
            }
        startDeviceScan()
    }

    @SuppressLint("MissingPermission")
    private fun startDeviceScan() {
        statusText.text = "Scanning..."
        bluetoothAdapter?.bluetoothLeScanner?.startScan(scanCallback)
        handler.postDelayed({ stopScan() }, 10000)
    }

    private fun stopScan() {
        bluetoothAdapter?.bluetoothLeScanner?.stopScan(scanCallback)
    }

    private val scanCallback = object : ScanCallback() {
        @SuppressLint("MissingPermission")
        override fun onScanResult(callbackType: Int, result: ScanResult) {
            if (result.device.name == DEVICE_NAME) {
                stopScan()
                connectToDevice(result.device)
            }
        }
    }

    @SuppressLint("MissingPermission")
    private fun connectToDevice(device: BluetoothDevice) {
        statusText.text = "Connecting..."
        bluetoothGatt = device.connectGatt(this, false, gattCallback)
    }

    private val gattCallback = object : BluetoothGattCallback() {
        @SuppressLint("MissingPermission")
        override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
            when (newState) {
                BluetoothProfile.STATE_CONNECTED -> {
                    isConnected = true
                    handler.post {
                        statusText.text = "Connected"
                        log("Connected")
                    }
                    gatt.discoverServices()
                }
                BluetoothProfile.STATE_DISCONNECTED -> {
                    isConnected = false
                    handler.post {
                        statusText.text = "Disconnected"
                        log("Connection lost")
                    }
                }
            }
        }

        override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
            if (status == BluetoothGatt.GATT_SUCCESS) {
                inputCharacteristic = gatt.getService(SERVICE_UUID)
                    ?.getCharacteristic(INPUT_CHAR_UUID)

                handler.post {
                    if (inputCharacteristic != null) {
                        log("Service found")
                        sendButton.isEnabled = true
                    } else {
                        log("Service not found")
                    }
                }
            }
        }
    }

    private fun sendClipboardContent() {
        if (!isConnected) {
            Toast.makeText(this, "Not connected", Toast.LENGTH_SHORT).show()
            return
        }

        val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
        clipboard.primaryClip?.getItemAt(0)?.text?.toString()?.let { text ->
            if (text.isNotEmpty()) sendData(text)
        } ?: Toast.makeText(this, "Clipboard empty", Toast.LENGTH_SHORT).show()
    }

    @SuppressLint("MissingPermission")
    private fun sendData(text: String) {
        inputCharacteristic?.let {
            it.value = text.toByteArray()
            bluetoothGatt?.writeCharacteristic(it)
            log("Sent: ${text.take(20)}...")
        } ?: log("Characteristic null")
    }

    private fun log(message: String) {
        logText.append("\n$message")
    }

    @SuppressLint("MissingPermission")
    private fun reconnect() {
        log("Reconnect...")
        bluetoothGatt?.disconnect()
        bluetoothGatt?.close()
        bluetoothGatt = null
        inputCharacteristic = null
        sendButton.isEnabled = false
        startDeviceScan()
    }

    override fun onDestroy() {
        super.onDestroy()
        bluetoothGatt?.disconnect()
        bluetoothGatt?.close()
    }
}
activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp">

    <TextView
        android:id="@+id/statusText"
        android:layout_marginTop="20dp"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Status: Disconnected"
        android:textSize="18sp"/>

    <TextView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Log:"
        android:textSize="16sp"/>

    <ScrollView
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:layout_weight="1">

        <TextView
            android:id="@+id/logText"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:textSize="14sp"/>
    </ScrollView>

    <Button
        android:id="@+id/reconnectButton"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Переподключиться"/>

    <Button
        android:id="@+id/sendButton"
        android:layout_width="match_parent"
        android:layout_height="87dp"
        android:enabled="false"
        android:text="Отправить"/>
</LinearLayout>
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.mc.bleapp">

    <uses-permission android:name="android.permission.BLUETOOTH"/>
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"/>
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
    <uses-permission android:name="android.permission.BLUETOOTH_PRIVILEGED"/>
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"/>

    <application
        android:theme="@style/Theme.AppCompat.DayNight.DarkActionBar"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="BLE Keyboard">

        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

    </application>
</manifest>

Дальше приехал контроллер, и вот с ним я бился две недели.

После исправления ошибок компиляции оно прошилось, но не работало.

Первая ошибка -
BT_GATT: gatts_write_attr_perm_check - GATT_INSUF_AUTHENTICATION,handle 002a, perm 0020

Она сыпалась в лог при попытке отправить текст с телефона. Я решил, что раз про аутентификацию — нужно добавить пин‑код при bluetooth‑спаривании. К этому моменту я уже понял, что, хоть нейросеть и понимает контекст, лучше начинать новый чат для новой идеи.

Дальше были ошибки и решения:

error: lvalue required as unary '&' operand -> Заменили #define PIN_CODE на константную переменную const uint32_t PIN_CODE
error: conversion from 'String' to non-scalar type 'std::string' {aka 'std::_cxx11::basic_string'} requested -> std::string value = pCharacteristic->getValue().c_str();

И ещё штук пяток.

Причём, эти ошибки повторяются из итерации в итерацию. Исправил одну — вернул другую. Исправил другую, новая итерация — опа, вернулась первая. Я сначала копировал только текст ошибки, затем перешёл к варианту текст ошибки + строка с ошибкой — ответы значительно улучшились. В любом кодинге помогает опыт =)

В итоге, когда спаривание с пин-кодом случилось, я перешёл к имитации HID-клавиатуры. Тут вроде как проблем не случилось. Ну это только сначала.

error: 'BLEProperties' has not been declared BLEProperties::PROPERTY_NOTIFY -> Ошибка возникает из-за устаревшего синтаксиса. Нужно использовать BLECharacteristic::PROPERTY* вместо BLEProperties::.

Так зачем ты, собака, предлагаешь устаревший синтаксис???

error: 'class BLEAdvertising' has no member named ‘setMinSecurity' -> Удалена строка pAdvertising->setMinSecurity() - эта функция не существует в текущей версии библиотеки

Ну и так далее. Почему сразу не писать без ошибок? Это что, имитация человека?
К этому моменту я прочитал статью про то, что можно в запрос прикладывать файл с кодом. Сохранил работающий код, в котором был только запрос пин-кода, и мы начали сначала:

Ну и снова заново:

error: conversion from 'String' to non-scalar type ‘std::string'

Временами нейросеть что-то придумывала. Например, метод asciiToHID — ей так удобнее конвертировать текст в HID-символы. Оказалось, что «в классе SecurityCallbacks не реализованы все чисто виртуальные методы из базового класса BLESecurityCallbacks». И много ещё всякого, через что мы с ней прошли.

В итоге, я не добился чего хотел в чистом виде. В некоторых вариантах ядро ESP падало в панике, часто после внесения изменений не запрашивало пин-код и не соединялось соответственно. У меня была программа, которая получает из телефона текст и выводит в терминал, но никак не хочет в USB HID. Я взял код из примера для имитации клавиатуры от DeepSeek и попросил дублировать вывод терминала при помощи Keyboard.press:

Пример
#include <USB.h>
#include <USBHIDKeyboard.h>

USBHIDKeyboard Keyboard;

void setup() {
  USB.begin();
  Keyboard.begin();
  delay(2000); // Дать время для подключения USB
  Keyboard.press(KEY_LEFT_CTRL);
  Keyboard.press(KEY_ALT);
  Keyboard.press('t');
  Keyboard.releaseAll(); // Отправка Ctrl+Alt+T (открыть терминал в Linux)
}

void loop() {}

Тут заработало с первого раза. Но только латиница, цифры и спецсимволы. Впрочем, мне этого достаточно.

worked_hid_keyboard.ino
#include <BLEDevice.h>
#include <BLEUtils.h>
#include <BLEServer.h>
#include <BLE2902.h>
#include <USB.h>
#include <USBHIDKeyboard.h>

USBHIDKeyboard Keyboard;

#define DEVICE_NAME "Secure BLE Keyboard"
#define SERVICE_UUID        "4fafc201-1fb5-459e-8fcc-c5c9c331914b"
#define CHARACTERISTIC_UUID "beb5483e-36e1-4688-b7f5-ea07361b26a8"
const uint32_t PIN_CODE = 123456;

bool deviceConnected = false;

class CharacteristicCallbacks : public BLECharacteristicCallbacks {
    void onWrite(BLECharacteristic *pCharacteristic) override {
        String arduinoString = pCharacteristic->getValue().c_str(); 
        std::string value(arduinoString.c_str());
        
        if (!value.empty()) {
            Serial.print("Received: ");
            Serial.println(value.c_str());
            
            // Отправка текста через USB HID
            for (char c : value) {
                Keyboard.press(c); // Нажать клавишу
                delay(10);         // Короткая задержка
                Keyboard.release(c); // Отпустить клавишу
            }
        }
    }
};

class ServerCallbacks: public BLEServerCallbacks {
    void onConnect(BLEServer* pServer) override {
        deviceConnected = true;
        Serial.println("Device connected");
    };

    void onDisconnect(BLEServer* pServer) override {
        deviceConnected = false;
        Serial.println("Device disconnected");
        pServer->startAdvertising();
    }
};

class SecurityCallbacks: public BLESecurityCallbacks {
    bool onConfirmPIN(uint32_t pass_key) override {
        Serial.print("Confirm PIN: ");
        Serial.println(pass_key);
        return (pass_key == PIN_CODE);
    }

    void onAuthenticationComplete(esp_ble_auth_cmpl_t cmpl) override {
        if(cmpl.success) {
            Serial.println("Authentication success");
        } else {
            Serial.println("Authentication failed");
        }
    }

    uint32_t onPassKeyRequest() override {
        return PIN_CODE;
    }

    void onPassKeyNotify(uint32_t pass_key) override {
        Serial.print("PassKey Notify: ");
        Serial.println(pass_key);
    }

    bool onSecurityRequest() override {
        return true;
    }
};

void setup() {
    Serial.begin(115200);
    USB.begin();
    Keyboard.begin();
    delay(2000); // Дать время для подключения USB
    BLEDevice::init(DEVICE_NAME);
    BLEDevice::setEncryptionLevel(ESP_BLE_SEC_ENCRYPT);
    
    BLEServer *pServer = BLEDevice::createServer();
    pServer->setCallbacks(new ServerCallbacks());
    
    BLESecurity *pSecurity = new BLESecurity();
    pSecurity->setAuthenticationMode(ESP_LE_AUTH_REQ_SC_BOND);
    pSecurity->setCapability(ESP_IO_CAP_OUT);
    pSecurity->setInitEncryptionKey(ESP_BLE_ENC_KEY_MASK | ESP_BLE_ID_KEY_MASK);
    BLEDevice::setSecurityCallbacks(new SecurityCallbacks());

    esp_ble_gap_set_security_param(
        ESP_BLE_SM_SET_STATIC_PASSKEY,
        const_cast<uint32_t*>(reinterpret_cast<const uint32_t*>(&PIN_CODE)),
        sizeof(PIN_CODE)
    );

    BLEService *pService = pServer->createService(SERVICE_UUID);
    BLECharacteristic *pCharacteristic = pService->createCharacteristic(
        CHARACTERISTIC_UUID,
        BLECharacteristic::PROPERTY_READ |
        BLECharacteristic::PROPERTY_WRITE
    );

    // Добавлен обработчик входящих данных
    pCharacteristic->setCallbacks(new CharacteristicCallbacks());
    
    pCharacteristic->setValue("Hello World");
    pCharacteristic->addDescriptor(new BLE2902());
    pService->start();

    BLEAdvertising *pAdvertising = pServer->getAdvertising();
    pAdvertising->addServiceUUID(SERVICE_UUID);
    pAdvertising->start();
}

void loop() {
    delay(1000);
}

Осталось только добавить мигание светодиодом и нарисовать корпус (я и проектировщик не настоящий, умею только в SketchUp).
Итого:

  • вайб‑программисту всё равно пришлось побыть программистом. Правил код, добавлял нужное, убирал лишнее. Если б я был условным электронщиком, и DeepSeek бы делал ошибки в рисовании схем — я бы не смог исправить;

  • действительно, лучше разрабатывать шажочками, а не приложение целиком. Один шажочек — один запрос;

  • сохранять работающий код в файл и прикладывать к запросу — это прям здорово, для меня было прорывом;

  • с Android‑приложением получилось хорошо, я в этом совсем ноль был. Теперь умею кнопку по экрану двигать.

На весь процесс ушло 2 недели между делом. Чистого времени часа 2–3. Это, конечно, быстрее, нежели я бы читал доки и писал код самостоятельно.

Ссылка на Google Drive с исходниками, apk и stl для тех, кто хочет так же, а программировать лень.

В написании статьи нейросети участия не принимали, всё сам =)

Комментарии (56)


  1. lv333
    25.05.2025 21:48

    Телефон может прикидывается блютуз клавиатурой... Можно обойтись без лишнего звена. https://play.google.com/store/apps/details?id=io.appground.blek

    С рут правами можно и юсб клаву/мыш/флешку эмулировать https://habr.com/ru/companies/ruvds/articles/816595/


    1. Sergey_Morozov Автор
      25.05.2025 21:48

      Как говорится, "был бы у меня такой кот...", я бы носил на ключах BT-свисток, а не велосипедил ESP. Почему-то не знал, что в таких программах есть поле, куда можно скопировать и отправить текст.


    1. kenomimi
      25.05.2025 21:48

      Телефон может прикидывается блютуз клавиатурой...

      Это не очень жизнеспособно. Донгл требует прав админа на установку, может быть запрещен к подключению, требует дрова, требует постоянного прохождения квеста с подключением (была не одна блютус-клава, и задрало то, что она регулярно не хочет автоматически подключаться)... HID же работает везде и сразу.

      С рут правами можно и юсб клаву/мыш/флешку эмулировать

      Эта идея лучше, но рутованный телефон требует изрядных приседаний, чтобы сохранить работоспособность всех приложений...


      1. lv333
        25.05.2025 21:48

        Это не очень жизнеспособно. Донгл требует прав админа на установку, может быть запрещен к подключению

        Как и левая "клавиатура".

        Если прикинуть, то: если система вам не подконтрольная и не доверенная, то вводить там пароли любым способом небезопасно, так как их очень легко перехватить, как с родной клавы руками, так и с любой эмулируемой. Если же наоборот - то проще просто установить менеджер паролей и не городить весь этот колхоз. Альтернатива по сути только 2-х факторная авторизация...

        Этот же сетап не решает никаких вопросов по факту.


        1. Firelander
          25.05.2025 21:48

          Ну например чтобы на неподконтрольной системе залогиниться на какой-нибудь специфический сайт скачать специфический файл. Если этот конкретный пароль утечет ничего страшного не произойдет, но на ней очень не хочется открывать свой менеджер паролей и вводить мастер пароль


          1. lv333
            25.05.2025 21:48

            Если пароль на специфических сайт который не жалко потерять, то его можно и несложным сделать для набивания на клавиатуре и это будет быстрее чем выполнить весь этот ритуал, ну имхо конечно.


            1. Firelander
              25.05.2025 21:48

              Его всё равно нужно где-то хранить, значит вбивать например с экрана телефона, потому что если он нужен раз в год он просто вылетит из головы, проходили уже. Да и как правило заранее не знаешь где и когда может понадобиться тот или иной пароль, не хочется специально делить пароли на простые и безопасные, плюс на сайтах обычно есть свои требования к паролям, минимальная длина символы итд


              1. Sergey_Morozov Автор
                25.05.2025 21:48

                Это прям мои мысли. У меня пароли в телефоне в KeePass. И я перепечатывал их оттуда.


  1. xSVPx
    25.05.2025 21:48

    Найти бы тоже самое, но с хранением паролей внутри esp, как, к примеру мультипасс делает. Прям хоть пили... Ещеб ТОТР туда же...


    1. PeterFukuyama
      25.05.2025 21:48

      1. xSVPx
        25.05.2025 21:48

        Вероятно. Тонкости в деталях. Я много готовых девайсов поразглядывал, но нормальный подход нашелся только у mooltopass. Но они нифига их не производят и имеют проблемы с аппаратной частью :(( (из-за европейских ограничений у них там никелевые аккумуляторы).


    1. niarbrnd
      25.05.2025 21:48

      Помотри ссылку на озон может подойдет?



    1. here-we-go-again
      25.05.2025 21:48

      А там есть какая-то аппаратная секьюрность уровня телефона?


  1. niarbrnd
    25.05.2025 21:48

    https://ozon.ru/t/NAOiOp9


    1. xSVPx
      25.05.2025 21:48

      Описание неплохое, но нет, пароли по подписке это через край. И подтверждение входа с смартфона тоже. Что то stand alone хотелось.


      1. niarbrnd
        25.05.2025 21:48

        Standalone, это значит ее надо носить с собой, подключать/отключать каждый раз, мы же про пароли, а это же вроде как безопасность. Но если про безопасность не беспокоиться, то можно и standalone делать, но зачем если тогда и пароля 123 достаточно.


        1. xSVPx
          25.05.2025 21:48

          Безопасность может обеспечить только девайс, пароли из которого попадают куда-то наружу только после вашего подтверждения(физически, на нем, кнопкой). И да, они должны храниться внутри, быть зашифрованы, требовать вводить пин при подключении.

          А то, что проходит через телефон по нажатию в нем клавиш имеет меньшую безопасность чем написанные на листочке пароли. А хочется большую.

          В этой штуке хотелось бы хранить пароли от брокеров где семизначные счета, к примеру... От файрволлов корпоративных итд итп. Не от озонов и вайлдбериз... Госуслуги с ТОТР итд итп...


          1. niarbrnd
            25.05.2025 21:48

            У каждого свои требования к безопасности, если для пароля нужен пин, то все сводится к получению пин, который даже по названию обычно цифровой, пином безопасность уже снижается, всегда будет какой-то компромисс, каждый выбирает себе тот который его устраивает.

            Ваш сценарий тоже можно реализовать.

            В предложенном варианте пароль и ключ разделены, удобство пользования телефона сложно переоценить, физическое устройство сложно удаленно сломать, из пк это устройство не сломать через HID.


            1. xSVPx
              25.05.2025 21:48

              Конечно у каждого свой. Мне нужен не хуже, чем у записанного на бумагу пароля...

              Пин позволяет получить пароль если у вас есть физический доступ к токену. Если нет, то пин вам не поможет. Посмотрите как мультипасс сделан, там всё довольно нормально. База паролей зашифрована смарт картой, сама смарт картаа позволяет до окирпичивания ввести три неверных пароля, сами пароли будучи расшифрованными набираются только после нажатия на устройстве физической клавиши, т.е. вы видите на мониторе устройства пароль к какому ресурсу сейчас будете набирать.

              В предложенном вами варианте сломают ваш телефон и сольют все ваши пароли.


              1. niarbrnd
                25.05.2025 21:48

                Где они возьмут ключ для расшифровывания пароля? Он только в устройстве. И нет функции для его извлечения, при каждой смене пароля ключ генерируется заново.


                1. xSVPx
                  25.05.2025 21:48

                  Не очень понятно как это всё работает, но где пароль хранится совершенно неважно если он оттуда набирается по какой-то внешней команде. Инициируют команду, сграббят со стороны куда оно набирается. Возможно придется инфицировать два устройства (и это неплохо).

                  Тут ничего нового не придумать. Если устройство хранения паролей без монитора - невозможно проверить что сейчас происходит. Если без кнопки - все пароли сопрут как только скомпромитируют внешнюю обвязку.


                  1. niarbrnd
                    25.05.2025 21:48

                    Т.е. вариант, когда надо нажать кпопку, потом в течении 2-5секунд, отправить шифрованный пароль и только в таком сочетании вводить данные в пк, вы считаете улучшенным?


                    1. xSVPx
                      25.05.2025 21:48

                      Я не очень понял что вы имеете ввиду.

                      Хорошее устройство показывает на экране что оно собирается вводить (пароль от какого логина) и вводит его после того, как вы на этом устройстве нажимаете кнопку подтверждения. Какие-то дополнительные таймауты обычно отсутствуют. При этом это устройство - это не сотовый телефон. Оно не имеет сетевого доступа и к нему никакого доступа без подтверждения той же кнопкой на нём нету.


                      1. Sergey_Morozov Автор
                        25.05.2025 21:48

                        Такое тоже есть. Мне не понравились размер и необходимость вручную забивать туда пароли при помощи энкодера: https://github.com/seawarrior181/PasswordPump_II


                      1. xSVPx
                        25.05.2025 21:48

                        В нормальном случае оно их само генерит и набирает...

                        Для мультипасса есть редактор, который позволяет их набивать с компьютера. Надо только кнопку подтверждения нажимать и по-моему переспрашивает пин в начале и конце редактирования. (Т.е. можно все отредактировать дважды введя пин)


                      1. niarbrnd
                        25.05.2025 21:48

                        Есть ссылка на мультипасс?


                      1. xSVPx
                        25.05.2025 21:48

                        Гугл mooltipass. В первых версиях была проблема с колесом, в следующих с аккумулятором.


  1. sergyk2
    25.05.2025 21:48

    я для есп начал с примеров которые идут вместе с библиотеками. но не все из них компилируюца, другие прошиваются но не работают.


    1. granv1
      25.05.2025 21:48

      я тебе больше скажу. куча готовых библиотек от "маститых" контор типа адафрут с кучей багов. всё приходится перепроверять и, порой, переписывать


  1. AndyKorg
    25.05.2025 21:48

    Минус такого подхода в том, что в результате не остается глубокого понимания работы компонентов. :(


    1. Sergey_Morozov Автор
      25.05.2025 21:48

      Абсолютно согласен. Но если эти знания нужны условно раз в жизни, - круто, что через три часа можно получить работающее устройство, а не изучать java ради одного приложения.


      1. xSVPx
        25.05.2025 21:48

        Всё-таки макет работающего устройства.

        С потенциально суровыми и непредсказуемыми багами, и не всегда работающий как надо. Для прототипирования пойдет. Для паролей.... вы очень смелый человек :).


  1. kenomimi
    25.05.2025 21:48

    Делал такое на Lilygo токене. Все бы хорошо, но аппаратные косяки умножают идею на ноль. Оно банально перегревается, и помирает дерьмовая паленая spi-флешка - зачем мне менеджер паролей, который в случайный момент перестает работать, или, еще хуже того, втихаря глючит на посыпавшемся флеше... И перепаять чтобы, надо, во-первых, экранчик отклеить - что не очень просто из-за его хрупкости, и, во-вторых, найти в рознице не поддельную industrial-grade флешку, что в наше время тот еще квест.


    1. xSVPx
      25.05.2025 21:48

      О, да, отличная заготовка под нормальный автономный девайс. А заранее залить каким-нибудь термоинтерфейсом жидким не вариант ? Или алюминиевый корпус ей сделать?

      Прям очень перспективно выглядит именно железо. Корпус, конечно дешево не сделать, но его скорее всего всё равно менять... т.е. делать придется в любом случае, зато хоть начинка недорогая.


      1. kenomimi
        25.05.2025 21:48

        Я заливал несколько штук эпоксидкой в металлическом корпусе - не помогает, все сдохли где-то к 50-60 перепрошивке. Флешку там под перепай однозначно, на высокотемпературную. А так да, очень вкеусный девайс.


        1. lv333
          25.05.2025 21:48

          Надо тогда брать самум примитивную ESP32-S3 Zero(или вообще ESP32-C3 Zero, хотя там уже разница в цене не такая уж прям большая что бы экономить, тем более если нужны возможности S3), вон как автор этой статьи, там на борту по сути ничего кроме одного чипа и нет, а если надо больше ПЗУ, просто припаять отдельно модуль под микросд по spi, а потом уже получившийся бутерброд заливать в эпоксидку, ну или не заливать :) Это будет раз в 5 дешевле, а главное, скорее всего не будет иметь тех проблем про которые вы говорите...

          Хотя у S3 4МБ ПЗУ, под любую базу паролей этого точно хватит...


          1. kenomimi
            25.05.2025 21:48

            Ну, после провала с Lilygo я для себя сделал прототитп на LuckFox Pico - тоже не без косяков, но они исправимы, так как материнка под SoM моя. Выходит чуть дороже esp32, но там и сенсорный экран навешивается без проблем, и ридер отпечатков, и mass storage там быстрый - можно запароленную флешку сделать... И самое главное, что там линукс, в котором вся железная часть уже реализована и протестирована сообществом - пишешь простой приклад и вперед. А корпус делается на фотополимернике, работы на полчаса-час во фрикаде...


            1. lv333
              25.05.2025 21:48

              Интересная платка, надо будет прочитать... Можно было ещё наверное вообще rpi zero 2W взять, но это уже явный оверхед:) Хотя она тоже может спокойно питаться от любого юсб порта - проверено.


        1. xSVPx
          25.05.2025 21:48

          Теплопроводность эпоксидки под вопросом. Они точно перегреваются ? Если через прокладку к радиатору прислонить - тоже ? Или флеш такой, что десятки перепрошивок и "всё"?


          1. kenomimi
            25.05.2025 21:48

            Всё в комплексе. Там флешка нонейм или подделка под winbond - уже ресурс мизерный. А ее еще и нагревают до 80 градусов в работе - типовая температура S3... Понятно дело, что она кончается моментально.

            Ее бы просто отодвинуть на 1см от чипа и всё было бы хорошо, но нет.


            1. xSVPx
              25.05.2025 21:48

              Можно прислонить к алюминиевому корпусу через термоинтерфейс(или даже напрямую) и греться не будет.

              Но, похоже надо просто заказать своё из нормальных комплектующих. Надо подумать...


            1. lv333
              25.05.2025 21:48

              Такие платки есть, но и габариты у них побольше, а вообще хз зачем контроллеру все эти объемы ОЗУ и ПЗУ, там где это реально нужно и правда проще уже микрокопьютер с линуксом поставить. А для таких вот поделок, хватит того флеша и ОЗУ что есть уже в чипе S3.


              1. xSVPx
                25.05.2025 21:48

                Тут применение такое, что лучше микрокомпьютеры не использовать. Неизвестно что с линуксом получишь в плане функциональности (что-нибудь закешируется некстати, к примеру)...

                Надо что-то достаточно минималистичное но при этом с монитором и кнопкой...


  1. NutsUnderline
    25.05.2025 21:48

    прикинуться клавиатурой, и с телефона отправить текст.

    хакеры очень любят такую технологию что бы отправлять весьма зловредные тексты (нажатия клавиш). можно было не мучиться с gpt а взять готовый код.


  1. PTM
    25.05.2025 21:48

    а пастильда не нравится?

    https://github.com/thirdpin/pastilda


    1. xSVPx
      25.05.2025 21:48

      Mooltipass гораздо лучше, но с аппаратными проблемами. С пастилдой рано или поздно наберешь невовремя мастер-пароль и он попадет в ПК. Ну и в целом "такое себе", все эти попытки сделать управление от клавиатуры только безмерно всё усложняют. Совершенно не везде будет работать итд итп.


    1. Sergey_Morozov Автор
      25.05.2025 21:48

      Она мне встречалась. Во-первых, нужно ползать отключать-подключать клавиатуру, когда понадобится пароль. Она обычно сзади системника, системник в ногах. Во-вторых, я не понял, как это будет работать с ноутбуком.


  1. Firelander
    25.05.2025 21:48

    Наверное каждый должен написать свой менеджер паролей :) Потому что доверять продукту какой-то компании в этом деле не хочется. Есть конечно опенсорс решения но судя по отзывам в комментариях они имеют свои недостатки. Может дойдут руки сварганить что-нибудь на black pill и ble модуле. Раньше останавливала необходимость писать под андроид но с теперешними нейронками действительно минимальное приложение сварганить должно быть проще


    1. Sergey_Morozov Автор
      25.05.2025 21:48

      Полчаса-час с нуля.


  1. Albert_latariya
    25.05.2025 21:48

    Тоже недавно загорелся такой штукой.

    Но за основу взял esp32 2432s028. Там esp32, сенсорный цветной дисплей и слот sd карты на одной плате. Плюс из мелочей: фоторезистор, rgb светодион и пара распаянных коннекторов.

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


    1. lv333
      25.05.2025 21:48

      Сам смотрел на эти платки не раз, но мне тут люди говорят, что они говно редкое, это все перегревается и "расклеивается" очень быстро. Короче прикольно компоновка плотная, маленькая платка, но... Работать нормально не может...

      При включении расшифровывать файл вводом пин кода на экране, выбирать необходимый пароль и отправлять по блютусу.

      Это все можно реализовать на смартфоне, лишнее устройство как скрипач - ненужно! :)


      1. xSVPx
        25.05.2025 21:48

        На смартфоне можно конечно. Если ты согласен отдать все бабки когда этот смартфон взломают, то тебе всё это вообще не нужно. Можно пароль использовать везде один 111111, его легко запомнить и быстро набирать.


        1. lv333
          25.05.2025 21:48

          Ну штош, делай тогда на китайской платке, которая в любой момент сгорит, а главное нигде больше кроме нее его не храни! Вдруг сломают!


          1. xSVPx
            25.05.2025 21:48

            Спасибо за совет. Однако если хочешь я тебе базу моих паролей пришлю за 1000руб. Сможешь расшифровать - всё твое.

            А мастер-пароль да, нигде больше не хранится в электронном виде. Заперт в двух сейфах напечатанный на бумажке.

            ЗЫ. Иногда лучше жевать ей богу...


    1. sergyk2
      25.05.2025 21:48

      я бы смотрел в сторону более компактного, хтя ввод пароля с экрана будет затруднён

      https://m.media-amazon.com/images/I/61xCcUlX35L._AC_UF1000,1000_QL80_.jpg