Repository: nats-io/nats-server
Pull request #4301 (2023-07-12 01:24:15)

[FIXED] Restore of encrypted filestore with no main key could cause dataloss
When restoring a filestore with no key generator but it was encrypted, fail to restore.

Signed-off-by: Derek Collison <derek@nats.io>




Patch:
diff --git a/server/filestore.go b/server/filestore.go
index be61e3ce..7140c242 100644
--- a/server/filestore.go
+++ b/server/filestore.go
@@ -349,6 +349,14 @@ func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created tim
 		return nil, fmt.Errorf("could not create hash: %v", err)
 	}
 
+	keyFile := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey)
+	// Make sure we do not have an encrypted store underneath of us but no main key.
+	if fs.prf == nil {
+		if _, err := os.Stat(keyFile); err == nil {
+			return nil, errNoMainKey
+		}
+	}
+
 	// Recover our message state.
 	if err := fs.recoverMsgs(); err != nil {
 		return nil, err
@@ -366,7 +374,6 @@ func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created tim
 	// If we expect to be encrypted check that what we are restoring is not plaintext.
 	// This can happen on snapshot restores or conversions.
 	if fs.prf != nil {
-		keyFile := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey)
 		if _, err := os.Stat(keyFile); err != nil && os.IsNotExist(err) {
 			if err := fs.writeStreamMeta(); err != nil {
 				return nil, err
@@ -4390,6 +4397,7 @@ var (
 	errMsgBlkTooBig  = errors.New("message block size exceeded int capacity")
 	errUnknownCipher = errors.New("unknown cipher")
 	errDIOStalled    = errors.New("IO is stalled")
+	errNoMainKey     = errors.New("encrypted store encountered with no main key")
 )
 
 // Used for marking messages that have had their checksums checked.
diff --git a/server/jetstream_cluster.go b/server/jetstream_cluster.go
index 3a86f587..c9d6ec6d 100644
--- a/server/jetstream_cluster.go
+++ b/server/jetstream_cluster.go
@@ -759,6 +759,7 @@ func (js *jetStream) setupMetaGroup() error {
 		s.Errorf("Error creating filestore: %v", err)
 		return err
 	}
+
 	// Register our server.
 	fs.registerServer(s)
 


Test patch:
diff --git a/server/filestore_test.go b/server/filestore_test.go
index 939a37c5..973933bd 100644
--- a/server/filestore_test.go
+++ b/server/filestore_test.go
@@ -5499,3 +5499,41 @@ func TestFileStoreNumPendingLargeNumBlks(t *testing.T) {
 	require_True(t, time.Since(start) < 50*time.Millisecond)
 	require_True(t, total == 4000)
 }
+
+func TestFileStoreRestoreEncryptedWithNoKeyFuncFails(t *testing.T) {
+	// No need for all permutations here.
+	fcfg := FileStoreConfig{StoreDir: t.TempDir(), Cipher: AES}
+	scfg := StreamConfig{Name: "zzz", Subjects: []string{"zzz"}, Storage: FileStorage}
+
+	// Create at first with encryption (prf)
+	prf := func(context []byte) ([]byte, error) {
+		h := hmac.New(sha256.New, []byte("dlc22"))
+		if _, err := h.Write(context); err != nil {
+			return nil, err
+		}
+		return h.Sum(nil), nil
+	}
+
+	fs, err := newFileStoreWithCreated(
+		fcfg, scfg,
+		time.Now(),
+		prf,
+	)
+	require_NoError(t, err)
+
+	subj, msg := "zzz", bytes.Repeat([]byte("X"), 100)
+	numMsgs := 100
+	for i := 0; i < numMsgs; i++ {
+		fs.StoreMsg(subj, nil, msg)
+	}
+
+	fs.Stop()
+
+	// Make sure if we try to restore with no prf (key) that it fails.
+	_, err = newFileStoreWithCreated(
+		fcfg, scfg,
+		time.Now(),
+		nil,
+	)
+	require_Error(t, err, errNoMainKey)
+}
diff --git a/server/jetstream_cluster_3_test.go b/server/jetstream_cluster_3_test.go
index bd4f75df..c3f7e8fb 100644
--- a/server/jetstream_cluster_3_test.go
+++ b/server/jetstream_cluster_3_test.go
@@ -4519,3 +4519,50 @@ func TestJetStreamClusterSnapshotAndRestoreWithHealthz(t *testing.T) {
 	require_NoError(t, err)
 	require_True(t, si.State.Msgs == uint64(toSend))
 }
+
+func TestJetStreamClusterBadEncryptKey(t *testing.T) {
+	c := createJetStreamClusterWithTemplate(t, jsClusterEncryptedTempl, "JSC", 3)
+	defer c.shutdown()
+
+	nc, js := jsClientConnect(t, c.randomServer())
+	defer nc.Close()
+
+	// Create 10 streams.
+	for i := 0; i < 10; i++ {
+		_, err := js.AddStream(&nats.StreamConfig{
+			Name:     fmt.Sprintf("TEST-%d", i),
+			Replicas: 3,
+		})
+		require_NoError(t, err)
+	}
+
+	// Grab random server.
+	s := c.randomServer()
+	s.Shutdown()
+	s.WaitForShutdown()
+
+	var opts *Options
+	for i := 0; i < len(c.servers); i++ {
+		if c.servers[i] == s {
+			opts = c.opts[i]
+			break
+		}
+	}
+	require_NotNil(t, opts)
+
+	// Replace key with an empty key.
+	buf, err := os.ReadFile(opts.ConfigFile)
+	require_NoError(t, err)
+	nbuf := bytes.Replace(buf, []byte("key: \"s3cr3t!\""), []byte("key: \"\""), 1)
+	err = os.WriteFile(opts.ConfigFile, nbuf, 0640)
+	require_NoError(t, err)
+
+	// Make sure trying to start the server now fails.
+	s, err = NewServer(LoadConfig(opts.ConfigFile))
+	require_NoError(t, err)
+	require_NotNil(t, s)
+	s.Start()
+	if err := s.readyForConnections(1 * time.Second); err == nil {
+		t.Fatalf("Expected server not to start")
+	}
+}