feat: add filemanager session and crypto version detection (#516)

* feat: add filemanager session and crypto version detection

* refactor: move copy logic into filemanager, remove fileutil dependency

* fix: apply review suggestions for filemanager

* feat: add Windows locked file tests, fix readFileContent with ReadFile+FileMapping fallback

* fix: remove self-PID skip in findFileHandle to fix Windows CI test

* fix: seek to file start before reading duplicated handle

* fix: use full path matching in findFileHandle to avoid cross-app handle collision

* test: enhance Windows copyLocked tests with write-then-read, large file, and normal copy scenarios

* fix: check all errors in Windows tests, use bytes.Equal for large file comparison

* fix: use stable path suffix matching to handle Windows short path names in CI
This commit is contained in:
Roger
2026-03-25 23:54:22 +08:00
committed by moonD4rk
parent e86e3e62d6
commit 12436217ae
8 changed files with 809 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
package crypto
// CipherVersion represents the encryption version used by Chromium browsers.
type CipherVersion string
const (
// CipherV10 is Chrome 80+ encryption (AES-GCM on Windows, AES-CBC on macOS/Linux).
CipherV10 CipherVersion = "v10"
// CipherV20 is Chrome 127+ App-Bound Encryption.
CipherV20 CipherVersion = "v20"
// CipherDPAPI is pre-Chrome 80 raw DPAPI encryption (no version prefix).
CipherDPAPI CipherVersion = "dpapi"
)
// DetectVersion identifies the encryption version from a ciphertext prefix.
func DetectVersion(ciphertext []byte) CipherVersion {
if len(ciphertext) < 3 {
return CipherDPAPI
}
prefix := string(ciphertext[:3])
switch prefix {
case "v10":
return CipherV10
case "v20":
return CipherV20
default:
return CipherDPAPI
}
}
// StripPrefix removes the version prefix (e.g. "v10") from ciphertext.
// Returns the ciphertext unchanged if no known prefix is found.
func StripPrefix(ciphertext []byte) []byte {
ver := DetectVersion(ciphertext)
if ver == CipherV10 || ver == CipherV20 {
return ciphertext[3:]
}
return ciphertext
}
+47
View File
@@ -0,0 +1,47 @@
package crypto
import (
"testing"
"github.com/stretchr/testify/assert"
)
func TestDetectVersion(t *testing.T) {
tests := []struct {
name string
ciphertext []byte
want CipherVersion
}{
{"v10 prefix", []byte("v10" + "encrypted_data"), CipherV10},
{"v20 prefix", []byte("v20" + "encrypted_data"), CipherV20},
{"no prefix (DPAPI)", []byte{0x01, 0x00, 0x00, 0x00}, CipherDPAPI},
{"short input", []byte{0x01, 0x02}, CipherDPAPI},
{"empty input", []byte{}, CipherDPAPI},
{"nil input", nil, CipherDPAPI},
{"unknown prefix", []byte("xyz_data"), CipherDPAPI},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, DetectVersion(tt.ciphertext))
})
}
}
func TestStripPrefix(t *testing.T) {
tests := []struct {
name string
ciphertext []byte
want []byte
}{
{"strips v10", []byte("v10PAYLOAD"), []byte("PAYLOAD")},
{"strips v20", []byte("v20PAYLOAD"), []byte("PAYLOAD")},
{"keeps DPAPI unchanged", []byte{0x01, 0x00, 0x00}, []byte{0x01, 0x00, 0x00}},
{"keeps short unchanged", []byte{0x01}, []byte{0x01}},
{"keeps nil unchanged", nil, nil},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, StripPrefix(tt.ciphertext))
})
}
}