diff --git a/README.md b/README.md index f42fdf3..6dade0f 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,26 @@ - **文件夹传输**:支持发送整个文件夹结构。 - **文本传输**:快速同步设备间的文本内容。 - **加密传输**:确保数据在传输过程中的安全性。 +- **安全身份**:基于 Ed25519 的签名验证,防止伪造。 + +## 安全机制 + +Mesh Drop 采用多层安全设计来保护用户免受潜在的恶意攻击: + +1. **身份验证 (Identity)** + - 每个设备在首次启动时生成一对唯一的 Ed25519 密钥。 + - 所有广播包(Presence Broadcast)都使用私钥签名。 + - 接收端通过公钥验证签名,确保身份未被篡改。 + +2. **信任机制 (Trust)** + - 采用 TOFU (Trust On First Use) 策略。 + - 用户可以选择“信任”某个 Peer,一旦信任,该 Peer 的公钥将被固定(Pinning)。 + - 之后收到该 Peer ID 的所有数据包,必须通过已保存公钥的验证,否则会被标记为 **Mismatch**。 + - **防欺骗**:如果有人试图伪造已信任 Peer 的 ID,UI 会显示明显的“Mismatch”安全警告,并阻止元数据被覆盖。 + +3. **传输加密 (Encryption)** + - 文件传输服务使用 HTTPS 协议。 + - 自动生成自签名证书进行通信加密,防止传输内容被窃听。 ## 截图 @@ -27,8 +47,9 @@ - [x] 清理历史 - [x] 自动接收 - [x] 应用图标 +- [x] 信任Peer +- [ ] 通过IP添加非局域网Peer - [ ] 系统托盘(最小化到托盘)徽章 https://github.com/wailsapp/wails/issues/4494 -- [ ] 收藏Peer - [ ] 多语言 ## 技术栈 diff --git a/build/linux/Taskfile.yml b/build/linux/Taskfile.yml index e56049f..8e41350 100644 --- a/build/linux/Taskfile.yml +++ b/build/linux/Taskfile.yml @@ -21,7 +21,7 @@ tasks: # 1. Cross-compiling from non-Linux, OR # 2. No C compiler is available, OR # 3. Target architecture differs from host architecture (cross-arch compilation) - - task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}{{else}}build:docker{{end}}' + - task: '{{if and (eq OS "linux") (eq .HAS_CC "true") (eq .TARGET_ARCH ARCH)}}build:native{{else}}build:docker{{end}}' vars: ARCH: "{{.ARCH}}" DEV: "{{.DEV}}" diff --git a/build/linux/mesh-drop.desktop b/build/linux/mesh-drop.desktop index e9ba2c9..736921a 100755 --- a/build/linux/mesh-drop.desktop +++ b/build/linux/mesh-drop.desktop @@ -3,8 +3,8 @@ Type=Application Name=mesh-drop Exec=mesh-drop Icon=mesh-drop -Categories=GTK;FileTransfer;Utility; +Categories=GTK;Utility Terminal=false -Keywords=file transfer +Keywords=utility Version=1.0 StartupNotify=false diff --git a/frontend/bindings/mesh-drop/internal/config/config.ts b/frontend/bindings/mesh-drop/internal/config/config.ts index ff10861..e8e365f 100644 --- a/frontend/bindings/mesh-drop/internal/config/config.ts +++ b/frontend/bindings/mesh-drop/internal/config/config.ts @@ -9,6 +9,10 @@ import { Call as $Call, CancellablePromise as $CancellablePromise, Create as $Cr // @ts-ignore: Unused imports import * as $models from "./models.js"; +export function AddTrustedPeer(peerID: string, publicKey: string): $CancellablePromise { + return $Call.ByID(2866399505, peerID, publicKey); +} + export function GetAutoAccept(): $CancellablePromise { return $Call.ByID(2605668438); } @@ -29,16 +33,30 @@ export function GetSavePath(): $CancellablePromise { return $Call.ByID(4081533263); } +export function GetTrustedPeer(): $CancellablePromise<{ [_: string]: string }> { + return $Call.ByID(1253442080).then(($result: any) => { + return $$createType0($result); + }); +} + export function GetVersion(): $CancellablePromise { return $Call.ByID(3578438023); } export function GetWindowState(): $CancellablePromise<$models.WindowState> { return $Call.ByID(341414414).then(($result: any) => { - return $$createType0($result); + return $$createType1($result); }); } +export function IsTrustedPeer(peerID: string): $CancellablePromise { + return $Call.ByID(3452062706, peerID); +} + +export function RemoveTrustedPeer(peerID: string): $CancellablePromise { + return $Call.ByID(909233322, peerID); +} + /** * Save 保存配置到磁盘 */ @@ -70,4 +88,5 @@ export function SetWindowState(state: $models.WindowState): $CancellablePromise< } // Private type creation functions -const $$createType0 = $models.WindowState.createFrom; +const $$createType0 = $Create.Map($Create.Any, $Create.Any); +const $$createType1 = $models.WindowState.createFrom; diff --git a/frontend/bindings/mesh-drop/internal/discovery/models.ts b/frontend/bindings/mesh-drop/internal/discovery/models.ts index 88e0c3d..24da8d6 100644 --- a/frontend/bindings/mesh-drop/internal/discovery/models.ts +++ b/frontend/bindings/mesh-drop/internal/discovery/models.ts @@ -47,6 +47,13 @@ export class Peer { */ "port": number; "os": OS; + "pk": string; + + /** + * TrustMismatch 指示该节点的公钥与本地信任列表中的公钥不匹配 + * 如果为 true,说明可能存在 ID 欺骗或密钥轮换 + */ + "trust_mismatch": boolean; /** Creates a new Peer instance. */ constructor($$source: Partial = {}) { @@ -65,6 +72,12 @@ export class Peer { if (!("os" in $$source)) { this["os"] = OS.$zero; } + if (!("pk" in $$source)) { + this["pk"] = ""; + } + if (!("trust_mismatch" in $$source)) { + this["trust_mismatch"] = false; + } Object.assign(this, $$source); } diff --git a/frontend/bindings/mesh-drop/internal/discovery/service.ts b/frontend/bindings/mesh-drop/internal/discovery/service.ts index 0854693..6324a62 100644 --- a/frontend/bindings/mesh-drop/internal/discovery/service.ts +++ b/frontend/bindings/mesh-drop/internal/discovery/service.ts @@ -17,22 +17,29 @@ export function GetLocalIPInSameSubnet(receiverIP: string): $CancellablePromise< return $Call.ByID(3089425954, receiverIP); } -export function GetLocalIPs(): $CancellablePromise<[string[], boolean]> { - return $Call.ByID(2403939179).then(($result: any) => { - $result[0] = $$createType0($result[0]); +export function GetPeerByID(id: string): $CancellablePromise<[$models.Peer | null, boolean]> { + return $Call.ByID(1962377788, id).then(($result: any) => { + $result[0] = $$createType1($result[0]); return $result; }); } -export function GetPeerByIP(ip: string): $CancellablePromise<$models.Peer | null> { +export function GetPeerByIP(ip: string): $CancellablePromise<[$models.Peer | null, boolean]> { return $Call.ByID(1626825408, ip).then(($result: any) => { - return $$createType2($result); + $result[0] = $$createType1($result[0]); + return $result; }); } export function GetPeers(): $CancellablePromise<$models.Peer[]> { return $Call.ByID(3041084029).then(($result: any) => { - return $$createType3($result); + return $$createType2($result); + }); +} + +export function GetSelf(): $CancellablePromise<$models.Peer> { + return $Call.ByID(3599633538).then(($result: any) => { + return $$createType0($result); }); } @@ -41,7 +48,6 @@ export function Start(): $CancellablePromise { } // Private type creation functions -const $$createType0 = $Create.Array($Create.Any); -const $$createType1 = $models.Peer.createFrom; -const $$createType2 = $Create.Nullable($$createType1); -const $$createType3 = $Create.Array($$createType1); +const $$createType0 = $models.Peer.createFrom; +const $$createType1 = $Create.Nullable($$createType0); +const $$createType2 = $Create.Array($$createType0); diff --git a/frontend/bindings/mesh-drop/internal/transfer/index.ts b/frontend/bindings/mesh-drop/internal/transfer/index.ts index 55b474e..c3c044c 100644 --- a/frontend/bindings/mesh-drop/internal/transfer/index.ts +++ b/frontend/bindings/mesh-drop/internal/transfer/index.ts @@ -9,7 +9,6 @@ export { export { ContentType, Progress, - Sender, Transfer, TransferStatus, TransferType diff --git a/frontend/bindings/mesh-drop/internal/transfer/models.ts b/frontend/bindings/mesh-drop/internal/transfer/models.ts index 856fb6c..c5c4553 100644 --- a/frontend/bindings/mesh-drop/internal/transfer/models.ts +++ b/frontend/bindings/mesh-drop/internal/transfer/models.ts @@ -5,6 +5,10 @@ // @ts-ignore: Unused imports import { Create as $Create } from "@wailsio/runtime"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as discovery$0 from "../discovery/models.js"; + export enum ContentType { /** * The Go zero value for the underlying type of the enum. @@ -59,46 +63,6 @@ export class Progress { } } -export class Sender { - /** - * 发送者 ID - */ - "id": string; - - /** - * 发送者名称 - */ - "name": string; - - /** - * 发送者 IP - */ - "ip": string; - - /** Creates a new Sender instance. */ - constructor($$source: Partial = {}) { - if (!("id" in $$source)) { - this["id"] = ""; - } - if (!("name" in $$source)) { - this["name"] = ""; - } - if (!("ip" in $$source)) { - this["ip"] = ""; - } - - Object.assign(this, $$source); - } - - /** - * Creates a new Sender instance from a string or object. - */ - static createFrom($$source: any = {}): Sender { - let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; - return new Sender($$parsedSource as Partial); - } -} - /** * Transfer */ @@ -116,7 +80,7 @@ export class Transfer { /** * 发送者 */ - "sender": Sender; + "sender": discovery$0.Peer; /** * 文件名 @@ -177,7 +141,7 @@ export class Transfer { this["create_time"] = 0; } if (!("sender" in $$source)) { - this["sender"] = (new Sender()); + this["sender"] = (new discovery$0.Peer()); } if (!("file_name" in $$source)) { this["file_name"] = ""; @@ -256,5 +220,5 @@ export enum TransferType { }; // Private type creation functions -const $$createType0 = Sender.createFrom; +const $$createType0 = discovery$0.Peer.createFrom; const $$createType1 = Progress.createFrom; diff --git a/frontend/src/components/MainLayout.vue b/frontend/src/components/MainLayout.vue index 674d479..25b598c 100644 --- a/frontend/src/components/MainLayout.vue +++ b/frontend/src/components/MainLayout.vue @@ -66,12 +66,10 @@ onMounted(async () => { // --- 后端集成 & 事件监听 --- onMounted(async () => { peers.value = await GetPeers(); - peers.value = peers.value.sort((a, b) => a.name.localeCompare(b.name)); }); Events.On("peers:update", (event) => { peers.value = event.data; - peers.value = peers.value.sort((a, b) => a.name.localeCompare(b.name)); }); Events.On("transfer:refreshList", async () => { diff --git a/frontend/src/components/PeerCard.vue b/frontend/src/components/PeerCard.vue index afec9d2..80b6a67 100644 --- a/frontend/src/components/PeerCard.vue +++ b/frontend/src/components/PeerCard.vue @@ -1,6 +1,6 @@