package lkgateway import ( "context" "fmt" "io" "net/http" "os" "os/user" "path/filepath" "strings" "time" ) // FlashContainer — найденный на смонтированной флешке контейнер КриптоПро. // КриптоПро CSP под Linux ожидает контейнер в виде папки .000 с // файлами header.key/masks.key/name.key/primary.key/primary2.key. type FlashContainer struct { // Mountpoint — путь смонтированной флешки, например /run/media/user/USB. Mountpoint string // Path — полный путь до папки .000. Path string // Name — имя контейнера (без суффикса .000). Name string // Files — список файлов в контейнере (для дисплея). Files []string // AlreadyImported — true, если папка .000 уже есть в локальном // хранилище /var/opt/cprocsp/keys//. AlreadyImported bool } // scanFlashContainers ищет контейнеры формата .000 на типичных // точках монтирования USB-носителей в Linux: /run/media//* и // /media//* и /media/*. Возвращает список найденных контейнеров. func scanFlashContainers() []FlashContainer { u, err := user.Current() if err != nil { return nil } roots := []string{ filepath.Join("/run/media", u.Username), filepath.Join("/media", u.Username), "/media", "/mnt", } localKeysDir := filepath.Join("/var/opt/cprocsp/keys", u.Username) var out []FlashContainer for _, root := range roots { entries, err := os.ReadDir(root) if err != nil { continue } for _, e := range entries { if !e.IsDir() { continue } mountpoint := filepath.Join(root, e.Name()) out = append(out, findContainersAt(mountpoint, localKeysDir)...) } } return out } func findContainersAt(mountpoint, localKeysDir string) []FlashContainer { var out []FlashContainer // Ищем папки .000 на верхнем уровне и на 1 уровне вглубь. _ = filepath.Walk(mountpoint, func(p string, info os.FileInfo, err error) error { if err != nil { return nil } // Глубже 2 уровней не лезем (на флешке могут быть личные папки). rel, _ := filepath.Rel(mountpoint, p) if strings.Count(rel, string(filepath.Separator)) > 2 { return filepath.SkipDir } if !info.IsDir() || !strings.HasSuffix(strings.ToLower(p), ".000") { return nil } // Проверяем, что внутри лежат файлы вида *.key. entries, _ := os.ReadDir(p) var files []string hasKey := false for _, ent := range entries { files = append(files, ent.Name()) if strings.HasSuffix(strings.ToLower(ent.Name()), ".key") { hasKey = true } } if !hasKey { return nil } name := strings.TrimSuffix(filepath.Base(p), ".000") fc := FlashContainer{ Mountpoint: mountpoint, Path: p, Name: name, Files: files, } // Проверка: уже скопирован в локальное хранилище? if _, err := os.Stat(filepath.Join(localKeysDir, name+".000")); err == nil { fc.AlreadyImported = true } out = append(out, fc) return filepath.SkipDir }) return out } // copyContainerToLocal копирует папку .000 с флешки в локальное // хранилище КриптоПро /var/opt/cprocsp/keys//.000. После // этого контейнер виден как \\.\HDIMAGE\ и работает даже без // вставленной флешки. func copyContainerToLocal(srcDir string) (string, error) { u, err := user.Current() if err != nil { return "", err } localKeysDir := filepath.Join("/var/opt/cprocsp/keys", u.Username) if err := os.MkdirAll(localKeysDir, 0o700); err != nil { return "", fmt.Errorf("создать %s: %w", localKeysDir, err) } base := filepath.Base(srcDir) dstDir := filepath.Join(localKeysDir, base) if _, err := os.Stat(dstDir); err == nil { return "", fmt.Errorf("контейнер %s уже существует в локальном хранилище", dstDir) } if err := os.MkdirAll(dstDir, 0o700); err != nil { return "", fmt.Errorf("создать %s: %w", dstDir, err) } entries, err := os.ReadDir(srcDir) if err != nil { return "", err } for _, e := range entries { if e.IsDir() { continue } src, err := os.Open(filepath.Join(srcDir, e.Name())) if err != nil { return "", err } dst, err := os.OpenFile(filepath.Join(dstDir, e.Name()), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600) if err != nil { src.Close() return "", err } if _, err := io.Copy(dst, src); err != nil { src.Close() dst.Close() return "", err } src.Close() dst.Close() } return dstDir, nil } // copyContainer — POST /admin/setup/crypto/copy-container. // Параметр src — путь до папки .000 на флешке. func (h *setupHandlers) copyContainer(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method", http.StatusMethodNotAllowed) return } src := strings.TrimSpace(r.FormValue("src")) if src == "" { setupFlash(w, r, "Копирование контейнера: не указан путь") return } // Минимальная защита: ожидаем .000 в конце пути. if !strings.HasSuffix(strings.ToLower(src), ".000") { setupFlash(w, r, "Копирование контейнера: путь должен заканчиваться на .000") return } if _, err := os.Stat(src); err != nil { setupFlash(w, r, "Копирование контейнера: исходная папка недоступна: "+err.Error()) return } dst, err := copyContainerToLocal(src) if err != nil { setupFlash(w, r, "Копирование контейнера: "+err.Error()) return } // Дадим CSP несколько мс «заметить» новый контейнер (не критично). _, cancel := context.WithTimeout(r.Context(), 100*time.Millisecond) cancel() setupFlash(w, r, "Контейнер скопирован в "+dst+". Теперь он виден как \\\\.\\HDIMAGE\\"+strings.TrimSuffix(filepath.Base(dst), ".000")+" и работает без вставленной флешки. Импортируйте сертификат: certmgr -inst -cont '\\\\.\\HDIMAGE\\"+strings.TrimSuffix(filepath.Base(dst), ".000")+"' -store uMy.") }