siyuan-note/siyuan · critical

hkdf derive failed:

Error message

hkdf derive failed: 

What it means

DeriveSubKey runs HKDF-SHA256 over the master DEK and reads exactly 32 output bytes. The HKDF reader is deterministic and cannot fail for a non-empty key; if io.ReadFull returns an error (practically only when dek is empty/nil), the code panics rather than silently returning a weak or empty derived key.

Solutions

  1. Ensure the DEK is initialized (EnableEncryptedNotebook completed) before any derive/encrypt call
  2. Check len(dek) == 32 at the call site and return a proper error instead of reaching the panic
  3. If the DEK was lost because config was corrupted/restored, recover it from the wrapped-DEK backup envelope with the user password
  4. Guard initialization ordering so decryption paths cannot run before key loading

Example fix

// before
sub := util.DeriveSubKey(nil, "av") // panics

// after
if len(dek) != 32 {
    return fmt.Errorf("DEK not initialized (len=%d)", len(dek))
}
sub := util.DeriveSubKey(dek, "av")
Defensive patterns

Strategy: validation

Validate before calling

if len(dek) != 32 {
    return fmt.Errorf("DEK not ready (len=%d); enable notebook encryption first", len(dek))
}

Type guard

func dekReady(dek []byte) bool { return len(dek) == 32 }

Try / catch

func safeDeriveSubKey(dek []byte, purpose string) (out []byte, err error) {
    defer func() {
        if r := recover(); r != nil {
            err = fmt.Errorf("derive failed: %v", r)
        }
    }()
    return util.DeriveSubKey(dek, purpose), nil
}

Prevention

When it happens

Trigger: Calling DeriveSubKey with a nil or zero-length dek slice — e.g. a master key that failed to load/derive, an uninitialized DEK field in config, or an encryption flow running before EnableEncryptedNotebook populated the key.

Common situations: Calling AV/metadata encrypt-decrypt helpers (encryptAVData, decryptDataWithDEK, encryptBoxMetadata) before notebook encryption is enabled, a corrupted config where the DEK was lost, or a race where the DEK is read before initialization completes.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


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

Appendix: source

Thrown at kernel/util/kdf.go:131

		return nil, errors.New("unsupported encrypted envelope algorithm")
	}
	nonceLength := int(ciphertext[len(encryptionMagic)+2])
	if nonceLength == 0 || len(ciphertext) < encryptionEnvelopeHeaderSize+nonceLength {
		return nil, errors.New("invalid encrypted envelope nonce length")
	}
	return append([]byte(nil), ciphertext[encryptionEnvelopeHeaderSize:encryptionEnvelopeHeaderSize+nonceLength]...), nil
}

// DeriveSubKey 用 HKDF-SHA256 从主 DEK 派生用途隔离的子密钥。
// 同一 (dek, purpose) 多次调用结果一致;不同 purpose 派生出相互独立的子密钥,
// 实现用途分离——.sy/assets/AV 各用独立子密钥,互不可替代,限制单点密钥泄漏的影响面。
func DeriveSubKey(dek []byte, purpose string) []byte {
	// HKDF info 用 purpose 字节;salt 为 nil(DEK 本身已是高熵随机密钥,无需额外 salt)
	r := hkdf.New(sha256.New, dek, nil, []byte(purpose))
	out := make([]byte, 32) // AES-256
	if _, err := io.ReadFull(r, out); err != nil {
		// hkdf.Read 不应出错(除非 dek 为空);防御性 panic 避免静默返回弱密钥
		panic("hkdf derive failed: " + err.Error())
	}
	return out
}

// EncryptWithAAD 用 AES-256-GCM 加密并绑定 AAD(附加认证数据)。
// AAD 不被加密,但参与 GCM 认证——解密时必须提供相同 AAD,否则认证失败。
// 把用途/boxID/路径等元数据放入 AAD,可防止同 box 内密文被替换用途或路径(bind 到上下文)。
// 返回格式与 Encrypt 一致,但 AAD 参与校验。
func EncryptWithAAD(key, plaintext, aad []byte) ([]byte, error) {
	return encryptGCM(key, plaintext, aad, "EncryptWithAAD")
}

// DecryptWithAAD 对应 EncryptWithAAD 的解密。格式无效、AAD 不匹配或密文被篡改时返回错误。
func DecryptWithAAD(key, ciphertext, aad []byte) ([]byte, error) {
	return decryptGCM(key, ciphertext, aad, "DecryptWithAAD")
}

func encryptGCM(key, plaintext, aad []byte, operation string) ([]byte, error) {

View on GitHub (pinned to 9f775e8a12)