-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathupload.go
More file actions
339 lines (303 loc) · 13.7 KB
/
upload.go
File metadata and controls
339 lines (303 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
package ffsendgo
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"time"
"github.com/gorilla/websocket"
"golang.org/x/crypto/hkdf"
)
// NewKeychainFromRand создает экземпляр Keychain со случайно сгенерированным мастер-ключом
func NewKeychainFromRand() (*Keychain, error) {
masterKey := make([]byte, 16)
if _, err := rand.Read(masterKey); err != nil {
return nil, fmt.Errorf("не удалось сгенерировать случайный ключ: %w", err)
}
return NewKeychain(masterKey)
}
// NewKeychain создает новый экземпляр Keychain из 16-ти байтного мастер-ключа,
// из него с помощью HKDF выводятся ключ аутентификации и ключ метаданных
func NewKeychain(masterKey []byte) (*Keychain, error) {
if len(masterKey) != 16 {
return nil, fmt.Errorf("мастер-ключ должен быть длиной 16 байт")
}
authKey, err := deriveKey(masterKey, nil, hkdfInfoAuth, 64)
if err != nil {
return nil, fmt.Errorf("не удалось получить ключ аутентификации: %w", err)
}
metaKey, err := deriveKey(masterKey, nil, hkdfInfoMetadata, 16)
if err != nil {
return nil, fmt.Errorf("не удалось получить ключ метаданных: %w", err)
}
return &Keychain{MasterKey: masterKey, AuthKey: authKey, MetaKey: metaKey}, nil
}
// MasterKeyB64 возвращает мастер-ключ в кодировке base64 Raw URL-safe
func (k *Keychain) MasterKeyB64() string { return base64.RawURLEncoding.EncodeToString(k.MasterKey) }
// AuthKeyB64 возвращает ключ аутентификации в виде base64-строки Raw URL-safe
func (k *Keychain) AuthKeyB64() string { return base64.RawURLEncoding.EncodeToString(k.AuthKey) }
// EncryptMetadata шифрует метаданные с помощью AES-GCM, возвращает зашифрованные данные в виде строки base64 Raw URL-safe
func (k *Keychain) EncryptMetadata(metadataJSON []byte) (string, error) {
block, err := aes.NewCipher(k.MetaKey)
if err != nil {
return "", err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, gcm.NonceSize())
encrypted := gcm.Seal(nil, nonce, metadataJSON, nil)
return base64.RawURLEncoding.EncodeToString(encrypted), nil
}
// deriveKey производит деривацию ключа с помощью HKDF-SHA256
func deriveKey(secret, salt []byte, info string, length int) ([]byte, error) {
hkdfReader := hkdf.New(sha256.New, secret, salt, []byte(info))
key := make([]byte, length)
if _, err := io.ReadFull(hkdfReader, key); err != nil {
return nil, err
}
return key, nil
}
// newEceWriter инициализирует eceWriter: генерирует соль и применяет HKDF к IKM для получения ключа шифрования и основы nonce AES-GCM.
func newEceWriter(ws *websocket.Conn, ikm []byte, fileSize int64) (*eceWriter, error) {
salt := make([]byte, eceSaltLen)
if _, err := rand.Read(salt); err != nil {
return nil, fmt.Errorf("не удалось сгенерировать соль: %w", err)
}
contentEncKey, err := deriveKey(ikm, salt, hkdfInfoEncryption, eceKeyLen)
if err != nil {
return nil, err
}
nonceBase, err := deriveKey(ikm, salt, hkdfInfoNonce, eceNonceLen)
if err != nil {
return nil, err
}
block, err := aes.NewCipher(contentEncKey)
if err != nil {
return nil, err
}
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
return &eceWriter{
ws: ws, ikm: ikm, salt: salt, contentEncKey: contentEncKey, nonceBase: nonceBase,
gcm: gcm, buffer: new(bytes.Buffer), fileSize: fileSize,
}, nil
}
// WriteHeader записывает заголовок ECE (16-байтовая соль, 4-байтовый размер записи, 1-байтовый отступ) в нижележащее websocket-соединение
func (ew *eceWriter) WriteHeader() error {
header := make([]byte, eceHeaderLen)
copy(header[0:16], ew.salt)
binary.BigEndian.PutUint32(header[16:20], eceRecordSize)
header[20] = 0
return ew.ws.WriteMessage(websocket.BinaryMessage, header)
}
// Write записывает данные во внутренний буфер, при накоплении полной ECE-записи, шифрует ее и передает в нижележащий io.Writer
func (ew *eceWriter) Write(p []byte) (int, error) {
n, err := ew.buffer.Write(p)
if err != nil {
return n, err
}
for ew.buffer.Len() >= eceRecordSize-1-eceTagLen {
if err := ew.encryptAndSendChunk(false); err != nil {
return n, err
}
}
return n, nil
}
// Close сбрасывает буфер, отправляя последнюю ECE-запись, и закрывает поток
func (ew *eceWriter) Close() error {
if ew.buffer.Len() > 0 || ew.totalWritten == 0 {
if err := ew.encryptAndSendChunk(true); err != nil {
return err
}
}
return ew.ws.WriteMessage(websocket.BinaryMessage, []byte{0})
}
// encryptAndSendChunk шифрует чанк данных AES-GCM, добавляет ECE-паддинг по флагу last (1 — не финальный, 2 — финальный)
// и отправляет через WebSocket, после отправки инкрементирует счётчик последовательности
func (ew *eceWriter) encryptAndSendChunk(last bool) error {
chunkSize := eceRecordSize - 1 - eceTagLen
plaintext := make([]byte, chunkSize)
n, _ := ew.buffer.Read(plaintext)
ew.totalWritten += int64(n)
var paddedPlaintext []byte
if last {
paddedPlaintext = append(plaintext[:n], 2)
} else {
paddedPlaintext = make([]byte, chunkSize+1)
copy(paddedPlaintext, plaintext[:n])
paddedPlaintext[n] = 1
}
nonce := ew.getNonceForSeq(ew.seq)
ciphertext := ew.gcm.Seal(nil, nonce, paddedPlaintext, nil)
ew.seq++
return ew.ws.WriteMessage(websocket.BinaryMessage, ciphertext)
}
// getNonceForSeq генерирует nonce для указанного номера последовательности seq,
// путем XOR-операции между последними 4 байтами ew.nonceBase и значением seq
func (ew *eceWriter) getNonceForSeq(seq uint32) []byte {
nonce := make([]byte, eceNonceLen)
copy(nonce, ew.nonceBase)
val := binary.BigEndian.Uint32(nonce[eceNonceLen-4:])
val ^= seq
binary.BigEndian.PutUint32(nonce[eceNonceLen-4:], val)
return nonce
}
// uploadFile выполняет полную процедуру шифрования на лету и потоковой ws-выгрузки файла на указанного провайдера,
// возвращает ответ сервера и сгенерированный Keychain для доступа к файлу по ссылке
func uploadFile(filePath string, provider ProviderInfo) (UploadResponse, *Keychain, error) {
file, err := os.Open(filePath)
if err != nil {
return UploadResponse{}, nil, fmt.Errorf("не удалось открыть файл: %w", err)
}
defer func() {
err = errors.Join(err, file.Close())
}()
stat, err := file.Stat()
if err != nil {
return UploadResponse{}, nil, fmt.Errorf("не удалось получить информацию о файле: %w", err)
}
keychain, err := NewKeychainFromRand()
if err != nil {
return UploadResponse{}, nil, err
}
fileName := filepath.Base(filePath)
metadata := FileMetadataV3{Name: fileName, Type: "application/octet-stream", Size: stat.Size(),
Manifest: Manifest{Files: []ManifestFile{{Name: fileName, Type: "application/octet-stream", Size: stat.Size()}}},
}
metadataJSON, _ := json.Marshal(metadata)
encryptedMetadata, err := keychain.EncryptMetadata(metadataJSON)
if err != nil {
return UploadResponse{}, nil, fmt.Errorf("не удалось зашифровать метаданные: %w", err)
}
fileInfo := &FileInfo{
TimeLimit: provider.MaxExpireSeconds,
DownloadLimit: provider.MaxDownloads,
FileMetadata: encryptedMetadata,
Authorization: "send-v1 " + keychain.AuthKeyB64(),
}
fileInfoJSON, _ := json.Marshal(fileInfo)
log.Printf("⚙️ Параметры загрузки: имя файла '%s', размер %s, время жизни %d сек, скачиваний %d.",
fileName, HumanReadableSize(stat.Size()), fileInfo.TimeLimit, fileInfo.DownloadLimit)
u, err := url.Parse(provider.URL)
if err != nil {
return UploadResponse{}, nil, fmt.Errorf("неверный URL хоста: %w", err)
}
u.Path = "/api/ws"
u.Scheme = strings.Replace(u.Scheme, "http", "ws", 1)
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
return UploadResponse{}, nil, fmt.Errorf("не удалось установить WebSocket-соединение: %w", err)
}
defer func() {
err = errors.Join(err, conn.Close())
}()
if err := conn.WriteMessage(websocket.TextMessage, fileInfoJSON); err != nil {
return UploadResponse{}, nil, fmt.Errorf("не удалось отправить метаданные: %w", err)
}
var uploadResp UploadResponse
if err := conn.ReadJSON(&uploadResp); err != nil {
return UploadResponse{}, nil, fmt.Errorf("не удалось получить ответ от сервера: %w", err)
}
log.Printf("✅ Сервер подтвердил загрузку. ID файла: %s", uploadResp.ID)
log.Println("🚀 Начало шифрования и отправки файла...")
writer, err := newEceWriter(conn, keychain.MasterKey, stat.Size())
if err != nil {
return UploadResponse{}, nil, fmt.Errorf("не удалось инициализировать ECE-шифратор: %w", err)
}
if err := writer.WriteHeader(); err != nil {
return UploadResponse{}, nil, fmt.Errorf("не удалось отправить ECE-заголовок: %w", err)
}
if _, err := io.Copy(writer, file); err != nil {
return UploadResponse{}, nil, fmt.Errorf("ошибка во время копирования и шифрования файла: %w", err)
}
if err := writer.Close(); err != nil {
return UploadResponse{}, nil, fmt.Errorf("ошибка при завершении ECE-потока: %w", err)
}
var statusResp UploadStatusResponse
if err := conn.ReadJSON(&statusResp); err != nil {
return UploadResponse{}, nil, fmt.Errorf("не удалось получить финальное подтверждение: %w", err)
}
if !statusResp.OK {
return UploadResponse{}, nil, fmt.Errorf("сервер сообщил о неудачной загрузке")
}
return uploadResp, keychain, nil
}
// Uploader проверяет и готовит цель выгрузки (архивирует директории), выбирает хосты и запускает uploadFile
//
// - inputPath: путь к файлу или директории
//
// - host: URL конкретного хоста (опционально, при nil выбор автоматический)
//
// - all: флаг загрузки на все подходящие хосты (при true), иначе останов после первого успеха
//
// возвращает ошибку при неудачной загрузке на все хосты или ошибке подготовки.
func Uploader(inputPath string, host *string, all *bool) error {
uploadPath, fileInfo, cleanup, err := prepareUploadTarget(inputPath)
if err != nil {
return fmt.Errorf("ошибка подготовки: %w", err)
}
defer cleanup()
var hostsToUpload []ProviderInfo
if host != nil && *host != "" {
log.Printf("🔎 получение конфигурации для указанного хоста: %s", *host)
client := &http.Client{Timeout: 10 * time.Second}
providerInfo, err := fetchProviderConfig(*host, client)
if err != nil {
return fmt.Errorf("не удалось получить конфигурацию для хоста %s: %w", *host, err)
}
if providerInfo.MaxFileSize < fileInfo.Size() {
return fmt.Errorf("указанный хост %s не поддерживает файлы размером %s (лимит: %s)",
*host, HumanReadableSize(fileInfo.Size()), HumanReadableSize(providerInfo.MaxFileSize))
}
hostsToUpload = append(hostsToUpload, *providerInfo)
} else {
hosts, err := findAndSelectHosts(fileInfo.Size())
if err != nil {
return fmt.Errorf("ошибка выбора инстанса: %w", err)
}
hostsToUpload = hosts
}
var successfulUploads int
for i, provider := range hostsToUpload {
log.Printf("\n--- [%d/%d] Попытка загрузки на: %s ---", i+1, len(hostsToUpload), provider.URL)
uploadResp, keychain, err := uploadFile(uploadPath, provider)
if err != nil {
log.Printf("❌ ошибка загрузки на %s: %v", provider.URL, err)
continue
}
successfulUploads++
browserURL, _ := url.Parse(uploadResp.URL)
browserURL.Fragment = keychain.MasterKeyB64()
fmt.Printf("🎉 Файл успешно загружен на %s!\n", provider.URL)
fmt.Printf("🔗 Ссылка: %s\n", browserURL.String())
// прерываем цикл после первого успеха, если -all не указан
if all == nil || !*all {
log.Println("\n✅ успешная загрузка завершена, остановка (флаг -all не указан).")
break
}
}
if successfulUploads == 0 {
return fmt.Errorf("не удалось загрузить файл ни на один из инстансов")
}
if all != nil && *all {
log.Printf("\n✅ успешно загружено на %d из %d инстансов.", successfulUploads, len(hostsToUpload))
}
return nil
}