Initial vault: single-binary post-quantum file vault
Strip Fury back to its vault functionality in one `vault` command: genkey, init, add (file or directory, optional folder inside the vault), list, get, mv and rm. The hybrid FrodoKEM-640 + ML-KEM-768 encryption core is kept under internal/pqcrypt. Signing, policies, profiles, backups, SSH remotes, FIDO2, agents, provenance and compression are removed.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
// Copyright 2026 The fury Authors.
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
package pqcrypt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestPQRoundTrip exercises the full Encrypt -> Decrypt path through the default
|
||||
// post-quantum suite with a 256-bit file key.
|
||||
func TestPQRoundTrip(t *testing.T) {
|
||||
id, err := GeneratePQIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
r := id.Recipient()
|
||||
|
||||
plaintext := []byte("fury: post-quantum only, hard g like GIF")
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
w, err := Encrypt(buf, r)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := w.Write(plaintext); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := w.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
out, err := Decrypt(bytes.NewReader(buf.Bytes()), id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := io.ReadAll(out)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(got, plaintext) {
|
||||
t.Fatalf("round-trip mismatch: got %q want %q", got, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPQFileKeyIs256Bit(t *testing.T) {
|
||||
if fileKeySize != 32 {
|
||||
t.Fatalf("fileKeySize = %d, want 32 (256-bit)", fileKeySize)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPQWrongIdentity confirms a different identity cannot decrypt.
|
||||
func TestPQWrongIdentity(t *testing.T) {
|
||||
id, _ := GeneratePQIdentity()
|
||||
other, _ := GeneratePQIdentity()
|
||||
|
||||
buf := &bytes.Buffer{}
|
||||
w, err := Encrypt(buf, id.Recipient())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w.Write([]byte("secret"))
|
||||
w.Close()
|
||||
|
||||
if _, err := Decrypt(bytes.NewReader(buf.Bytes()), other); err == nil {
|
||||
t.Fatal("decryption with wrong identity unexpectedly succeeded")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSuiteRegistry checks the default suite is registered and self-describing.
|
||||
func TestSuiteRegistry(t *testing.T) {
|
||||
s := defaultSuite()
|
||||
if s.tag() != "frodo640shake+mlkem768" {
|
||||
t.Fatalf("unexpected default suite %q", s.tag())
|
||||
}
|
||||
if pqSuitesByID[s.id()] != s {
|
||||
t.Fatal("suite not registered by id")
|
||||
}
|
||||
if pqSuitesByTag[s.tag()] != s {
|
||||
t.Fatal("suite not registered by tag")
|
||||
}
|
||||
}
|
||||
|
||||
// TestSeedDeterministic confirms an identity round-trips through its seed, which
|
||||
// is what makes compact passphrase-wrapped identities possible.
|
||||
func TestSeedDeterministic(t *testing.T) {
|
||||
id, err := GeneratePQIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id2, err := newPQIdentityFromSeed(defaultSuite(), id.Seed())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(id.pub, id2.pub) || !bytes.Equal(id.priv, id2.priv) {
|
||||
t.Fatal("identity not deterministic from seed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentityAndRecipientAccessorsReturnCopies(t *testing.T) {
|
||||
id, err := GeneratePQIdentity()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
seed := id.Seed()
|
||||
seed[0] ^= 0xff
|
||||
if id.seed[0] == seed[0] {
|
||||
t.Fatal("Seed returned mutable identity internals")
|
||||
}
|
||||
|
||||
inputSeed := id.Seed()
|
||||
id2, err := newPQIdentityFromSeed(defaultSuite(), inputSeed)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inputSeed[0] ^= 0xff
|
||||
if id2.seed[0] == inputSeed[0] {
|
||||
t.Fatal("newPQIdentityFromSeed retained caller-owned seed slice")
|
||||
}
|
||||
|
||||
recipient := id.Recipient()
|
||||
pub := recipient.Bytes()
|
||||
pub[0] ^= 0xff
|
||||
if recipient.pub[0] == pub[0] {
|
||||
t.Fatal("Bytes returned mutable recipient internals")
|
||||
}
|
||||
|
||||
recipient.pub[0] ^= 0xff
|
||||
if id.pub[0] == recipient.pub[0] {
|
||||
t.Fatal("Recipient returned mutable identity public-key internals")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user