siyuan-note/siyuan · error

%w: %v

Error message

%w: %v

What it means

deriveKEK wraps errMasterPasswordMigrationPending (with the underlying cause via %w/%v) when a master-password migration is still in progress. During migration, the KEK is only accepted after all notebooks and encrypted history have been re-wrapped and a new authenticated global crypto backup can be saved. If saving that backup fails, or the re-wrap verification fails, derivation aborts because the old/new password transition is not yet complete.

Solutions

  1. Complete the pending master-password migration: call ChangeMasterPassword again with the correct password so all notebooks/history are re-wrapped and the migration marker is removed
  2. Fix the underlying cause in the wrapped error (%v part): free disk space or repair permissions for the workspace crypto backup path
  3. If verification keeps failing, ensure every encrypted notebook is present and synced; a missing notebook prevents migration completion
  4. As last resort restore the workspace from a backup taken before the interrupted migration

Example fix

// before
kek, err := deriveKEK(password)
if errors.Is(err, model.ErrMasterPasswordMigrationPending) {
    return err // treated as wrong password, user stuck
}
// after
if errors.Is(err, model.ErrMasterPasswordMigrationPending) {
    // retry ChangeMasterPassword/verify with the same password to finish migration,
    // surface the %v cause to the user for actionable feedback
    return fmt.Errorf("migration pending: %w", err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

if model.IsMasterPasswordMigrationPending() {
    return errors.New("complete pending master-password migration before unlocking")
}

Try / catch

kek, err := model.DeriveKEKForTest(password)
if err != nil {
    if errors.Is(err, model.ErrMasterPasswordMigrationPending) {
        // surface cause via errors.Unwrap and retry ChangeMasterPassword
    }
}

Prevention

When it happens

Trigger: Calling UnlockBox/UnlockAndMountBox or ChangeMasterPassword (and test paths) with the new password while a master-password migration marker exists and either decryptHistoryKEKs/verifyKEKAgainst* fails (returns bare errMasterPasswordMigrationPending) or saveNotebookCryptoBackup returns a write error (returns wrapped '%w: %v').

Common situations: A previous ChangeMasterPassword was interrupted (crash/power loss) leaving the migration marker on disk; the workspace backup directory is not writable or on a full disk; synced machines have partially migrated WrappedDEKs so verification against all boxes fails.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/2c4efe4160fee529. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/crypto.go:1310

			logging.LogInfof("repaired notebook crypto configuration from authenticated backup")
		} else if !backupAuthenticated {
			// 同步备份可能属于另一轮完整改密;只要本地配置仍与全部笔记本一致,就继续使用本地配置,
			// 不覆盖候选备份,等待其余 WrappedDEK 同步完成后由新密码采用。
			logging.LogWarnf("notebook crypto backup differs from usable local configuration; keeping both candidates")
		}
	}

	if migrationPending {
		// 崩溃恢复后的首次新密码验证:确认所有笔记本都已切换到新 KEK,再生成带认证的全局备份并结束迁移。
		keys, keyErr := decryptHistoryKEKs(kek, nc.HistoryKEKs)
		clearHistoryKEKs(keys)
		if keyErr != nil || !verifyKEKAgainstExistingBoxes(kek, nil) || !verifyKEKAgainstEncryptedHistory(kek, &nc) {
			zeroAndClear(kek)
			return nil, errMasterPasswordMigrationPending
		}
		if err = saveNotebookCryptoBackup(kek); err != nil {
			zeroAndClear(kek)
			return nil, fmt.Errorf("%w: %v", errMasterPasswordMigrationPending, err)
		}
		removeMasterPasswordMigration()
	}
	return kek, nil
}

// decryptBoxCrypt 用 KEK 解密 box 的 WrappedDEK。优先使用 GetBoxEncryption 的结果(conf → backup fallback),
// 若解密失败则尝试 backup 中不同的 WrappedDEK。
// 返回解密后的 DEK 和实际使用的 BoxCrypt(可能来自 backup)。
// 若 backup 被使用会自动修复 conf.json 和刷新 backup。
func decryptBoxCrypt(boxID string, kek []byte) (dek []byte, boxCrypt *conf.BoxEncryption, err error) {
	boxCrypt, err = GetBoxEncryption(boxID)
	if err != nil || boxCrypt == nil || len(boxCrypt.WrappedDEK) == 0 {
		return nil, nil, fmt.Errorf("no encrypted key material for box [%s]", boxID)
	}

	nc := currentNotebookCrypto()
	dek, err = decryptWrappedDEKWithHistory(boxID, boxCrypt, kek, nc)

View on GitHub (pinned to 9f775e8a12)