sync

package
v1.12.3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Nov 15, 2026 License: Apache-2.0 Imports: 36 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrRemoteNotFound = errors.New("sync: remote resource not found")

ErrRemoteNotFound reports that a resource is absent on the sync remote. Every RemoteFS implementation normalizes its backend's own not-found error to this sentinel, so the snapshot engine can tell "remote is empty" (Head returning nil) apart from a real failure.

View Source
var ErrWrongSyncPassword = errors.New("WRONG_SYNC_PASSWORD")

Functions

func DecryptConfigFiles

func DecryptConfigFiles(srcDir, destDir string, key []byte) error

DecryptConfigFiles decrypts the whole default sync set. Kept for callers/tests outside a SyncService scope context (the password rotation and the wrong-key guard work on staging dirs, not the store registry).

func DecryptConfigFilesScoped added in v1.10.0

func DecryptConfigFilesScoped(files []string, srcDir, destDir string, key []byte) error

DecryptConfigFilesScoped decrypts exactly the given config files from srcDir into destDir. files must come from EffectiveSyncFiles. Byte-level: read the encrypted blob, decryptBytes under the sync key, write the plaintext — no field handling. A blob the source never carried is skipped: sync must not stage an empty document that would disagree with the null sentinel hashStoresScoped records for a store whose backing file was never written.

func DeriveKey

func DeriveKey(password string, salt []byte) []byte

DeriveKey derives a 32-byte AES-256 key from a password and salt using PBKDF2-SHA256. DeriveKey derives the sync encryption key. Re-exported from backend/kdf (the shared leaf package credentials also uses), so existing call sites inside and outside the package keep compiling.

func EffectiveSyncFiles added in v1.10.0

func EffectiveSyncFiles(scope []string) []string

EffectiveSyncFiles resolves the user's scope against syncableFiles. A nil/empty scope returns the default set. Unknown names are dropped so a stale scope list from an older or newer build can never smuggle an unintended file into the repo.

func EncryptConfigFiles

func EncryptConfigFiles(srcDir, destDir string, key []byte) error

EncryptConfigFiles encrypts the whole default sync set. Kept for callers/tests outside a SyncService scope context (the password rotation re-encrypts a staging dir rather than going through the store registry).

func EncryptConfigFilesScoped added in v1.10.0

func EncryptConfigFilesScoped(files []string, srcDir, destDir string, key []byte) error

EncryptConfigFilesScoped encrypts exactly the given config files from srcDir into destDir. files must come from EffectiveSyncFiles. The body is byte-level: read the plaintext blob, encryptBytes under the sync key, write — all field-level semantics live in the stores (ExportPlain / ImportPlain), so nothing here knows what a secret field is. A file srcDir never carried is skipped: sync must not upload a document that would clobber the remote copy.

func GenerateSalt

func GenerateSalt() ([]byte, error)

GenerateSalt generates a random 16-byte salt. Re-exported from backend/kdf; see DeriveKey.

func ReadSaltFile

func ReadSaltFile(repoPath string) ([]byte, error)

Returns nil if the file doesn't exist (new repo).

func TestConnection

func TestConnection(repoURL, username, token string) error

TestConnection verifies the repo URL is reachable.

func WriteSaltFile

func WriteSaltFile(repoPath string, salt []byte) error

WriteSaltFile writes the salt to .sync-salt in the repo directory.

Types

type BranchHeads added in v1.9.2

type BranchHeads struct {
	Local  *plumbing.Hash
	Remote *plumbing.Hash
}

BranchHeads is the resolved state of the sync branch after a fetch. A nil Local means HEAD is still unborn (no commits yet); a nil Remote means the remote branch does not exist (empty repository).

type ConflictInfo

type ConflictInfo struct {
	LocalTime  time.Time `json:"localTime"`
	RemoteTime time.Time `json:"remoteTime"`
}

type GitRepo

type GitRepo struct {
	// contains filtered or unexported fields
}

func CloneOrOpen

func CloneOrOpen(repoPath, repoURL, branch, username, token string) (*GitRepo, error)

CloneOrOpen opens the repo at repoPath, or clones it from the given URL.

func (*GitRepo) CommitDepth added in v1.10.0

func (g *GitRepo) CommitDepth() (int, error)

CommitDepth counts commits along first-parent from HEAD. The clone is single-branch, so HEAD is always the sync branch. An unborn HEAD (no commits yet) counts as 0. The result is capped at depthScanCap.

func (*GitRepo) CommitTime added in v1.9.2

func (g *GitRepo) CommitTime(h plumbing.Hash) (time.Time, error)

CommitTime returns the committer timestamp of the given commit.

func (*GitRepo) ExtractCommitFiles added in v1.9.2

func (g *GitRepo) ExtractCommitFiles(hash plumbing.Hash, destDir string) error

ExtractCommitFiles writes the whole default sync set as committed at hash into destDir. The extracted files are still encrypted — the caller decrypts them. Files absent from that commit's tree are skipped.

func (*GitRepo) ExtractCommitFilesIn added in v1.10.0

func (g *GitRepo) ExtractCommitFilesIn(hash plumbing.Hash, files []string, destDir string) error

ExtractCommitFilesIn writes exactly files as committed at hash into destDir (still encrypted). Files absent from the tree are skipped. files must come from EffectiveSyncFiles.

func (*GitRepo) Fetch

func (g *GitRepo) Fetch(username, token string) error

func (*GitRepo) HeadRef added in v1.10.0

func (g *GitRepo) HeadRef() (*plumbing.Reference, error)

HeadRef returns the resolved HEAD branch reference (name and hash), so callers can remember the pre-mutation head and restore it if a later step fails.

func (*GitRepo) MergeBase added in v1.9.2

func (g *GitRepo) MergeBase(a, b plumbing.Hash) (*plumbing.Hash, error)

MergeBase returns the best common ancestor of the two commits, or nil when they share no history.

func (*GitRepo) Pull

func (g *GitRepo) Pull(username, token string) error

func (*GitRepo) Push

func (g *GitRepo) Push(username, token string) error

func (*GitRepo) PushForce added in v1.10.0

func (g *GitRepo) PushForce(username, token string) error

PushForce force-pushes HEAD to its upstream, replacing remote history. Other devices lose their merge base against the new head — Sync handles that explicitly (nil merge base → direct content comparison).

func (*GitRepo) PushToBranch

func (g *GitRepo) PushToBranch(branch, username, token string) error

PushToBranch pushes the current HEAD to the specified remote branch.

func (*GitRepo) ReadRemoteFile

func (g *GitRepo) ReadRemoteFile(branch, filePath string) ([]byte, error)

ReadRemoteFile reads a file from the remote tracking branch without touching the worktree.

func (*GitRepo) ResetToRemote

func (g *GitRepo) ResetToRemote(branch string) error

ResetToRemote resets local HEAD to match remote branch.

func (*GitRepo) ResolveBranchHeads added in v1.9.2

func (g *GitRepo) ResolveBranchHeads(branch string) (BranchHeads, error)

ResolveBranchHeads returns the local HEAD hash and the origin/<branch> hash without assuming either exists.

func (*GitRepo) RestoreHead added in v1.10.0

func (g *GitRepo) RestoreHead(refName plumbing.ReferenceName, hash plumbing.Hash) error

RestoreHead repoints the branch reference at hash — the rollback for a history rewrite whose force-push failed.

func (*GitRepo) RewriteHistory added in v1.10.0

func (g *GitRepo) RewriteHistory(keep int) error

RewriteHistory rebuilds the branch history keeping only the most recent keep commits, cutting everything older. Each rebuilt commit preserves the original tree, author, committer, message and timestamps; the new root has no parent. When the existing depth is within keep, the rebuild reproduces byte-identical commit objects (same hashes) and is a no-op.

Rebuilt hashes differ from the originals once history is actually cut, so other devices lose their merge base against the rewritten chain — Sync must treat a nil merge base explicitly. Only the local branch is repointed; call PushForce to publish the rewritten chain. The remote tracking ref (refs/remotes/origin/...) is NOT updated — it stays stale until the next fetch or a re-clone, which is what the compaction caller does. Merge commits inside the kept window lose their second parent, so even a depth ≤ keep rebuild is only byte-identical for linear history.

func (*GitRepo) StageAndCommit

func (g *GitRepo) StageAndCommit(msg string) (bool, error)

StageAndCommit stages the whole default sync set plus repo metadata. Returns true if committed.

func (*GitRepo) StageAndCommitFiles added in v1.10.0

func (g *GitRepo) StageAndCommitFiles(msg string, files []string) (bool, error)

StageAndCommitFiles stages exactly files plus ".sync-salt"/"README.md". Whitelisting keeps stray files (e.g. an SSH key) out of the repo (SYNC-P1-9). files must come from EffectiveSyncFiles; arbitrary lists weaken the SYNC-P1-9 stray-file whitelist.

type Keychain

type Keychain struct {
	// contains filtered or unexported fields
}

func NewKeychain

func NewKeychain() *Keychain

func (*Keychain) Delete

func (k *Keychain) Delete(key string) error

func (*Keychain) DeleteModelAPIKey

func (k *Keychain) DeleteModelAPIKey(modelID string) error

func (*Keychain) DeletePassword

func (k *Keychain) DeletePassword(connID string) error

func (*Keychain) DeleteRemoteSecret added in v1.10.1

func (k *Keychain) DeleteRemoteSecret(backend string) error

DeleteRemoteSecret removes the credential of a snapshot backend.

func (*Keychain) Get

func (k *Keychain) Get(key string) (string, error)

func (*Keychain) GetEncryptionKey

func (k *Keychain) GetEncryptionKey() ([]byte, error)

GetEncryptionKey retrieves the derived encryption key from OS keychain.

func (*Keychain) GetGitToken

func (k *Keychain) GetGitToken() (string, error)

func (*Keychain) GetModelAPIKey

func (k *Keychain) GetModelAPIKey(modelID string) (string, error)

func (*Keychain) GetPassword

func (k *Keychain) GetPassword(connID string) (string, error)

func (*Keychain) GetRemoteSecret added in v1.10.1

func (k *Keychain) GetRemoteSecret(backend string) (string, error)

GetRemoteSecret reads the password-class credential of a snapshot backend. A missing entry reads as empty, like the WebDAV helper.

func (*Keychain) GetWebDAVPassword added in v1.10.0

func (k *Keychain) GetWebDAVPassword() (string, error)

func (*Keychain) List added in v1.10.1

func (k *Keychain) List() ([]string, error)

List returns the entry names stored under the app's keychain service. Used by credential recovery to find keys written under another data-dir hash (copied/moved data directory). Platforms without enumeration return an error — recovery is simply skipped there.

func (*Keychain) Set

func (k *Keychain) Set(key, value string) error

func (*Keychain) SetGitToken

func (k *Keychain) SetGitToken(token string) error

func (*Keychain) SetModelAPIKey

func (k *Keychain) SetModelAPIKey(modelID, apiKey string) error

func (*Keychain) SetPassword

func (k *Keychain) SetPassword(connID, password string) error

func (*Keychain) SetRemoteSecret added in v1.10.1

func (k *Keychain) SetRemoteSecret(backend, secret string) error

SetRemoteSecret stores (or clears, on empty) the credential of a snapshot backend.

func (*Keychain) SetWebDAVPassword added in v1.10.0

func (k *Keychain) SetWebDAVPassword(pw string) error

func (*Keychain) StoreEncryptionKey

func (k *Keychain) StoreEncryptionKey(key []byte) error

StoreEncryptionKey stores the derived encryption key in OS keychain.

type PasswordStore added in v1.8.0

type PasswordStore interface {
	// Unlocked reports whether the credential store currently holds a usable
	// key. When false (master-password not yet entered, keychain lost, or never
	// set up), sync must refuse to run rather than leak or mis-carry secrets.
	Unlocked() bool
}

PasswordStore is the local credential store as sync sees it. Sync only consults Unlocked: every field-level concern (which fields are secret, enc:v1: encryption, legacy keychain backfill) lives in the stores, so a locked vault is the only thing that can still make sync misbehave.

type RemoteConfig added in v1.10.1

type RemoteConfig struct {
	// Server is the WebDAV server URL; for sftp/smb host[:port]; for s3 the
	// endpoint (required — custom endpoints cover MinIO/OSS/COS, and AWS is
	// reachable through s3.amazonaws.com).
	Server string `json:"server,omitempty"`
	// Path is the remote directory (webdav/sftp/smb) or object key prefix
	// (s3) holding the snapshot.
	Path string `json:"path,omitempty"`
	// User authenticates the share; for s3 it is the access key (the secret
	// key rides in the keychain, matching the session-layer field semantics).
	User string `json:"user,omitempty"`
	// Bucket is s3-only.
	Bucket string `json:"bucket,omitempty"`
	// Region is s3-only (signature scope).
	Region string `json:"region,omitempty"`
	// URLStyle is s3-only: ""/"virtual" uses virtual-hosted URLs — required
	// by OSS/COS/OBS which reject path-style requests; "path" keeps
	// endpoint/bucket/key addressing for AWS S3 and MinIO.
	URLStyle string `json:"urlStyle,omitempty"`
	// Domain is smb-only (NTLM domain; empty = local account).
	Domain string `json:"domain,omitempty"`
	// Share is smb-only: the share name mounted before any file operation.
	Share string `json:"share,omitempty"`
}

RemoteConfig is the connection description shared by every snapshot backend (webdav/sftp/s3/smb). The pattern mirrors the original WebDAV config: server + remote path + user, with the password-class secret kept out of the JSON in the keychain (Keychain.GetRemoteSecret) — only the non-secret fields live here.

type RemoteFS added in v1.10.1

type RemoteFS interface {
	// Probe verifies reachability and credentials. Idempotent.
	Probe() error
	// Get reads a file and returns ErrRemoteNotFound when it is absent.
	Get(name string) ([]byte, error)
	// Put writes a file, creating any missing parent directories.
	Put(name string, data []byte) error
}

RemoteFS is the minimal filesystem view a snapshot backend needs on the remote: verify reachability, read a file, write a file. The snapshot engine (remote_snapshot.go) turns this into the SnapshotBackend contract of Probe, Head, FetchAll and StoreAll, so one engine serves all remotes.

name arguments are bare file names such as manifest.json or connections.json; each implementation owns the base path and joins it.

type SecretStore added in v1.12.3

type SecretStore interface {
	Get(key string) (string, error)
	Set(key, value string) error
	Delete(key string) error
	List() ([]string, error)
}

SecretStore replaces the platform keychain as the backing storage of a Keychain. Headless server containers have no OS keychain (no Secret Service D-Bus), so the server build wires an encrypted-file store here; desktop builds leave it nil and use the platform keychain functions.

type SnapshotBackend added in v1.10.0

type SnapshotBackend interface {
	// Probe verifies reachability/credentials and creates the remote
	// collection if missing.
	Probe() error
	// Head returns the current remote snapshot, or nil when the remote
	// is empty (freshly created / wiped).
	Head() (*SnapshotInfo, error)
	// FetchAll downloads the current snapshot's encrypted resources into
	// dir (.sync-salt + ciphertext files + manifest.json). A missing
	// remote snapshot leaves dir untouched.
	FetchAll(dir string) error
	// StoreAll uploads the encrypted files from stagingDir as a new
	// snapshot with the given salt and manifest. The manifest is written
	// last so readers never observe a half-written snapshot.
	StoreAll(stagingDir string, salt []byte, manifest SnapshotInfo) error
}

SnapshotBackend is the contract for snapshot-type sync remotes.

func NewRemoteSnapshotBackend added in v1.10.1

func NewRemoteSnapshotBackend(fs RemoteFS, files []string) SnapshotBackend

NewRemoteSnapshotBackend builds the engine over fs. files is the scoped file list used by StoreAll's staging upload.

func NewWebDAVBackend added in v1.10.0

func NewWebDAVBackend(serverURL, basePath, user, password string, files []string) SnapshotBackend

NewWebDAVBackend builds the engine over a WebDAV share through the session layer, dialing a dedicated session for the sync run. Kept as the legacy constructor name so existing call sites and tests stay valid.

type SnapshotInfo added in v1.10.0

type SnapshotInfo struct {
	// Version is an opaque token that changes on every successful upload
	// (timestamp-based; no server-side monotonic counter is assumed). It
	// must be unique per upload — the sync driver re-Heads after StoreAll
	// to detect lost-update races, and a duplicate Version makes that
	// check meaningless. Writers should use an RFC3339Nano timestamp plus
	// a random suffix.
	Version   string    `json:"version"`
	UpdatedAt time.Time `json:"updatedAt"`
	// FileHashes maps config file name → hash of its canonical plaintext
	// (hashStoresScoped / canonicalPlainHash), so hashes are
	// device-independent.
	FileHashes map[string]string `json:"fileHashes"`
}

SnapshotInfo describes one remote snapshot state. In git mode history provides the merge base; snapshot backends (WebDAV, future S3/OneDrive) carry FileHashes instead and the three-way base lives in the device's SyncConfig.LastSyncedHash.

type SyncConfig

type SyncConfig struct {
	RepoURL  string `json:"repoUrl"`
	Branch   string `json:"branch"`
	Username string `json:"username"`
	AutoSync bool   `json:"autoSync"`
	// SyncScope is this device's per-file sync selection over syncableFiles.
	// nil/empty resolves to the default set (defaultSyncedFiles).
	SyncScope []string `json:"syncScope,omitempty"`
	// Backend selects the sync transport: "git" (default, private git repo)
	// or one of the snapshot backends "webdav"/"sftp"/"s3"/"smb".
	Backend string       `json:"backend,omitempty"`
	Remote  RemoteConfig `json:"remote,omitempty"`
	// Legacy flat WebDAV fields — read by Load for configs written before
	// RemoteConfig existed, written back only as a transition dual-write in
	// ConfigureRepoRemote. New code must use Remote.
	WebDAVServer string `json:"webdavServer,omitempty"`
	WebDAVPath   string `json:"webdavPath,omitempty"`
	WebDAVUser   string `json:"webdavUser,omitempty"`
	// LastSyncedHash is the aggregate content hash both sides agreed on at
	// the last successful snapshot sync (three-way base for snapshot mode).
	LastSyncedHash string `json:"lastSyncedHash,omitempty"`

	LastSyncAt     time.Time `json:"lastSyncAt"`
	LastSyncStatus string    `json:"lastSyncStatus"`
	LastSyncError  string    `json:"lastSyncError"`
}

type SyncConfigStore

type SyncConfigStore struct {
	// contains filtered or unexported fields
}

func NewSyncConfigStore

func NewSyncConfigStore(configDir string) *SyncConfigStore

func (*SyncConfigStore) Load

func (s *SyncConfigStore) Load() (SyncConfig, error)

func (*SyncConfigStore) Save

func (s *SyncConfigStore) Save(config SyncConfig) error

type SyncDirection

type SyncDirection int
const (
	SyncNone SyncDirection = iota
	SyncPush
	SyncPull
	SyncConflict
)

type SyncResult

type SyncResult struct {
	Direction SyncDirection `json:"direction"`
	Message   string        `json:"message"`
	Conflict  *ConflictInfo `json:"conflict,omitempty"`
}

type SyncService

type SyncService struct {
	// contains filtered or unexported fields
}

func NewSyncService

func NewSyncService(dataDir string) (*SyncService, error)

func NewSyncServiceAsync added in v1.7.0

func NewSyncServiceAsync(dataDir string) (*SyncService, context.Context)

NewSyncServiceAsync returns a SyncService whose disk-touching init (UserConfigDir / MkdirAll / NewKeychain) runs on a background goroutine. F-407: macOS Security framework keychain IPC + the PBKDF2 600k iterations probe can take 50–500ms+ on first launch — doing it synchronously inside wails.OnStartup blocks the first paint of the main window.

Callers should `Ready()` (with a short timeout) before invoking service methods; otherwise methods may fail with ErrSyncNotInitialized until init completes.

func (*SyncService) ChangePassword

func (s *SyncService) ChangePassword(oldPassword, newPassword string) error

ChangePassword re-encrypts every ciphertext file present in the repo with a new master password and a fresh random salt. The rotation is repo-global because the salt is repo-global: every device derives its key from the same salt, so no ciphertext may survive under the old key. A new salt is required so PBKDF2 work cannot be amortized across old and new passwords (SYNC-P1-6). The repo's existing ciphertext is decrypted with the old key and re-encrypted with the new key via a temp-then-rename pattern, so a crash mid-rotation leaves the repo decryptable with the old key.

func (*SyncService) ConfigureRepo

func (s *SyncService) ConfigureRepo(repoURL, username, token, masterPassword string) (*SyncResult, error)

ConfigureRepo sets up a new or existing sync repository.

func (*SyncService) ConfigureRepoRemote added in v1.10.1

func (s *SyncService) ConfigureRepoRemote(backend string, remote RemoteConfig, secret, masterPassword string) (*SyncResult, error)

ConfigureRepoRemote sets up (or adopts) a snapshot remote for any of the snapshot backends (webdav/sftp/s3/smb). An existing remote snapshot is adopted: its salt is the repo identity, the master password is verified against its ciphertext, and the local side either pulls (no local data), records the agreed hash (identical), or surfaces the conflict dialog (both sides have data). A fresh remote generates a new salt and publishes the local config as the initial snapshot.

func (*SyncService) DeleteRepo

func (s *SyncService) DeleteRepo() error

DeleteRepo removes the local sync repo and credentials.

func (*SyncService) GetConfig

func (s *SyncService) GetConfig() (SyncConfig, error)

GetConfig returns the current sync configuration.

func (*SyncService) IsAutoSyncEnabled

func (s *SyncService) IsAutoSyncEnabled() bool

IsAutoSyncEnabled returns whether auto sync is enabled and configured for the active backend (git repo URL, or a WebDAV server in webdav mode).

func (*SyncService) PasswordStore

func (s *SyncService) PasswordStore() *Keychain

PasswordStore returns the keychain as a PasswordStore for connection store integration.

func (*SyncService) Ready added in v1.7.0

func (s *SyncService) Ready() <-chan struct{}

Ready returns a channel closed once init has finished. Pair with a short timeout in callers that race startup (F-407).

func (*SyncService) RepoPath

func (s *SyncService) RepoPath() string

RepoPath returns the local git repo path.

func (*SyncService) ResolveConflict

func (s *SyncService) ResolveConflict(useLocal bool) (*SyncResult, error)

ResolveConflict handles a conflict by forcing push or reset.

func (*SyncService) SaveConfig

func (s *SyncService) SaveConfig(config SyncConfig, token string) error

SaveConfig persists sync configuration and stores the token if provided.

func (*SyncService) SetPasswordStore added in v1.8.0

func (s *SyncService) SetPasswordStore(ps PasswordStore)

SetPasswordStore wires the credential store so sync can refuse to run on a locked vault. Field-level encryption is the stores' job now.

func (*SyncService) SetSecretBackend added in v1.12.3

func (s *SyncService) SetSecretBackend(backend SecretStore)

SetSecretBackend swaps the keychain's backing storage. Server builds wire an encrypted-file store here: the container has no OS keychain (no Secret Service D-Bus), so every platform-keychain write would fail.

func (*SyncService) SetStores added in v1.10.1

func (s *SyncService) SetStores(stores []SyncableStore)

SetStores wires the config-exchange registry (SyncableStore). Assembled in app.go — the only place that imports both sync and store. nil until set; sync then skips config exchange instead of touching files itself.

func (*SyncService) Sync

func (s *SyncService) Sync() (*SyncResult, error)

Sync runs a full sync cycle: clone/open → fetch → three-way content analysis → commit/pull/push → decrypt.

The remote state is fetched BEFORE anything is committed locally: the old commit-then-fetch order turned "not pulled yet" into a local commit on a stale head, so two machines used alternately diverged and hit the conflict dialog on every open. Content is then analyzed three-way (local data dir vs merge base vs remote head) so that "merely out of date" is a silent pull and only a genuine both-sides edit surfaces the conflict dialog.

func (*SyncService) TestConnection

func (s *SyncService) TestConnection() error

TestConnection verifies the sync backend is reachable with stored credentials.

func (*SyncService) TestRemote added in v1.10.1

func (s *SyncService) TestRemote(backend string, remote RemoteConfig, secret string) error

TestRemote probes a snapshot remote with form-provided values, without touching the stored configuration — the add-repo dialog's test button. Unlike TestConnection it never reads sync-config.json, so it can verify a candidate remote before the user commits to it.

func (*SyncService) UpdateRemotePassword added in v1.10.1

func (s *SyncService) UpdateRemotePassword(secret, masterPassword string) error

UpdateRemotePassword swaps the snapshot backend credential without touching any other configuration. Mirrors the git credential-edit semantics: an empty secret means "keep the stored one" — it is verified against the server but never rewritten. A non-empty secret is probed against the server first, and when the remote already holds a snapshot the master password must decrypt it — a typo can neither lock the device out of its own snapshot nor silently save a broken credential. Changing server/path/user is intentionally not possible here: that is a different sync target, which must go through delete + re-configure.

func (*SyncService) VerifySyncPassword

func (s *SyncService) VerifySyncPassword(password, username, token string) error

VerifySyncPassword validates credentials against the remote and verifies the password can decrypt the remote config. username and token are the new values from the form; token may be empty to keep the stored one.

type SyncableStore added in v1.10.1

type SyncableStore interface {
	BlobName() string
	ExportPlain() ([]byte, error)
	ImportPlain([]byte) error
}

SyncableStore is the only surface the sync package sees for config exchange. Sync never imports store model types: it holds a registry of these interface values, moving canonical plaintext bytes in and out.

The interface lives here, on the consumer side: store's implementations satisfy it structurally, and the registry is assembled in app.go (the only place that imports both packages). backend/store must not import backend/sync — store → credentials → sync means sync → store would cycle, which is also what keeps the dependency graph acyclic.

The contract:

  • ExportPlain renders the store's canonical plaintext — the same shape the on-disk JSON carries after decryption (Load applies the store's full semantics: legacy keychain backfill with move semantics, lazy re-encryption, defaults backfill). Secret fields come out as plaintext; the sync layer re-encrypts the whole blob with the sync key downstream, so only the sync key protects it at rest in the repo. A cleared secret exports as cleared — the store is the single source of truth, and a stale keychain entry can never resurrect it through sync.
  • A store whose backing file was never written returns an error wrapping fs.ErrNotExist: sync skips such blobs instead of uploading an empty document that would clobber the remote copy.
  • ImportPlain persists the blob through the store's Save, so every save-time invariant (password clearing for identity-auth, manifest encryption, hash-skip, quarantine) applies unchanged.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL
⌂ 换网址