pw_kvs: Initial commit of key value store module

This commit does not build or pass presubmit checks.

Change-Id: I3d4dd393ede1c778888c3cd8be9f12dfbf92fb88
diff --git a/pw_kvs/flash.cc b/pw_kvs/flash.cc
new file mode 100644
index 0000000..bcc695f
--- /dev/null
+++ b/pw_kvs/flash.cc
@@ -0,0 +1,63 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_kvs/util/flash.h"
+
+#include "pw_kvs/config.h"
+
+namespace pw {
+
+Status PaddedWrite(FlashPartition* partition,
+                   FlashPartition::Address address,
+                   const uint8_t* buffer,
+                   uint16_t size) {
+  RETURN_STATUS_IF(
+      address % partition->GetAlignmentBytes() ||
+          partition->GetAlignmentBytes() > cfg::kFlashUtilMaxAlignmentBytes,
+      Status::INVALID_ARGUMENT);
+  uint8_t alignment_buffer[cfg::kFlashUtilMaxAlignmentBytes] = {0};
+  uint16_t aligned_bytes = size - size % partition->GetAlignmentBytes();
+  RETURN_IF_ERROR(partition->Write(address, buffer, aligned_bytes));
+  uint16_t remaining_bytes = size - aligned_bytes;
+  if (remaining_bytes > 0) {
+    memcpy(alignment_buffer, &buffer[aligned_bytes], remaining_bytes);
+    RETURN_IF_ERROR(partition->Write(address + aligned_bytes,
+                                     alignment_buffer,
+                                     partition->GetAlignmentBytes()));
+  }
+  return Status::OK;
+}
+
+Status UnalignedRead(FlashPartition* partition,
+                     uint8_t* buffer,
+                     FlashPartition::Address address,
+                     uint16_t size) {
+  RETURN_STATUS_IF(
+      address % partition->GetAlignmentBytes() ||
+          partition->GetAlignmentBytes() > cfg::kFlashUtilMaxAlignmentBytes,
+      Status::INVALID_ARGUMENT);
+  uint16_t aligned_bytes = size - size % partition->GetAlignmentBytes();
+  RETURN_IF_ERROR(partition->Read(buffer, address, aligned_bytes));
+  uint16_t remaining_bytes = size - aligned_bytes;
+  if (remaining_bytes > 0) {
+    uint8_t alignment_buffer[cfg::kFlashUtilMaxAlignmentBytes];
+    RETURN_IF_ERROR(partition->Read(alignment_buffer,
+                                    address + aligned_bytes,
+                                    partition->GetAlignmentBytes()));
+    memcpy(&buffer[aligned_bytes], alignment_buffer, remaining_bytes);
+  }
+  return Status::OK;
+}
+
+}  // namespace pw
diff --git a/pw_kvs/key_value_store.cc b/pw_kvs/key_value_store.cc
new file mode 100644
index 0000000..ce21740
--- /dev/null
+++ b/pw_kvs/key_value_store.cc
@@ -0,0 +1,805 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+// KVS is a key-value storage format for flash based memory.
+//
+// Currently it stores key-value sets in chunks aligned with the flash memory.
+// Each chunk contains a header (KvsHeader), which is immediately followed by
+// the key, and then the data blob. To support different alignments both the
+// key length and value length are rounded up to be aligned.
+//
+// Example memory layout of sector with two KVS chunks:
+//    [ SECTOR_HEADER - Meta | alignment_padding ]
+//    [ SECTOR_HEADER - Cleaning State | alignment_padding ]
+//    [First Chunk Header | alignment_padding ]
+//    [First Chunk Key | alignment_padding ]
+//    [First Chunk Value | alignment_padding ]
+//    [Second Chunk Header | alignment_padding ]
+//    [Second Chunk Key | alignment_padding ]
+//    [Second Chunk Value | alignment_padding ]
+//
+// For efficiency if a key's value is rewritten the new value is just appended
+// to the same sector, a clean of the sector is only needed if there is no more
+// room. Cleaning the sector involves moving the most recent value of each key
+// to another sector and erasing the sector. Erasing data is treated the same
+// as rewriting data, but an erased chunk has the erased flag set, and no data.
+//
+// KVS maintains a data structure in RAM for quick indexing of each key's most
+// recent value, but no caching of data is ever performed. If a write/erase
+// function returns successfully, it is guaranteed that the data has been
+// pushed to flash. The KVS should also be resistant to sudden unplanned power
+// outages and be capable of recovering even mid clean (this is accomplished
+// using a flag which marks the sector before the clean is started).
+
+#include "pw_kvs/key_value_store.h"
+
+#include <cstring>
+
+#include "pw_kvs/os/mutex.h"
+#include "pw_kvs/util/ccitt_crc16.h"
+#include "pw_kvs/util/constexpr.h"
+#include "pw_kvs/util/flash.h"
+
+namespace pw {
+
+// Declare static constexpr variables so it can be used for pass-by-reference
+// functions.
+constexpr uint16_t KeyValueStore::kSectorReadyValue;
+
+Status KeyValueStore::Enable() {
+  os::MutexLock lock(&lock_);
+  if (enabled_) {
+    return Status::OK;
+  }
+
+  // Reset parameters.
+  memset(sector_space_remaining_, 0, sizeof(sector_space_remaining_));
+  map_size_ = 0;
+
+  // For now alignment is set to use partitions alignment.
+  alignment_bytes_ = partition_.GetAlignmentBytes();
+  DCHECK(alignment_bytes_ <= kMaxAlignmentBytes);
+
+  LOG_WARN_IF(partition_.GetSectorCount() > kSectorCountMax,
+              "Partition is larger then KVS max sector count, not all space "
+              "will be used.");
+  // Load map and setup sectors if needed (first word isn't kSectorReadyValue).
+  next_sector_clean_order_ = 0;
+  for (SectorIndex i = 0; i < SectorCount(); i++) {
+    KvsSectorHeaderMeta sector_header_meta;
+    // Because underlying flash can be encrypted + erased, trying to readback
+    // may give random values. It's important to make sure that data is not
+    // erased before trying to see if there is a token match.
+    bool is_sector_meta_erased;
+    RETURN_IF_ERROR(partition_.IsChunkErased(
+        SectorIndexToAddress(i),
+        RoundUpForAlignment(sizeof(sector_header_meta)),
+        &is_sector_meta_erased));
+    RETURN_IF_ERROR(
+        UnalignedRead(&partition_,
+                      reinterpret_cast<uint8_t*>(&sector_header_meta),
+                      SectorIndexToAddress(i),
+                      sizeof(sector_header_meta)));
+
+    constexpr int kVersion3 = 3;  // Version 3 only cleans 1 sector at a time.
+    constexpr int kVersion2 = 2;  // Version 2 is always 1 byte aligned.
+    if (is_sector_meta_erased ||
+        sector_header_meta.synchronize_token != kSectorReadyValue) {
+      // Sector needs to be setup
+      RETURN_IF_ERROR(ResetSector(i));
+      continue;
+    } else if (sector_header_meta.version != kVersion &&
+               sector_header_meta.version != kVersion3 &&  // Allow version 3
+               sector_header_meta.version != kVersion2) {  // Allow version 2
+      LOG(ERROR) << "Unsupported KVS version in sector: " << i;
+      return Status::FAILED_PRECONDITION;
+    }
+    uint32_t sector_header_cleaning_offset =
+        RoundUpForAlignment(sizeof(KvsSectorHeaderMeta));
+
+    bool clean_not_pending;
+    RETURN_IF_ERROR(partition_.IsChunkErased(
+        SectorIndexToAddress(i) + sector_header_cleaning_offset,
+        RoundUpForAlignment(sizeof(KvsSectorHeaderCleaning)),
+        &clean_not_pending));
+
+    if (!clean_not_pending) {
+      // Sector is marked for cleaning, read the sector_clean_order
+      RETURN_IF_ERROR(
+          UnalignedRead(&partition_,
+                        reinterpret_cast<uint8_t*>(&sector_clean_order_[i]),
+                        SectorIndexToAddress(i) + sector_header_cleaning_offset,
+                        sizeof(KvsSectorHeaderCleaning::sector_clean_order)));
+      next_sector_clean_order_ =
+          std::max(sector_clean_order_[i] + 1, next_sector_clean_order_);
+    } else {
+      sector_clean_order_[i] = kSectorCleanNotPending;
+    }
+
+    // Handle alignment
+    if (sector_header_meta.version == kVersion2) {
+      sector_header_meta.alignment_bytes = 1;
+    }
+    if (sector_header_meta.alignment_bytes != alignment_bytes_) {
+      // NOTE: For now all sectors must have same alignment.
+      LOG(ERROR) << "Sector " << i << " has unexpected alignment "
+                 << alignment_bytes_
+                 << " != " << sector_header_meta.alignment_bytes;
+      return Status::FAILED_PRECONDITION;
+    }
+
+    // Scan through sector and add key/value pairs to map.
+    FlashPartition::Address offset =
+        RoundUpForAlignment(sizeof(KvsSectorHeaderMeta)) +
+        RoundUpForAlignment(sizeof(KvsSectorHeaderCleaning));
+    sector_space_remaining_[i] = partition_.GetSectorSizeBytes() - offset;
+    while (offset < partition_.GetSectorSizeBytes() -
+                        RoundUpForAlignment(sizeof(KvsHeader))) {
+      FlashPartition::Address address = SectorIndexToAddress(i) + offset;
+
+      // Read header
+      KvsHeader header;
+      bool is_kvs_header_erased;
+      // Because underlying flash can be encrypted + erased, trying to readback
+      // may give random values. It's important to make sure that data is not
+      // erased before trying to see if there is a token match.
+      RETURN_IF_ERROR(partition_.IsChunkErased(
+          address, RoundUpForAlignment(sizeof(header)), &is_kvs_header_erased));
+      RETURN_IF_ERROR(UnalignedRead(&partition_,
+                                    reinterpret_cast<uint8_t*>(&header),
+                                    address,
+                                    sizeof(header)));
+      if (is_kvs_header_erased || header.synchronize_token != kChunkSyncValue) {
+        if (!is_kvs_header_erased) {
+          LOG_ERROR("Next sync_token is not clear!");
+          // TODO: handle this?
+        }
+        break;  // End of elements in sector
+      }
+
+      CHECK(header.key_len <= kChunkKeyLengthMax);
+      static_assert(sizeof(temp_key_buffer_) >= (kChunkKeyLengthMax + 1u),
+                    "Key buffer must be at least big enough for a key and a "
+                    "nul-terminator.");
+
+      // Read key and add to map
+      RETURN_IF_ERROR(
+          UnalignedRead(&partition_,
+                        reinterpret_cast<uint8_t*>(&temp_key_buffer_),
+                        address + RoundUpForAlignment(sizeof(header)),
+                        header.key_len));
+      temp_key_buffer_[header.key_len] = '\0';
+      bool is_erased = header.flags & kFlagsIsErasedMask;
+
+      KeyIndex index = FindKeyInMap(temp_key_buffer_);
+      if (index == kListCapacityMax) {
+        RETURN_IF_ERROR(AppendToMap(
+            temp_key_buffer_, address, header.chunk_len, is_erased));
+      } else if (sector_clean_order_[i] >=
+                 sector_clean_order_[AddressToSectorIndex(
+                     key_map_[index].address)]) {
+        // The value being added is also in another sector (which is marked for
+        // cleaning), but the current sector's order is larger and thefore this
+        // is a newer version then what is already in the map.
+        UpdateMap(index, address, header.chunk_len, is_erased);
+      }
+
+      // Increment search
+      offset += ChunkSize(header.key_len, header.chunk_len);
+    }
+    sector_space_remaining_[i] =
+        clean_not_pending ? partition_.GetSectorSizeBytes() - offset : 0;
+  }
+
+  LOG_IF_ERROR(EnforceFreeSector())
+      << "Failed to force clean at boot, no free sectors available!";
+  enabled_ = true;
+  return Status::OK;
+}
+
+Status KeyValueStore::Get(const char* key,
+                          void* raw_value,
+                          uint16_t size,
+                          uint16_t offset) {
+  uint8_t* const value = reinterpret_cast<uint8_t*>(raw_value);
+
+  if (key == nullptr || value == nullptr) {
+    return Status::INVALID_ARGUMENT;
+  }
+
+  size_t key_len = util::StringLength(key, kChunkKeyLengthMax + 1u);
+  if (key_len == 0 || key_len > kChunkKeyLengthMax) {
+    return Status::INVALID_ARGUMENT;
+  }
+
+  // TODO: Support unaligned offset reads.
+  if (offset % alignment_bytes_ != 0) {
+    LOG_ERROR("Currently unaligned offsets are not supported");
+    return Status::INVALID_ARGUMENT;
+  }
+  os::MutexLock lock(&lock_);
+  if (!enabled_) {
+    return Status::FAILED_PRECONDITION;
+  }
+
+  KeyIndex key_index = FindKeyInMap(key);
+  if (key_index == kListCapacityMax || key_map_[key_index].is_erased) {
+    return Status::NOT_FOUND;
+  }
+  KvsHeader header;
+  // TODO: Could cache the CRC and avoid reading the header.
+  RETURN_IF_ERROR(UnalignedRead(&partition_,
+                                reinterpret_cast<uint8_t*>(&header),
+                                key_map_[key_index].address,
+                                sizeof(header)));
+  if (kChunkSyncValue != header.synchronize_token) {
+    return Status::DATA_LOSS;
+  }
+  if (size + offset > header.chunk_len) {
+    LOG_ERROR("Out of bounds read: offset(%u) + size(%u) > data_size(%u)",
+              offset,
+              size,
+              header.chunk_len);
+    return Status::INVALID_ARGUMENT;
+  }
+  RETURN_IF_ERROR(UnalignedRead(
+      &partition_,
+      value,
+      key_map_[key_index].address + RoundUpForAlignment(sizeof(KvsHeader)) +
+          RoundUpForAlignment(header.key_len) + offset,
+      size));
+
+  // Verify CRC only when full packet was read.
+  if (offset == 0 && size == header.chunk_len) {
+    uint16_t crc = CalculateCrc(key, key_len, value, size);
+    if (crc != header.crc) {
+      LOG_ERROR("KVS CRC does not match for key=%s [expected %u, found %u]",
+                key,
+                header.crc,
+                crc);
+      return Status::DATA_LOSS;
+    }
+  }
+  return Status::OK;
+}
+
+uint16_t KeyValueStore::CalculateCrc(const char* key,
+                                     uint16_t key_size,
+                                     const uint8_t* value,
+                                     uint16_t value_size) const {
+  CcittCrc16 crc;
+  crc.AppendBytes(ConstBuffer(reinterpret_cast<const uint8_t*>(key), key_size));
+  return crc.AppendBytes(ConstBuffer(value, value_size));
+}
+
+Status KeyValueStore::CalculateCrcFromValueAddress(
+    const char* key,
+    uint16_t key_size,
+    FlashPartition::Address value_address,
+    uint16_t value_size,
+    uint16_t* crc_ret) {
+  CcittCrc16 crc;
+  crc.AppendBytes(ConstBuffer(reinterpret_cast<const uint8_t*>(key), key_size));
+  for (size_t i = 0; i < value_size; i += TempBufferAlignedSize()) {
+    auto read_size = std::min(value_size - i, TempBufferAlignedSize());
+    RETURN_IF_ERROR(
+        UnalignedRead(&partition_, temp_buffer_, value_address + i, read_size));
+    crc.AppendBytes(ConstBuffer(temp_buffer_, read_size));
+  }
+  *crc_ret = crc.CurrentValue();
+  return Status::OK;
+}
+
+Status KeyValueStore::Put(const char* key,
+                          const void* raw_value,
+                          uint16_t size) {
+  const uint8_t* const value = reinterpret_cast<const uint8_t*>(raw_value);
+  if (key == nullptr || value == nullptr) {
+    return Status::INVALID_ARGUMENT;
+  }
+
+  size_t key_len = util::StringLength(key, (kChunkKeyLengthMax + 1u));
+  if (key_len == 0 || key_len > kChunkKeyLengthMax ||
+      size > kChunkValueLengthMax) {
+    return Status::INVALID_ARGUMENT;
+  }
+
+  os::MutexLock lock(&lock_);
+  if (!enabled_) {
+    return Status::FAILED_PRECONDITION;
+  }
+
+  KeyIndex index = FindKeyInMap(key);
+  if (index != kListCapacityMax) {  // Key already in map, rewrite value
+    return RewriteValue(index, value, size);
+  }
+
+  FlashPartition::Address address = FindSpace(ChunkSize(key_len, size));
+  if (address == kSectorInvalid) {
+    return Status::RESOURCE_EXHAUSTED;
+  }
+
+  // Check if this would use the last empty sector on KVS with multiple sectors
+  if (SectorCount() > 1 && IsInLastFreeSector(address)) {
+    // Forcing a full garbage collect to free more sectors.
+    RETURN_IF_ERROR(FullGarbageCollect());
+    address = FindSpace(ChunkSize(key_len, size));
+    if (address == kSectorInvalid || IsInLastFreeSector(address)) {
+      // Couldn't find space, KVS is full.
+      return Status::RESOURCE_EXHAUSTED;
+    }
+  }
+
+  RETURN_IF_ERROR(WriteKeyValue(address, key, value, size));
+  RETURN_IF_ERROR(AppendToMap(key, address, size));
+
+  return Status::OK;
+}
+
+// Garbage collection cleans sectors to try to free up space.
+// If clean_pending_sectors is true, it will only clean sectors which are
+// currently pending a clean.
+// If clean_pending_sectors is false, it will only clean sectors which are not
+// currently pending a clean, instead it will mark them for cleaning and attempt
+// a clean.
+// If exit_when_have_free_sector is true, it will exit once a single free sector
+// exists.
+Status KeyValueStore::GarbageCollectImpl(bool clean_pending_sectors,
+                                         bool exit_when_have_free_sector) {
+  // Try to clean any pending sectors
+  for (SectorIndex sector = 0; sector < SectorCount(); sector++) {
+    if (clean_pending_sectors ==
+        (sector_clean_order_[sector] != kSectorCleanNotPending)) {
+      if (!clean_pending_sectors) {
+        RETURN_IF_ERROR(MarkSectorForClean(sector));
+      }
+      Status status = CleanSector(sector);
+      if (!status.ok() && status != Status::RESOURCE_EXHAUSTED) {
+        return status;
+      }
+      if (exit_when_have_free_sector && HaveEmptySectorImpl()) {
+        return Status::OK;  // Now have a free sector
+      }
+    }
+  }
+  return Status::OK;
+}
+
+Status KeyValueStore::EnforceFreeSector() {
+  if (SectorCount() == 1 || HasEmptySector()) {
+    return Status::OK;
+  }
+  LOG_INFO("KVS garbage collecting to get a free sector");
+  RETURN_IF_ERROR(GarbageCollectImpl(true /*clean_pending_sectors*/,
+                                     true /*exit_when_have_free_sector*/));
+  if (HasEmptySector()) {
+    return Status::OK;
+  }
+  LOG_INFO("KVS: trying to clean non-pending sectors for more space");
+  RETURN_IF_ERROR(GarbageCollectImpl(false /*clean_pending_sectors*/,
+                                     true /*exit_when_have_free_sector*/));
+  return HaveEmptySectorImpl() ? Status::OK : Status::RESOURCE_EXHAUSTED;
+}
+
+Status KeyValueStore::FullGarbageCollect() {
+  LOG_INFO("KVS: Full garbage collecting to try to free space");
+  RETURN_IF_ERROR(GarbageCollectImpl(true /*clean_pending_sectors*/,
+                                     false /*exit_when_have_free_sector*/));
+  return GarbageCollectImpl(false /*clean_pending_sectors*/,
+                            false /*exit_when_have_free_sector*/);
+}
+
+Status KeyValueStore::RewriteValue(KeyIndex key_index,
+                                   const uint8_t* value,
+                                   uint16_t size,
+                                   bool is_erased) {
+  // Compare values, return if values are the same.
+  if (ValueMatches(key_index, value, size, is_erased)) {
+    return Status::OK;
+  }
+
+  size_t key_length =
+      util::StringLength(key_map_[key_index].key, (kChunkKeyLengthMax + 1u));
+  RETURN_STATUS_IF(key_length > kChunkKeyLengthMax, Status::INTERNAL);
+
+  uint32_t space_required = ChunkSize(key_length, size);
+  SectorIndex sector = AddressToSectorIndex(key_map_[key_index].address);
+  uint32_t sector_space_remaining = SectorSpaceRemaining(sector);
+
+  FlashPartition::Address address = kSectorInvalid;
+  if (sector_space_remaining >= space_required) {
+    // Space available within sector, append to end
+    address = SectorIndexToAddress(sector) + partition_.GetSectorSizeBytes() -
+              sector_space_remaining;
+  } else {
+    // No space in current sector, mark sector for clean and use another sector.
+    RETURN_IF_ERROR(MarkSectorForClean(sector));
+    address = FindSpace(ChunkSize(key_length, size));
+  }
+  if (address == kSectorInvalid) {
+    return Status::RESOURCE_EXHAUSTED;
+  }
+  RETURN_IF_ERROR(
+      WriteKeyValue(address, key_map_[key_index].key, value, size, is_erased));
+  UpdateMap(key_index, address, size, is_erased);
+
+  return EnforceFreeSector();
+}
+
+bool KeyValueStore::ValueMatches(KeyIndex index,
+                                 const uint8_t* value,
+                                 uint16_t size,
+                                 bool is_erased) {
+  // Compare sizes of CRC.
+  if (size != key_map_[index].chunk_len) {
+    return false;
+  }
+  KvsHeader header;
+  UnalignedRead(&partition_,
+                reinterpret_cast<uint8_t*>(&header),
+                key_map_[index].address,
+                sizeof(header));
+  uint8_t key_len =
+      util::StringLength(key_map_[index].key, (kChunkKeyLengthMax + 1u));
+  if (key_len > kChunkKeyLengthMax) {
+    return false;
+  }
+
+  if ((header.flags & kFlagsIsErasedMask) != is_erased) {
+    return false;
+  } else if ((header.flags & kFlagsIsErasedMask) && is_erased) {
+    return true;
+  }
+
+  // Compare checksums.
+  if (header.crc != CalculateCrc(key_map_[index].key, key_len, value, size)) {
+    return false;
+  }
+  FlashPartition::Address address = key_map_[index].address +
+                                    RoundUpForAlignment(sizeof(KvsHeader)) +
+                                    RoundUpForAlignment(key_len);
+  // Compare full values.
+  for (size_t i = 0; i < key_map_[index].chunk_len;
+       i += TempBufferAlignedSize()) {
+    auto read_size =
+        std::min(key_map_[index].chunk_len - i, TempBufferAlignedSize());
+    auto status =
+        UnalignedRead(&partition_, temp_buffer_, address + i, read_size);
+    if (!status.ok()) {
+      LOG(ERROR) << "Failed to read chunk: " << status;
+      return false;
+    }
+    if (memcmp(value + i, temp_buffer_, read_size) != 0) {
+      return false;
+    }
+  }
+  return true;
+}
+
+Status KeyValueStore::Erase(const char* key) {
+  if (key == nullptr) {
+    return Status::INVALID_ARGUMENT;
+  }
+
+  size_t key_len = util::StringLength(key, (kChunkKeyLengthMax + 1u));
+  if (key_len == 0 || key_len > kChunkKeyLengthMax) {
+    return Status::INVALID_ARGUMENT;
+  }
+  os::MutexLock lock(&lock_);
+  if (!enabled_) {
+    return Status::FAILED_PRECONDITION;
+  }
+
+  KeyIndex key_index = FindKeyInMap(key);
+  if (key_index == kListCapacityMax || key_map_[key_index].is_erased) {
+    return Status::NOT_FOUND;
+  }
+  return RewriteValue(key_index, nullptr, 0, true);
+}
+
+Status KeyValueStore::ResetSector(SectorIndex sector_index) {
+  KvsSectorHeaderMeta sector_header = {.synchronize_token = kSectorReadyValue,
+                                       .version = kVersion,
+                                       .alignment_bytes = alignment_bytes_,
+                                       .padding = 0xFFFF};
+  bool sector_erased = false;
+  partition_.IsChunkErased(SectorIndexToAddress(sector_index),
+                           partition_.GetSectorSizeBytes(),
+                           &sector_erased);
+  auto status = partition_.Erase(SectorIndexToAddress(sector_index), 1);
+
+  // If erasure failed, check first to see if it's merely unimplemented
+  // (as in sub-sector KVSs).
+  if (!status.ok() && !(status == Status::UNIMPLEMENTED && sector_erased)) {
+    return status;
+  }
+
+  RETURN_IF_ERROR(PaddedWrite(&partition_,
+                              SectorIndexToAddress(sector_index),
+                              reinterpret_cast<const uint8_t*>(&sector_header),
+                              sizeof(sector_header)));
+
+  // Update space remaining
+  sector_clean_order_[sector_index] = kSectorCleanNotPending;
+  sector_space_remaining_[sector_index] = SectorSpaceAvailableWhenEmpty();
+  return Status::OK;
+}
+
+Status KeyValueStore::WriteKeyValue(FlashPartition::Address address,
+                                    const char* key,
+                                    const uint8_t* value,
+                                    uint16_t size,
+                                    bool is_erased) {
+  uint16_t key_length = util::StringLength(key, (kChunkKeyLengthMax + 1u));
+  RETURN_STATUS_IF(key_length > kChunkKeyLengthMax, Status::INTERNAL);
+
+  constexpr uint16_t kFlagDefaultValue = 0;
+  KvsHeader header = {
+      .synchronize_token = kChunkSyncValue,
+      .crc = CalculateCrc(key, key_length, value, size),
+      .flags = is_erased ? kFlagsIsErasedMask : kFlagDefaultValue,
+      .key_len = key_length,
+      .chunk_len = size};
+
+  SectorIndex sector = AddressToSectorIndex(address);
+  RETURN_IF_ERROR(PaddedWrite(&partition_,
+                              address,
+                              reinterpret_cast<uint8_t*>(&header),
+                              sizeof(header)));
+  address += RoundUpForAlignment(sizeof(header));
+  RETURN_IF_ERROR(PaddedWrite(
+      &partition_, address, reinterpret_cast<const uint8_t*>(key), key_length));
+  address += RoundUpForAlignment(key_length);
+  if (size > 0) {
+    RETURN_IF_ERROR(PaddedWrite(&partition_, address, value, size));
+  }
+  sector_space_remaining_[sector] -= ChunkSize(key_length, size);
+  return Status::OK;
+}
+
+Status KeyValueStore::MoveChunk(FlashPartition::Address dest_address,
+                                FlashPartition::Address src_address,
+                                uint16_t size) {
+  DCHECK_EQ(src_address % partition_.GetAlignmentBytes(), 0);
+  DCHECK_EQ(dest_address % partition_.GetAlignmentBytes(), 0);
+  DCHECK_EQ(size % partition_.GetAlignmentBytes(), 0);
+
+  // Copy data over in chunks to reduce the size of the temporary buffer
+  for (size_t i = 0; i < size; i += TempBufferAlignedSize()) {
+    size_t move_size = std::min(size - i, TempBufferAlignedSize());
+    DCHECK_EQ(move_size % alignment_bytes_, 0);
+    RETURN_IF_ERROR(partition_.Read(temp_buffer_, src_address + i, move_size));
+    RETURN_IF_ERROR(
+        partition_.Write(dest_address + i, temp_buffer_, move_size));
+  }
+  return Status::OK;
+}
+
+Status KeyValueStore::MarkSectorForClean(SectorIndex sector) {
+  if (sector_clean_order_[sector] != kSectorCleanNotPending) {
+    return Status::OK;  // Sector is already marked for clean
+  }
+
+  // Flag the sector as clean being active. This ensures we can handle losing
+  // power while a clean is active.
+  const KvsSectorHeaderCleaning kValue = {next_sector_clean_order_};
+  RETURN_IF_ERROR(
+      PaddedWrite(&partition_,
+                  SectorIndexToAddress(sector) +
+                      RoundUpForAlignment(sizeof(KvsSectorHeaderMeta)),
+                  reinterpret_cast<const uint8_t*>(&kValue),
+                  sizeof(kValue)));
+  sector_space_remaining_[sector] = 0;
+  sector_clean_order_[sector] = next_sector_clean_order_;
+  next_sector_clean_order_++;
+  return Status::OK;
+}
+
+Status KeyValueStore::CleanSector(SectorIndex sector) {
+  // Iterate through the map, for each valid element which is in this sector:
+  //    - Find space in another sector which can fit this chunk.
+  //    - Add to the other sector and update the map.
+  for (KeyValueStore::KeyIndex i = 0; i < map_size_; i++) {
+    // If this key is an erased chunk don't need to move it.
+    while (i < map_size_ &&
+           sector == AddressToSectorIndex(key_map_[i].address) &&
+           key_map_[i].is_erased) {  // Remove this key from the map.
+      RemoveFromMap(i);
+      // NOTE: i is now a new key, and map_size_ has been decremented.
+    }
+
+    if (i < map_size_ && sector == AddressToSectorIndex(key_map_[i].address)) {
+      uint8_t key_len =
+          util::StringLength(key_map_[i].key, (kChunkKeyLengthMax + 1u));
+      FlashPartition::Address address = key_map_[i].address;
+      auto size = ChunkSize(key_len, key_map_[i].chunk_len);
+      FlashPartition::Address move_address = FindSpace(size);
+      if (move_address == kSectorInvalid) {
+        return Status::RESOURCE_EXHAUSTED;
+      }
+      RETURN_IF_ERROR(MoveChunk(move_address, address, size));
+      sector_space_remaining_[AddressToSectorIndex(move_address)] -= size;
+      key_map_[i].address = move_address;  // Update map
+    }
+  }
+  ResetSector(sector);
+  return Status::OK;
+}
+
+Status KeyValueStore::CleanOneSector(bool* all_sectors_have_been_cleaned) {
+  if (all_sectors_have_been_cleaned == nullptr) {
+    return Status::INVALID_ARGUMENT;
+  }
+  os::MutexLock lock(&lock_);
+  bool have_cleaned_sector = false;
+  for (SectorIndex sector = 0; sector < SectorCount(); sector++) {
+    if (sector_clean_order_[sector] != kSectorCleanNotPending) {
+      if (have_cleaned_sector) {  // only clean 1 sector
+        *all_sectors_have_been_cleaned = false;
+        return Status::OK;
+      }
+      RETURN_IF_ERROR(CleanSector(sector));
+      have_cleaned_sector = true;
+    }
+  }
+  *all_sectors_have_been_cleaned = true;
+  return Status::OK;
+}
+
+Status KeyValueStore::CleanAllInternal() {
+  for (SectorIndex sector = 0; sector < SectorCount(); sector++) {
+    if (sector_clean_order_[sector] != kSectorCleanNotPending) {
+      RETURN_IF_ERROR(CleanSector(sector));
+    }
+  }
+  return Status::OK;
+}
+
+FlashPartition::Address KeyValueStore::FindSpace(
+    uint16_t requested_size) const {
+  if (requested_size > SectorSpaceAvailableWhenEmpty()) {
+    return kSectorInvalid;  // This would never fit
+  }
+  // Iterate through sectors, find first available sector with enough space.
+  SectorIndex first_empty_sector = kSectorInvalid;
+  for (SectorIndex i = 0; i < SectorCount(); i++) {
+    uint32_t space_remaining = SectorSpaceRemaining(i);
+    if (space_remaining == SectorSpaceAvailableWhenEmpty() &&
+        first_empty_sector == kSectorInvalid) {
+      // Skip the first empty sector to encourage keeping one sector free.
+      first_empty_sector = i;
+      continue;
+    }
+    if (space_remaining >= requested_size) {
+      return SectorIndexToAddress(i) + partition_.GetSectorSizeBytes() -
+             space_remaining;
+    }
+  }
+  // Use first empty sector if that is all that is available.
+  if (first_empty_sector != kSectorInvalid) {
+    return SectorIndexToAddress(first_empty_sector) +
+           partition_.GetSectorSizeBytes() - SectorSpaceAvailableWhenEmpty();
+  }
+  return kSectorInvalid;
+}
+
+uint32_t KeyValueStore::SectorSpaceRemaining(SectorIndex sector_index) const {
+  return sector_space_remaining_[sector_index];
+}
+
+Status KeyValueStore::GetValueSize(const char* key, uint16_t* value_size) {
+  if (key == nullptr || value_size == nullptr) {
+    return Status::INVALID_ARGUMENT;
+  }
+
+  size_t key_len = util::StringLength(key, (kChunkKeyLengthMax + 1u));
+  if (key_len == 0 || key_len > kChunkKeyLengthMax) {
+    return Status::INVALID_ARGUMENT;
+  }
+  os::MutexLock lock(&lock_);
+  if (!enabled_) {
+    return Status::FAILED_PRECONDITION;
+  }
+
+  uint8_t idx = FindKeyInMap(key);
+  if (idx == kListCapacityMax || key_map_[idx].is_erased) {
+    return Status::NOT_FOUND;
+  }
+  *value_size = key_map_[idx].chunk_len;
+  return Status::OK;
+}
+
+Status KeyValueStore::AppendToMap(const char* key,
+                                  FlashPartition::Address address,
+                                  uint16_t chunk_len,
+                                  bool is_erased) {
+  if (map_size_ >= kListCapacityMax) {
+    LOG_ERROR("Can't add: reached max supported keys %d", kListCapacityMax);
+    return Status::INTERNAL;
+  }
+
+  // Copy incoming key into map entry, ensuring size checks and nul-termination.
+  StringBuilder key_builder(key_map_[map_size_].key,
+                            sizeof(key_map_[map_size_].key));
+  key_builder.append(key);
+
+  if (!key_builder.status().ok()) {
+    LOG_ERROR("Can't add: got invalid key: %s!", key_builder.status().str());
+    return Status::INTERNAL;
+  }
+
+  key_map_[map_size_].address = address;
+  key_map_[map_size_].chunk_len = chunk_len;
+  key_map_[map_size_].is_erased = is_erased;
+  map_size_++;
+
+  return Status::OK;
+}
+
+KeyValueStore::KeyIndex KeyValueStore::FindKeyInMap(const char* key) const {
+  for (KeyIndex i = 0; i < map_size_; i++) {
+    if (strncmp(key, key_map_[i].key, sizeof(key_map_[i].key)) == 0) {
+      return i;
+    }
+  }
+  return kListCapacityMax;
+}
+
+void KeyValueStore::UpdateMap(KeyIndex index,
+                              FlashPartition::Address address,
+                              uint16_t chunk_len,
+                              bool is_erased) {
+  key_map_[index].address = address;
+  key_map_[index].chunk_len = chunk_len;
+  key_map_[index].is_erased = is_erased;
+}
+
+void KeyValueStore::RemoveFromMap(KeyIndex key_index) {
+  key_map_[key_index] = key_map_[--map_size_];
+}
+
+uint8_t KeyValueStore::KeyCount() const {
+  uint8_t count = 0;
+  for (unsigned i = 0; i < map_size_; i++) {
+    count += key_map_[i].is_erased ? 0 : 1;
+  }
+  return count;
+}
+
+const char* KeyValueStore::GetKey(uint8_t idx) const {
+  uint8_t count = 0;
+  for (unsigned i = 0; i < map_size_; i++) {
+    if (!key_map_[i].is_erased) {
+      if (idx == count) {
+        return key_map_[i].key;
+      }
+      count++;
+    }
+  }
+  return nullptr;
+}
+
+uint16_t KeyValueStore::GetValueSize(uint8_t idx) const {
+  uint8_t count = 0;
+  for (unsigned i = 0; i < map_size_; i++) {
+    if (!key_map_[i].is_erased) {
+      if (idx == count++) {
+        return key_map_[i].chunk_len;
+      }
+    }
+  }
+  return 0;
+}
+
+}  // namespace pw
diff --git a/pw_kvs/key_value_store_sub_sector_test.cc b/pw_kvs/key_value_store_sub_sector_test.cc
new file mode 100644
index 0000000..ff908f5
--- /dev/null
+++ b/pw_kvs/key_value_store_sub_sector_test.cc
@@ -0,0 +1,130 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_kvs/key_value_store.h"
+
+#include "pw_kvs/assert.h"
+#include "pw_kvs/devices/flash_memory.h"
+#include "pw_kvs/platform/board.h"
+#include "pw_kvs/status.h"
+#include "pw_kvs/test/fixture.h"
+#include "pw_kvs/test/framework.h"
+#include "pw_kvs/test/status_macros.h"
+
+#if USE_MEMORY_BUFFER
+#include "pw_kvs/test/fakes/in_memory_fake_flash.h"
+#endif  // USE_MEMORY_BUFFER
+
+namespace pw {
+namespace {
+
+#if USE_MEMORY_BUFFER
+InMemoryFakeFlash<1024, 4> test_flash(8);  // 4 x 1k sectors, 8 byte alignment
+FlashPartition test_partition(&test_flash, 0, test_flash.GetSectorCount());
+// Test KVS against FlashSubSector
+FlashMemorySubSector test_subsector_flash(&test_flash,
+                                          0,
+                                          128);  // Expose less than a sector.
+#else   // Device test
+FlashPartition& test_partition = Board::Instance().FlashExternalTestPartition();
+FlashMemorySubSector& test_subsector_flash =
+    Board::Instance().FlashMemorySubSectorTestChunk();
+#endif  // USE_MEMORY_BUFFER
+
+FlashPartition test_subsector_partition(&test_subsector_flash, 0, 1);
+KeyValueStore subsector_kvs(&test_subsector_partition);
+
+std::array<const char*, 3> keys{"TestKey1", "Key2", "TestKey3"};
+
+}  // namespace
+
+TEST(KeyValueStoreTest, WorksWithFlashSubSector) {
+  // The subsector region is assumed to be a part of the test partition.
+  // In order to clear state before the test, we must erase the entire test
+  // partition because erase operations are disallowed on FlashMemorySubSectors.
+  ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+  // Reset KVS
+  subsector_kvs.Disable();
+  ASSERT_OK(subsector_kvs.Enable());
+
+  // Add some data
+  uint8_t value1 = 0xDA;
+  ASSERT_OK(subsector_kvs.Put(keys[0], &value1, sizeof(value1)));
+
+  uint32_t value2 = 0xBAD0301f;
+  ASSERT_OK(subsector_kvs.Put(
+      keys[1], reinterpret_cast<uint8_t*>(&value2), sizeof(value2)));
+
+  // Verify data
+  uint32_t test2;
+  EXPECT_OK(subsector_kvs.Get(
+      keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+  uint8_t test1;
+  ASSERT_OK(subsector_kvs.Get(
+      keys[0], reinterpret_cast<uint8_t*>(&test1), sizeof(test1)));
+
+  EXPECT_EQ(test1, value1);
+
+  // Erase a key
+  ASSERT_OK(subsector_kvs.Erase(keys[0]));
+
+  // Verify it was erased
+  EXPECT_EQ(subsector_kvs
+                .Get(keys[0], reinterpret_cast<uint8_t*>(&test1), sizeof(test1))
+                .code(),
+            Status::NOT_FOUND);
+  test2 = 0;
+  ASSERT_OK(subsector_kvs.Get(
+      keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+  EXPECT_EQ(test2, value2);
+
+  // Erase other key
+  subsector_kvs.Erase(keys[1]);
+
+  // Verify it was erased
+  EXPECT_EQ(subsector_kvs.KeyCount(), 0);
+}
+
+TEST(KeyValueStoreTest, WorksWithFlashSubSector_MemoryExhausted) {
+  // The subsector region is assumed to be a part of the test partition.
+  // In order to clear state before the test, we must erase the entire test
+  // partition because erase operations are disallowed on FlashMemorySubSectors.
+  ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+  // Reset KVS
+  subsector_kvs.Disable();
+  ASSERT_OK(subsector_kvs.Enable());
+
+  // Store as much data as possible in the KVS until it fills up.
+  uint64_t test = 0;
+  for (; test < test_subsector_flash.GetSizeBytes(); test++) {
+    Status status = subsector_kvs.Put(keys[0], test);
+    if (status.ok()) {
+      continue;
+    }
+    // Expected code for erasure failure.
+    ASSERT_EQ(Status::RESOURCE_EXHAUSTED, status);
+    break;
+  }
+  EXPECT_GT(test, 0U);  // Should've at least succeeded with one Put.
+
+  // Even though we failed to fill the KVS, it still works, and we
+  // should have the previous test value as the most recent value in the KVS.
+  uint64_t value = 0;
+  ASSERT_OK(subsector_kvs.Get(keys[0], &value));
+  EXPECT_EQ(test - 1, value);
+}
+
+}  // namespace pw
diff --git a/pw_kvs/key_value_store_test.cc b/pw_kvs/key_value_store_test.cc
new file mode 100644
index 0000000..fa5eaf9
--- /dev/null
+++ b/pw_kvs/key_value_store_test.cc
@@ -0,0 +1,1396 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+#include "pw_kvs/key_value_store.h"
+
+#include "pw_kvs/assert.h"
+#include "pw_kvs/devices/flash_memory.h"
+#include "pw_kvs/os/stack_checks.h"
+#include "pw_kvs/platform/board.h"
+#include "pw_kvs/status.h"
+#include "pw_kvs/test/fixture.h"
+#include "pw_kvs/test/framework.h"
+#include "pw_kvs/test/status_macros.h"
+#include "pw_kvs/util/ccitt_crc16.h"
+
+#if USE_MEMORY_BUFFER
+#include "pw_kvs/test/fakes/in_memory_fake_flash.h"
+#endif  // USE_MEMORY_BUFFER
+
+namespace pw {
+namespace {
+
+#if USE_MEMORY_BUFFER
+// Although it might be useful to test other configurations, some tests require
+// at least 3 sectors; therfore it should have this when checked in.
+InMemoryFakeFlash<4 * 1024, 4> test_flash(
+    16);  // 4 x 4k sectors, 16 byte alignment
+FlashPartition test_partition(&test_flash, 0, test_flash.GetSectorCount());
+InMemoryFakeFlash<1024, 60> large_test_flash(8);
+FlashPartition large_test_partition(&large_test_flash,
+                                    0,
+                                    large_test_flash.GetSectorCount());
+#else   // Device test
+FlashPartition& test_partition = Board::Instance().FlashExternalTestPartition();
+#endif  // USE_MEMORY_BUFFER
+
+KeyValueStore kvs(&test_partition);
+
+// Use test fixture for logging support
+class KeyValueStoreTest : public ::testing::Test {
+ protected:
+  KeyValueStoreTest() : kvs_local_(&test_partition) {}
+
+  KeyValueStore kvs_local_;
+};
+
+std::array<uint8_t, 512> buffer;
+std::array<const char*, 3> keys{"TestKey1", "Key2", "TestKey3"};
+
+Status PaddedWrite(FlashPartition* partition,
+                   FlashPartition::Address address,
+                   const uint8_t* buf,
+                   uint16_t size) {
+  static constexpr uint16_t kMaxAlignmentBytes = 128;
+  uint8_t alignment_buffer[kMaxAlignmentBytes] = {0};
+  uint16_t aligned_bytes = size - (size % partition->GetAlignmentBytes());
+  RETURN_IF_ERROR(partition->Write(address, buf, aligned_bytes));
+  uint16_t remaining_bytes = size - aligned_bytes;
+  if (remaining_bytes > 0) {
+    memcpy(alignment_buffer, &buf[aligned_bytes], remaining_bytes);
+    RETURN_IF_ERROR(partition->Write(address + aligned_bytes,
+                                     alignment_buffer,
+                                     partition->GetAlignmentBytes()));
+  }
+  return Status::OK;
+}
+
+size_t RoundUpForAlignment(size_t size) {
+  uint16_t alignment = test_partition.GetAlignmentBytes();
+  if (size % alignment != 0) {
+    return size + alignment - size % alignment;
+  }
+  return size;
+}
+
+// This class gives attributes of KVS that we are testing against
+class KvsAttributes {
+ public:
+  KvsAttributes(size_t key_size, size_t data_size)
+      : sector_header_meta_size_(
+            RoundUpForAlignment(KeyValueStore::kHeaderSize)),
+        sector_header_clean_size_(
+            RoundUpForAlignment(KeyValueStore::kHeaderSize)),
+        chunk_header_size_(RoundUpForAlignment(KeyValueStore::kHeaderSize)),
+        data_size_(RoundUpForAlignment(data_size)),
+        key_size_(RoundUpForAlignment(key_size)),
+        erase_size_(chunk_header_size_ + key_size_),
+        min_put_size_(chunk_header_size_ + key_size_ + data_size_) {}
+
+  size_t SectorHeaderSize() {
+    return sector_header_meta_size_ + sector_header_clean_size_;
+  }
+  size_t SectorHeaderMetaSize() { return sector_header_meta_size_; }
+  size_t ChunkHeaderSize() { return chunk_header_size_; }
+  size_t DataSize() { return data_size_; }
+  size_t KeySize() { return key_size_; }
+  size_t EraseSize() { return erase_size_; }
+  size_t MinPutSize() { return min_put_size_; }
+
+ private:
+  const size_t sector_header_meta_size_;
+  const size_t sector_header_clean_size_;
+  const size_t chunk_header_size_;
+  const size_t data_size_;
+  const size_t key_size_;
+  const size_t erase_size_;
+  const size_t min_put_size_;
+};
+
+// Intention of this is to put and erase key-val to fill up sectors. It's a
+// helper function in testing how KVS handles cases where flash sector is full
+// or near full.
+void FillKvs(const char* key, size_t size_to_fill) {
+  constexpr size_t kTestDataSize = 8;
+  KvsAttributes kvs_attr(std::strlen(key), kTestDataSize);
+  const size_t kMaxPutSize =
+      buffer.size() + kvs_attr.ChunkHeaderSize() + kvs_attr.KeySize();
+
+  CHECK(size_to_fill >= kvs_attr.MinPutSize() + kvs_attr.EraseSize());
+
+  // Saving enough space to perform erase after loop
+  size_to_fill -= kvs_attr.EraseSize();
+  // We start with possible small chunk to prevent too small of a Kvs.Put() at
+  // the end.
+  size_t chunk_len =
+      std::max(kvs_attr.MinPutSize(), size_to_fill % buffer.size());
+  memset(buffer.data(), 0, buffer.size());
+  while (size_to_fill > 0) {
+    buffer[0]++;  // Changing buffer value so put actually does something
+    ASSERT_OK(
+        kvs.Put(key,
+                buffer.data(),
+                chunk_len - kvs_attr.ChunkHeaderSize() - kvs_attr.KeySize()));
+    size_to_fill -= chunk_len;
+    chunk_len = std::min(size_to_fill, kMaxPutSize);
+  }
+  ASSERT_OK(kvs.Erase(key));
+}
+
+uint16_t CalcKvsCrc(const char* key, const void* data, size_t data_len) {
+  CcittCrc16 crc;
+  static constexpr size_t kChunkKeyLengthMax = 15;
+  crc.AppendBytes(
+      ConstBuffer(reinterpret_cast<const uint8_t*>(key),
+                  pw::util::StringLength(key, kChunkKeyLengthMax)));
+  return crc.AppendBytes(
+      ConstBuffer(reinterpret_cast<const uint8_t*>(data), data_len));
+}
+
+uint16_t CalcTestPartitionCrc() {
+  uint8_t buf[16];  // Read as 16 byte chunks
+  CHECK_EQ(sizeof(buf) % test_partition.GetAlignmentBytes(), 0);
+  CHECK_EQ(test_partition.GetSizeBytes() % sizeof(buf), 0);
+  CcittCrc16 crc;
+  for (size_t i = 0; i < test_partition.GetSizeBytes(); i += sizeof(buf)) {
+    test_partition.Read(buf, i, sizeof(buf));
+    crc.AppendBytes(ConstBuffer(buf, sizeof(buf)));
+  }
+  return crc.CurrentValue();
+}
+}  // namespace
+
+TEST_F(KeyValueStoreTest, FuzzTest) {
+  if (test_partition.GetSectorSizeBytes() < 4 * 1024 ||
+      test_partition.GetSectorCount() < 4) {
+    LOG_INFO("Sectors too small, skipping test.");
+    return;  // TODO: Test could be generalized
+  }
+  // Erase
+  ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+  const char* key1 = "Buf1";
+  const char* key2 = "Buf2";
+  const size_t kLargestBufSize = 3 * 1024;
+  static uint8_t buf1[kLargestBufSize];
+  static uint8_t buf2[kLargestBufSize];
+  memset(buf1, 1, sizeof(buf1));
+  memset(buf2, 2, sizeof(buf2));
+
+  // Start with things in KVS
+  ASSERT_OK(kvs.Put(key1, buf1, sizeof(buf1)));
+  ASSERT_OK(kvs.Put(key2, buf2, sizeof(buf2)));
+  for (size_t j = 0; j < keys.size(); j++) {
+    ASSERT_OK(kvs.Put(keys[j], j));
+  }
+
+  for (size_t i = 0; i < 100; i++) {
+    // Vary two sizes
+    size_t size1 = (kLargestBufSize) / (i + 1);
+    size_t size2 = (kLargestBufSize) / (100 - i);
+    for (size_t j = 0; j < 50; j++) {
+      // Rewrite a single key many times, can fill up a sector
+      ASSERT_OK(kvs.Put("some_data", j));
+    }
+    // Delete and re-add everything
+    ASSERT_OK(kvs.Erase(key1));
+    ASSERT_OK(kvs.Put(key1, buf1, size1));
+    ASSERT_OK(kvs.Erase(key2));
+    ASSERT_OK(kvs.Put(key2, buf2, size2));
+    for (size_t j = 0; j < keys.size(); j++) {
+      ASSERT_OK(kvs.Erase(keys[j]));
+      ASSERT_OK(kvs.Put(keys[j], j));
+    }
+
+    // Re-enable and verify
+    kvs.Disable();
+    ASSERT_OK(kvs.Enable());
+    static uint8_t buf[4 * 1024];
+    ASSERT_OK(kvs.Get(key1, buf, size1));
+    ASSERT_EQ(memcmp(buf, buf1, size1), 0);
+    ASSERT_OK(kvs.Get(key2, buf, size2));
+    ASSERT_EQ(memcmp(buf2, buf2, size2), 0);
+    for (size_t j = 0; j < keys.size(); j++) {
+      size_t ret = 1000;
+      ASSERT_OK(kvs.Get(keys[j], &ret));
+      ASSERT_EQ(ret, j);
+    }
+  }
+}
+
+TEST_F(KeyValueStoreTest, Basic) {
+  // Erase
+  ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  // Add some data
+  uint8_t value1 = 0xDA;
+  ASSERT_OK(kvs.Put(keys[0], &value1, sizeof(value1)));
+
+  uint32_t value2 = 0xBAD0301f;
+  ASSERT_OK(
+      kvs.Put(keys[1], reinterpret_cast<uint8_t*>(&value2), sizeof(value2)));
+
+  // Verify data
+  uint32_t test2;
+  EXPECT_OK(
+      kvs.Get(keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+  uint8_t test1;
+  ASSERT_OK(
+      kvs.Get(keys[0], reinterpret_cast<uint8_t*>(&test1), sizeof(test1)));
+
+  EXPECT_EQ(test1, value1);
+  EXPECT_EQ(test2, value2);
+
+  // Erase a key
+  EXPECT_OK(kvs.Erase(keys[0]));
+
+  // Verify it was erased
+  EXPECT_EQ(kvs.Get(keys[0], reinterpret_cast<uint8_t*>(&test1), sizeof(test1))
+                .code(),
+            Status::NOT_FOUND);
+  test2 = 0;
+  ASSERT_OK(
+      kvs.Get(keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+  EXPECT_EQ(test2, value2);
+
+  // Erase other key
+  kvs.Erase(keys[1]);
+
+  // Verify it was erased
+  EXPECT_EQ(kvs.KeyCount(), 0);
+}
+
+TEST_F(KeyValueStoreTest, MaxKeyLength) {
+  // Erase
+  ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  // Add some data
+  char key[16] = "123456789abcdef";  // key length 15 (without \0)
+  int value = 1;
+  ASSERT_OK(kvs.Put(key, value));
+
+  // Verify data
+  int test = 0;
+  ASSERT_OK(kvs.Get(key, &test));
+  EXPECT_EQ(test, value);
+
+  // Erase a key
+  kvs.Erase(key);
+
+  // Verify it was erased
+  EXPECT_EQ(kvs.Get(key, &test).code(), Status::NOT_FOUND);
+}
+
+TEST_F(KeyValueStoreTest, LargeBuffers) {
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  // Note this assumes that no other keys larger then key0
+  static_assert(sizeof(keys[0]) >= sizeof(keys[1]) &&
+                sizeof(keys[0]) >= sizeof(keys[2]));
+  KvsAttributes kvs_attr(std::strlen(keys[0]), buffer.size());
+
+  // Verify the data will fit in this test partition. This checks that all the
+  // keys chunks will fit and a header for each sector will fit. It requires 1
+  // empty sector also.
+  const size_t kAllChunkSize = kvs_attr.MinPutSize() * keys.size();
+  const size_t kAllSectorHeaderSizes =
+      kvs_attr.SectorHeaderSize() * (test_partition.GetSectorCount() - 1);
+  const size_t kMinSize = kAllChunkSize + kAllSectorHeaderSizes;
+  const size_t kAvailSectorSpace = test_partition.GetSectorSizeBytes() *
+                                   (test_partition.GetSectorCount() - 1);
+  if (kAvailSectorSpace < kMinSize) {
+    LOG_INFO("KVS too small, skipping test.");
+    return;
+  }
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  // Add and verify
+  for (unsigned add_idx = 0; add_idx < keys.size(); add_idx++) {
+    memset(buffer.data(), add_idx, buffer.size());
+    ASSERT_OK(kvs.Put(keys[add_idx], buffer.data(), buffer.size()));
+    EXPECT_EQ(kvs.KeyCount(), add_idx + 1);
+    for (unsigned verify_idx = 0; verify_idx <= add_idx; verify_idx++) {
+      memset(buffer.data(), 0, buffer.size());
+      ASSERT_OK(kvs.Get(keys[verify_idx], buffer.data(), buffer.size()));
+      for (unsigned i = 0; i < buffer.size(); i++) {
+        EXPECT_EQ(buffer[i], verify_idx);
+      }
+    }
+  }
+
+  // Erase and verify
+  for (unsigned erase_idx = 0; erase_idx < keys.size(); erase_idx++) {
+    ASSERT_OK(kvs.Erase(keys[erase_idx]));
+    EXPECT_EQ(kvs.KeyCount(), keys.size() - erase_idx - 1);
+    for (unsigned verify_idx = 0; verify_idx < keys.size(); verify_idx++) {
+      memset(buffer.data(), 0, buffer.size());
+      if (verify_idx <= erase_idx) {
+        ASSERT_EQ(
+            kvs.Get(keys[verify_idx], buffer.data(), buffer.size()).code(),
+            Status::NOT_FOUND);
+      } else {
+        ASSERT_OK(kvs.Get(keys[verify_idx], buffer.data(), buffer.size()));
+        for (uint32_t i = 0; i < buffer.size(); i++) {
+          EXPECT_EQ(buffer[i], verify_idx);
+        }
+      }
+    }
+  }
+}
+
+TEST_F(KeyValueStoreTest, Enable) {
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  KvsAttributes kvs_attr(std::strlen(keys[0]), buffer.size());
+
+  // Verify the data will fit in this test partition. This checks that all the
+  // keys chunks will fit and a header for each sector will fit. It requires 1
+  // empty sector also.
+  const size_t kAllChunkSize = kvs_attr.MinPutSize() * keys.size();
+  const size_t kAllSectorHeaderSizes =
+      kvs_attr.SectorHeaderSize() * (test_partition.GetSectorCount() - 1);
+  const size_t kMinSize = kAllChunkSize + kAllSectorHeaderSizes;
+  const size_t kAvailSectorSpace = test_partition.GetSectorSizeBytes() *
+                                   (test_partition.GetSectorCount() - 1);
+  if (kAvailSectorSpace < kMinSize) {
+    LOG_INFO("KVS too small, skipping test.");
+    return;
+  }
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  // Add some items
+  for (unsigned add_idx = 0; add_idx < keys.size(); add_idx++) {
+    memset(buffer.data(), add_idx, buffer.size());
+    ASSERT_OK(kvs.Put(keys[add_idx], buffer.data(), buffer.size()));
+    EXPECT_EQ(kvs.KeyCount(), add_idx + 1);
+  }
+
+  // Enable different KVS which should be able to properly setup the same map
+  // from what is stored in flash.
+  ASSERT_OK(kvs_local_.Enable());
+  EXPECT_EQ(kvs_local_.KeyCount(), keys.size());
+
+  // Ensure adding to new KVS works
+  uint8_t value = 0xDA;
+  const char* key = "new_key";
+  ASSERT_OK(kvs_local_.Put(key, &value, sizeof(value)));
+  uint8_t test;
+  ASSERT_OK(kvs_local_.Get(key, &test, sizeof(test)));
+  EXPECT_EQ(value, test);
+  EXPECT_EQ(kvs_local_.KeyCount(), keys.size() + 1);
+
+  // Verify previous data
+  for (unsigned verify_idx = 0; verify_idx < keys.size(); verify_idx++) {
+    memset(buffer.data(), 0, buffer.size());
+    ASSERT_OK(kvs_local_.Get(keys[verify_idx], buffer.data(), buffer.size()));
+    for (uint32_t i = 0; i < buffer.size(); i++) {
+      EXPECT_EQ(buffer[i], verify_idx);
+    }
+  }
+}
+
+TEST_F(KeyValueStoreTest, MultiSector) {
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  // Calculate number of elements to ensure multiple sectors are required.
+  uint16_t add_count =
+      (test_partition.GetSectorSizeBytes() / buffer.size()) + 1;
+
+  if (kvs.GetMaxKeys() < add_count) {
+    LOG_INFO("Sector size too large, skipping test.");
+    return;  // this chip has very large sectors, test won't work
+  }
+  if (test_partition.GetSectorCount() < 3) {
+    LOG_INFO("Not enough sectors, skipping test.");
+    return;  // need at least 3 sectors for multi-sector test
+  }
+
+  char key[20];
+  for (unsigned add_idx = 0; add_idx < add_count; add_idx++) {
+    memset(buffer.data(), add_idx, buffer.size());
+    snprintf(key, sizeof(key), "key_%u", add_idx);
+    ASSERT_OK(kvs.Put(key, buffer.data(), buffer.size()));
+    EXPECT_EQ(kvs.KeyCount(), add_idx + 1);
+  }
+
+  for (unsigned verify_idx = 0; verify_idx < add_count; verify_idx++) {
+    memset(buffer.data(), 0, buffer.size());
+    snprintf(key, sizeof(key), "key_%u", verify_idx);
+    ASSERT_OK(kvs.Get(key, buffer.data(), buffer.size()));
+    for (uint32_t i = 0; i < buffer.size(); i++) {
+      EXPECT_EQ(buffer[i], verify_idx);
+    }
+  }
+
+  // Check erase
+  for (unsigned erase_idx = 0; erase_idx < add_count; erase_idx++) {
+    snprintf(key, sizeof(key), "key_%u", erase_idx);
+    ASSERT_OK(kvs.Erase(key));
+    EXPECT_EQ(kvs.KeyCount(), add_count - erase_idx - 1);
+  }
+}
+
+TEST_F(KeyValueStoreTest, RewriteValue) {
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  // Write first value
+  const uint8_t kValue1 = 0xDA;
+  const uint8_t kValue2 = 0x12;
+  const char* key = "the_key";
+  ASSERT_OK(kvs.Put(key, &kValue1, sizeof(kValue1)));
+
+  // Verify
+  uint8_t value;
+  ASSERT_OK(kvs.Get(key, reinterpret_cast<uint8_t*>(&value), sizeof(value)));
+  EXPECT_EQ(kValue1, value);
+
+  // Write new value for key
+  ASSERT_OK(kvs.Put(key, &kValue2, sizeof(kValue2)));
+
+  // Verify
+  ASSERT_OK(kvs.Get(key, reinterpret_cast<uint8_t*>(&value), sizeof(value)));
+  EXPECT_EQ(kValue2, value);
+
+  // Verify only 1 element exists
+  EXPECT_EQ(kvs.KeyCount(), 1);
+}
+
+TEST_F(KeyValueStoreTest, OffsetRead) {
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  const char* key = "the_key";
+  constexpr uint8_t kReadSize = 16;  // needs to be a multiple of alignment
+  constexpr uint8_t kTestBufferSize = kReadSize * 10;
+  CHECK_GT(buffer.size(), kTestBufferSize);
+  CHECK_LE(kTestBufferSize, 0xFF);
+
+  // Write the entire buffer
+  for (uint8_t i = 0; i < kTestBufferSize; i++) {
+    buffer[i] = i;
+  }
+  ASSERT_OK(kvs.Put(key, buffer.data(), kTestBufferSize));
+  EXPECT_EQ(kvs.KeyCount(), 1);
+
+  // Read in small chunks and verify
+  for (int i = 0; i < kTestBufferSize / kReadSize; i++) {
+    memset(buffer.data(), 0, buffer.size());
+    ASSERT_OK(kvs.Get(key, buffer.data(), kReadSize, i * kReadSize));
+    for (unsigned j = 0; j < kReadSize; j++) {
+      ASSERT_EQ(buffer[j], j + i * kReadSize);
+    }
+  }
+}
+
+TEST_F(KeyValueStoreTest, MultipleRewrite) {
+  // Write many large buffers to force moving to new sector.
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  // Calculate number of elements to ensure multiple sectors are required.
+  unsigned add_count =
+      (test_partition.GetSectorSizeBytes() / buffer.size()) + 1;
+
+  const char* key = "the_key";
+  constexpr uint8_t kGoodVal = 0x60;
+  constexpr uint8_t kBadVal = 0xBA;
+  memset(buffer.data(), kBadVal, buffer.size());
+  for (unsigned add_idx = 0; add_idx < add_count; add_idx++) {
+    if (add_idx == add_count - 1) {  // last value
+      memset(buffer.data(), kGoodVal, buffer.size());
+    }
+    ASSERT_OK(kvs.Put(key, buffer.data(), buffer.size()));
+    EXPECT_EQ(kvs.KeyCount(), 1);
+  }
+
+  // Verify
+  memset(buffer.data(), 0, buffer.size());
+  ASSERT_OK(kvs.Get(key, buffer.data(), buffer.size()));
+  for (uint32_t i = 0; i < buffer.size(); i++) {
+    ASSERT_EQ(buffer[i], kGoodVal);
+  }
+}
+
+TEST_F(KeyValueStoreTest, FillSector) {
+  // Write key[0], Write/erase Key[2] multiple times to fill sector check
+  // everything makes sense after.
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  ASSERT_EQ(std::strlen(keys[0]), 8U);  // Easier for alignment
+  ASSERT_EQ(std::strlen(keys[2]), 8U);  // Easier for alignment
+  constexpr size_t kTestDataSize = 8;
+  KvsAttributes kvs_attr(std::strlen(keys[2]), kTestDataSize);
+  int bytes_remaining =
+      test_partition.GetSectorSizeBytes() - kvs_attr.SectorHeaderSize();
+  constexpr uint8_t kKey0Pattern = 0xBA;
+
+  memset(buffer.data(), kKey0Pattern, kvs_attr.DataSize());
+  ASSERT_OK(kvs.Put(keys[0], buffer.data(), kvs_attr.DataSize()));
+  bytes_remaining -= kvs_attr.MinPutSize();
+  memset(buffer.data(), 1, kvs_attr.DataSize());
+  ASSERT_OK(kvs.Put(keys[2], buffer.data(), kvs_attr.DataSize()));
+  bytes_remaining -= kvs_attr.MinPutSize();
+  EXPECT_EQ(kvs.KeyCount(), 2);
+  ASSERT_OK(kvs.Erase(keys[2]));
+  bytes_remaining -= kvs_attr.EraseSize();
+  EXPECT_EQ(kvs.KeyCount(), 1);
+
+  // Intentionally adding erase size to trigger sector cleanup
+  bytes_remaining += kvs_attr.EraseSize();
+  FillKvs(keys[2], bytes_remaining);
+
+  // Verify key[0]
+  memset(buffer.data(), 0, kvs_attr.DataSize());
+  ASSERT_OK(kvs.Get(keys[0], buffer.data(), kvs_attr.DataSize()));
+  for (uint32_t i = 0; i < kvs_attr.DataSize(); i++) {
+    EXPECT_EQ(buffer[i], kKey0Pattern);
+  }
+}
+
+TEST_F(KeyValueStoreTest, Interleaved) {
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  const uint8_t kValue1 = 0xDA;
+  const uint8_t kValue2 = 0x12;
+  uint8_t value[1];
+  ASSERT_OK(kvs.Put(keys[0], &kValue1, sizeof(kValue1)));
+  EXPECT_EQ(kvs.KeyCount(), 1);
+  ASSERT_OK(kvs.Erase(keys[0]));
+  EXPECT_EQ(kvs.Get(keys[0], value, sizeof(value)), Status::NOT_FOUND);
+  ASSERT_OK(kvs.Put(keys[1], &kValue1, sizeof(kValue1)));
+  ASSERT_OK(kvs.Put(keys[2], &kValue2, sizeof(kValue2)));
+  ASSERT_OK(kvs.Erase(keys[1]));
+  EXPECT_OK(kvs.Get(keys[2], value, sizeof(value)));
+  EXPECT_EQ(kValue2, value[0]);
+
+  EXPECT_EQ(kvs.KeyCount(), 1);
+}
+
+TEST_F(KeyValueStoreTest, BadCrc) {
+  static constexpr uint32_t kTestPattern = 0xBAD0301f;
+  // clang-format off
+  // There is a top and bottom because for each because we don't want to write
+  // the erase 0xFF, especially on encrypted flash.
+  static constexpr uint8_t kKvsTestDataAligned1Top[] = {
+      0xCD, 0xAB, 0x03, 0x00, 0x01, 0x00, 0xFF, 0xFF,  // Sector Header
+  };
+  static constexpr uint8_t kKvsTestDataAligned1Bottom[] = {
+      0xAA, 0x55, 0xBA, 0xDD, 0x00, 0x00, 0x18, 0x00,  // header (BAD CRC)
+      0x54, 0x65, 0x73, 0x74, 0x4B, 0x65, 0x79, 0x31,  // Key (keys[0])
+      0xDA,                                            // Value
+      0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00,  // Header (GOOD CRC)
+      0x4B, 0x65, 0x79, 0x32,                          // Key (keys[1])
+      0x1F, 0x30, 0xD0, 0xBA};                         // Value
+  static constexpr uint8_t kKvsTestDataAligned2Top[] = {
+      0xCD, 0xAB, 0x03, 0x00, 0x02, 0x00, 0xFF, 0xFF,  // Sector Header
+  };
+  static constexpr uint8_t kKvsTestDataAligned2Bottom[] = {
+      0xAA, 0x55, 0xBA, 0xDD, 0x00, 0x00, 0x18, 0x00,  // header (BAD CRC)
+      0x54, 0x65, 0x73, 0x74, 0x4B, 0x65, 0x79, 0x31,  // Key (keys[0])
+      0xDA, 0x00,                                      // Value + padding
+      0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00,  // Header (GOOD CRC)
+      0x4B, 0x65, 0x79, 0x32,                          // Key (keys[1])
+      0x1F, 0x30, 0xD0, 0xBA};                         // Value
+  static constexpr uint8_t kKvsTestDataAligned8Top[] = {
+      0xCD, 0xAB, 0x03, 0x00, 0x08, 0x00, 0xFF, 0xFF,  // Sector Header
+  };
+  static constexpr uint8_t kKvsTestDataAligned8Bottom[] = {
+      0xAA, 0x55, 0xBA, 0xDD, 0x00, 0x00, 0x18, 0x00,  // header (BAD CRC)
+      0x54, 0x65, 0x73, 0x74, 0x4B, 0x65, 0x79, 0x31,  // Key (keys[0])
+      0xDA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Value + padding
+      0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00,  // header (GOOD CRC)
+      0x4B, 0x65, 0x79, 0x32, 0x00, 0x00, 0x00, 0x00,  // Key (keys[1])
+      0x1F, 0x30, 0xD0, 0xBA, 0x00, 0x00, 0x00, 0x00,  // Value + padding
+  };
+  static constexpr uint8_t kKvsTestDataAligned16Top[] = {
+      0xCD, 0xAB, 0x03, 0x00, 0x10, 0x00, 0xFF, 0xFF,  // Sector Header
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Alignment to 16
+  };
+  static constexpr uint8_t kKvsTestDataAligned16Bottom[] = {
+      0xAA, 0x55, 0xBA, 0xDD, 0x00, 0x00, 0x18, 0x00,  // header (BAD CRC)
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Alignment to 16
+      0x54, 0x65, 0x73, 0x74, 0x4B, 0x65, 0x79, 0x31,  // Key (keys[0])
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Alignment to 16
+      0xDA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Value + padding
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Alignment to 16
+      0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00,  // header (GOOD CRC)
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Alignment to 16
+      0x4B, 0x65, 0x79, 0x32, 0x00, 0x00, 0x00, 0x00,  // Key (keys[1])
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Alignment to 16
+      0x1F, 0x30, 0xD0, 0xBA, 0x00, 0x00, 0x00, 0x00,  // Value + padding
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Alignment to 16
+  };
+  // clang-format on
+  ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+
+  // We don't actually care about the size values provided, since we are only
+  // using kvs_attr to get Sector Size
+  KvsAttributes kvs_attr(8, 8);
+  if (test_partition.GetAlignmentBytes() == 1) {
+    ASSERT_OK(test_partition.Write(
+        0, kKvsTestDataAligned1Top, sizeof(kKvsTestDataAligned1Top)));
+    ASSERT_OK(test_partition.Write(kvs_attr.SectorHeaderSize(),
+                                   kKvsTestDataAligned1Bottom,
+                                   sizeof(kKvsTestDataAligned1Bottom)));
+  } else if (test_partition.GetAlignmentBytes() == 2) {
+    ASSERT_OK(test_partition.Write(
+        0, kKvsTestDataAligned2Top, sizeof(kKvsTestDataAligned2Top)));
+    ASSERT_OK(test_partition.Write(kvs_attr.SectorHeaderSize(),
+                                   kKvsTestDataAligned2Bottom,
+                                   sizeof(kKvsTestDataAligned2Bottom)));
+  } else if (test_partition.GetAlignmentBytes() == 8) {
+    ASSERT_OK(test_partition.Write(
+        0, kKvsTestDataAligned8Top, sizeof(kKvsTestDataAligned8Top)));
+    ASSERT_OK(test_partition.Write(kvs_attr.SectorHeaderSize(),
+                                   kKvsTestDataAligned8Bottom,
+                                   sizeof(kKvsTestDataAligned8Bottom)));
+  } else if (test_partition.GetAlignmentBytes() == 16) {
+    ASSERT_OK(test_partition.Write(
+        0, kKvsTestDataAligned16Top, sizeof(kKvsTestDataAligned16Top)));
+    ASSERT_OK(test_partition.Write(kvs_attr.SectorHeaderSize(),
+                                   kKvsTestDataAligned16Bottom,
+                                   sizeof(kKvsTestDataAligned16Bottom)));
+  } else {
+    LOG_ERROR("Test only supports 1, 2, 8 and 16 byte alignments.");
+    ASSERT_OK(false);
+  }
+
+  EXPECT_EQ(kvs_local_.Enable(), Status::OK);
+  EXPECT_TRUE(kvs_local_.IsEnabled());
+
+  EXPECT_EQ(kvs_local_.Get(keys[0], buffer.data(), 1), Status::DATA_LOSS);
+
+  // Value with correct CRC should still be available.
+  uint32_t test2 = 0;
+  ASSERT_OK(kvs_local_.Get(
+      keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+  EXPECT_EQ(kTestPattern, test2);
+
+  // Test rewriting over corrupted data.
+  ASSERT_OK(kvs_local_.Put(keys[0], kTestPattern));
+  test2 = 0;
+  EXPECT_OK(kvs_local_.Get(keys[0], &test2));
+  EXPECT_EQ(kTestPattern, test2);
+
+  // Check correct when re-enabled
+  kvs_local_.Disable();
+  EXPECT_EQ(kvs_local_.Enable(), Status::OK);
+  test2 = 0;
+  EXPECT_OK(kvs_local_.Get(keys[0], &test2));
+  EXPECT_EQ(kTestPattern, test2);
+}
+
+TEST_F(KeyValueStoreTest, TestVersion2) {
+  static constexpr uint32_t kTestPattern = 0xBAD0301f;
+  // Since this test is not run on encypted flash, we can write the clean
+  // pending flag for just this test.
+  static constexpr uint8_t kKvsTestDataAligned1[] = {
+      0xCD, 0xAB, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF,  // Sector Header
+      0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,  // Clean pending flag
+      0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00,  // Header (GOOD CRC)
+      0x4B, 0x65, 0x79, 0x32,                          // Key (keys[1])
+      0x1F, 0x30, 0xD0, 0xBA};                         // Value
+
+  if (test_partition.GetAlignmentBytes() == 1) {
+    // Test only runs on 1 byte alignment partitions
+    test_partition.Erase(0, test_partition.GetSectorCount());
+    test_partition.Write(0, kKvsTestDataAligned1, sizeof(kKvsTestDataAligned1));
+    EXPECT_OK(kvs_local_.Enable());
+    uint32_t test2 = 0;
+    ASSERT_OK(kvs_local_.Get(
+        keys[1], reinterpret_cast<uint8_t*>(&test2), sizeof(test2)));
+    EXPECT_EQ(kTestPattern, test2);
+  }
+}
+
+TEST_F(KeyValueStoreTest, ReEnable) {
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+  kvs.Disable();
+
+  EXPECT_OK(kvs_local_.Enable());
+  // Write value
+  const uint8_t kValue = 0xDA;
+  ASSERT_OK(kvs_local_.Put(keys[0], &kValue, sizeof(kValue)));
+  uint8_t value;
+  ASSERT_OK(kvs_local_.Get(
+      keys[0], reinterpret_cast<uint8_t*>(&value), sizeof(value)));
+
+  // Verify
+  EXPECT_EQ(kValue, value);
+}
+
+TEST_F(KeyValueStoreTest, Erase) {
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  // Write value
+  const uint8_t kValue = 0xDA;
+  ASSERT_OK(kvs.Put(keys[0], &kValue, sizeof(kValue)));
+
+  ASSERT_OK(kvs.Erase(keys[0]));
+  uint8_t value[1];
+  ASSERT_EQ(kvs.Get(keys[0], value, sizeof(value)), Status::NOT_FOUND);
+
+  // Reset KVS, ensure captured at enable
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  ASSERT_EQ(kvs.Get(keys[0], value, sizeof(value)), Status::NOT_FOUND);
+}
+
+TEST_F(KeyValueStoreTest, TemplatedPutAndGet) {
+  test_partition.Erase(0, test_partition.GetSectorCount());
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+  // Store a value with the convenience method.
+  const uint32_t kValue = 0x12345678;
+  ASSERT_OK(kvs.Put(keys[0], kValue));
+
+  // Read it back with the other convenience method.
+  uint32_t value;
+  ASSERT_OK(kvs.Get(keys[0], &value));
+  ASSERT_EQ(kValue, value);
+
+  // Make sure we cannot get something where size isn't what we expect
+  const uint8_t kSmallValue = 0xBA;
+  uint8_t small_value = kSmallValue;
+  ASSERT_EQ(kvs.Get(keys[0], &small_value), Status::INVALID_ARGUMENT);
+  ASSERT_EQ(small_value, kSmallValue);
+}
+
+TEST_F(KeyValueStoreTest, SameValueRewrite) {
+  static constexpr uint32_t kTestPattern = 0xBAD0301f;
+  // clang-format off
+  static constexpr uint8_t kKvsTestDataAligned1Top[] = {
+      0xCD, 0xAB, 0x02, 0x00, 0x00, 0x00, 0xFF, 0xFF,  // Sector Header
+  };
+  static constexpr uint8_t kKvsTestDataAligned1Bottom[] = {
+      0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00,  // Header (GOOD CRC)
+      0x4B, 0x65, 0x79, 0x32,                          // Key (keys[1])
+      0x1F, 0x30, 0xD0, 0xBA};                         // Value
+  static constexpr uint8_t kKvsTestDataAligned2Top[] = {
+      0xCD, 0xAB, 0x03, 0x00, 0x02, 0x00, 0xFF, 0xFF,  // Sector Header
+  };
+  static constexpr uint8_t kKvsTestDataAligned2Bottom[] = {
+      0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00,  // Header (GOOD CRC)
+      0x4B, 0x65, 0x79, 0x32,                          // Key (keys[1])
+      0x1F, 0x30, 0xD0, 0xBA};                         // Value
+  static constexpr uint8_t kKvsTestDataAligned8Top[] = {
+      0xCD, 0xAB, 0x03, 0x00, 0x08, 0x00, 0xFF, 0xFF,  // Sector Header
+  };
+  static constexpr uint8_t kKvsTestDataAligned8Bottom[] = {
+      0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00,  // header (GOOD CRC)
+      0x4B, 0x65, 0x79, 0x32, 0x00, 0x00, 0x00, 0x00,  // Key (keys[1])
+      0x1F, 0x30, 0xD0, 0xBA, 0x00, 0x00, 0x00, 0x00,  // Value + padding
+  };
+  static constexpr uint8_t kKvsTestDataAligned16Top[] = {
+      0xCD, 0xAB, 0x03, 0x00, 0x10, 0x00, 0xFF, 0xFF,  // Sector Header
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Alignment to 16
+  };
+  static constexpr uint8_t kKvsTestDataAligned16Bottom[] = {
+      0xAA, 0x55, 0xB5, 0x87, 0x00, 0x00, 0x44, 0x00,  // header (GOOD CRC)
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Alignment to 16
+      0x4B, 0x65, 0x79, 0x32, 0x00, 0x00, 0x00, 0x00,  // Key (keys[1])
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Alignment to 16
+      0x1F, 0x30, 0xD0, 0xBA, 0x00, 0x00, 0x00, 0x00,  // Value + padding
+      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // Alignment to 16
+  };
+  // clang-format on
+
+  ASSERT_OK(test_partition.Erase(0, test_partition.GetSectorCount()));
+  // We don't actually care about the size values provided, since we are only
+  // using kvs_attr to get Sector Size
+  KvsAttributes kvs_attr(8, 8);
+  pw::FlashPartition::Address address = kvs_attr.SectorHeaderSize();
+  if (test_partition.GetAlignmentBytes() == 1) {
+    ASSERT_OK(test_partition.Write(
+        0, kKvsTestDataAligned1Top, sizeof(kKvsTestDataAligned1Top)));
+    ASSERT_OK(test_partition.Write(address,
+                                   kKvsTestDataAligned1Bottom,
+                                   sizeof(kKvsTestDataAligned1Bottom)));
+    address += sizeof(kKvsTestDataAligned1Bottom);
+  } else if (test_partition.GetAlignmentBytes() == 2) {
+    ASSERT_OK(test_partition.Write(
+        0, kKvsTestDataAligned2Top, sizeof(kKvsTestDataAligned2Top)));
+    ASSERT_OK(test_partition.Write(address,
+                                   kKvsTestDataAligned2Bottom,
+                                   sizeof(kKvsTestDataAligned2Bottom)));
+    address += sizeof(kKvsTestDataAligned2Bottom);
+  } else if (test_partition.GetAlignmentBytes() == 8) {
+    ASSERT_OK(test_partition.Write(
+        0, kKvsTestDataAligned8Top, sizeof(kKvsTestDataAligned8Top)));
+    ASSERT_OK(test_partition.Write(address,
+                                   kKvsTestDataAligned8Bottom,
+                                   sizeof(kKvsTestDataAligned8Bottom)));
+    address += sizeof(kKvsTestDataAligned8Bottom);
+  } else if (test_partition.GetAlignmentBytes() == 16) {
+    ASSERT_OK(test_partition.Write(
+        0, kKvsTestDataAligned16Top, sizeof(kKvsTestDataAligned16Top)));
+    ASSERT_OK(test_partition.Write(address,
+                                   kKvsTestDataAligned16Bottom,
+                                   sizeof(kKvsTestDataAligned16Bottom)));
+    address += sizeof(kKvsTestDataAligned16Bottom);
+  } else {
+    LOG_ERROR("Test only supports 1, 2, 8 and 16 byte alignments.");
+    ASSERT_EQ(true, false);
+  }
+
+  ASSERT_OK(kvs_local_.Enable());
+  EXPECT_TRUE(kvs_local_.IsEnabled());
+
+  // Put in same key/value pair
+  ASSERT_OK(kvs_local_.Put(keys[1], kTestPattern));
+
+  bool is_erased = false;
+  ASSERT_OK(test_partition.IsChunkErased(
+      address, test_partition.GetAlignmentBytes(), &is_erased));
+  EXPECT_EQ(is_erased, true);
+}
+
+// This test is derived from bug that was discovered. Testing this corner case
+// relies on creating a new key-value just under the size that is left over in
+// the sector.
+TEST_F(KeyValueStoreTest, FillSector2) {
+  if (test_partition.GetSectorCount() < 3) {
+    LOG_INFO("Not enough sectors, skipping test.");
+    return;  // need at least 3 sectors
+  }
+
+  // Reset KVS
+  kvs.Disable();
+  test_partition.Erase(0, test_partition.GetSectorCount());
+  ASSERT_OK(kvs.Enable());
+
+  // Start of by filling flash sector to near full
+  constexpr int kHalfBufferSize = buffer.size() / 2;
+  const int kSizeToFill = test_partition.GetSectorSizeBytes() - kHalfBufferSize;
+  constexpr size_t kTestDataSize = 8;
+  KvsAttributes kvs_attr(std::strlen(keys[2]), kTestDataSize);
+
+  FillKvs(keys[2], kSizeToFill);
+
+  // Find out how much space is remaining for new key-value and confirm it
+  // makes sense.
+  size_t new_keyvalue_size = 0;
+  size_t alignment = test_partition.GetAlignmentBytes();
+  // Starts on second sector since it will try to keep first sector free
+  pw::FlashPartition::Address read_address =
+      2 * test_partition.GetSectorSizeBytes() - alignment;
+  for (; read_address > 0; read_address -= alignment) {
+    bool is_erased = false;
+    ASSERT_OK(
+        test_partition.IsChunkErased(read_address, alignment, &is_erased));
+    if (is_erased) {
+      new_keyvalue_size += alignment;
+    } else {
+      break;
+    }
+  }
+
+  size_t expected_remaining = test_partition.GetSectorSizeBytes() -
+                              kvs_attr.SectorHeaderSize() - kSizeToFill;
+  ASSERT_EQ(new_keyvalue_size, expected_remaining);
+
+  const char* kNewKey = "NewKey";
+  constexpr size_t kValueLessThanChunkHeaderSize = 2;
+  constexpr uint8_t kTestPattern = 0xBA;
+  new_keyvalue_size -= kValueLessThanChunkHeaderSize;
+  memset(buffer.data(), kTestPattern, new_keyvalue_size);
+  ASSERT_OK(kvs.Put(kNewKey, buffer.data(), new_keyvalue_size));
+
+  // In failed corner case, adding new key is deceptively successful. It isn't
+  // until KVS is disabled and reenabled that issue can be detected.
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  // Might as well check that new key-value is what we expect it to be
+  ASSERT_OK(kvs.Get(kNewKey, buffer.data(), new_keyvalue_size));
+  for (size_t i = 0; i < new_keyvalue_size; i++) {
+    EXPECT_EQ(buffer[i], kTestPattern);
+  }
+}
+
+TEST_F(KeyValueStoreTest, GetValueSizeTests) {
+  constexpr uint16_t kSizeOfValueToFill = 20U;
+  constexpr uint8_t kKey0Pattern = 0xBA;
+  uint16_t value_size = 0;
+  // Start off with disabled KVS
+  kvs.Disable();
+
+  // Try getting value when KVS is disabled, expect failure
+  EXPECT_EQ(kvs.GetValueSize(keys[0], &value_size),
+            Status::FAILED_PRECONDITION);
+
+  // Reset KVS
+  test_partition.Erase(0, test_partition.GetSectorCount());
+  ASSERT_OK(kvs.Enable());
+
+  // Try some case that are expected to fail
+  ASSERT_EQ(kvs.GetValueSize(keys[0], &value_size), Status::NOT_FOUND);
+  ASSERT_EQ(kvs.GetValueSize(nullptr, &value_size), Status::INVALID_ARGUMENT);
+  ASSERT_EQ(kvs.GetValueSize(keys[0], nullptr), Status::INVALID_ARGUMENT);
+
+  // Add key[0] and test we get the right value size for it.
+  memset(buffer.data(), kKey0Pattern, kSizeOfValueToFill);
+  ASSERT_OK(kvs.Put(keys[0], buffer.data(), kSizeOfValueToFill));
+  ASSERT_OK(kvs.GetValueSize(keys[0], &value_size));
+  ASSERT_EQ(value_size, kSizeOfValueToFill);
+
+  // Verify after erase key is not found
+  ASSERT_OK(kvs.Erase(keys[0]));
+  ASSERT_EQ(kvs.GetValueSize(keys[0], &value_size), Status::NOT_FOUND);
+}
+
+TEST_F(KeyValueStoreTest, CanFitEntryTests) {
+  // Reset KVS
+  kvs.Disable();
+  test_partition.Erase(0, test_partition.GetSectorCount());
+  ASSERT_OK(kvs.Enable());
+
+  // Get exactly the number of bytes that can fit in the space remaining for
+  // a large value, accounting for alignment.
+  constexpr uint16_t kTestKeySize = 2;
+  size_t space_remaining =
+      test_partition.GetSectorSizeBytes()                //
+      - RoundUpForAlignment(KeyValueStore::kHeaderSize)  // Sector Header
+      - RoundUpForAlignment(KeyValueStore::kHeaderSize)  // Cleaning Header
+      - RoundUpForAlignment(KeyValueStore::kHeaderSize)  // Chunk Header
+      - RoundUpForAlignment(kTestKeySize);
+  space_remaining -= test_partition.GetAlignmentBytes() / 2;
+  space_remaining = RoundUpForAlignment(space_remaining);
+
+  EXPECT_TRUE(kvs.CanFitEntry(kTestKeySize, space_remaining));
+  EXPECT_FALSE(kvs.CanFitEntry(kTestKeySize, space_remaining + 1));
+}
+
+TEST_F(KeyValueStoreTest, DifferentValueSameCrc16) {
+  test_partition.Erase(0, test_partition.GetSectorCount());
+  const char kKey[] = "k";
+  // With the key and our CRC16 algorithm these both have CRC of 0x82AE
+  // Given they are the same size and same key, the KVS will need to check
+  // the actual bits to know they are different.
+  const char kValue1[] = {'d', 'a', 't'};
+  const char kValue2[] = {'u', 'c', 'd'};
+
+  // Verify the CRC matches
+  ASSERT_EQ(CalcKvsCrc(kKey, kValue1, sizeof(kValue1)),
+            CalcKvsCrc(kKey, kValue2, sizeof(kValue2)));
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+  ASSERT_OK(kvs.Put(kKey, kValue1));
+
+  // Now try to rewrite with the similar value.
+  ASSERT_OK(kvs.Put(kKey, kValue2));
+
+  // Read it back and check it is correct
+  char value[3];
+  ASSERT_OK(kvs.Get(kKey, value, sizeof(value)));
+  ASSERT_EQ(memcmp(value, kValue2, sizeof(value)), 0);
+}
+
+TEST_F(KeyValueStoreTest, CallingEraseTwice) {
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+
+  const uint8_t kValue = 0xDA;
+  ASSERT_OK(kvs.Put(keys[0], &kValue, sizeof(kValue)));
+  ASSERT_OK(kvs.Erase(keys[0]));
+  uint16_t crc = CalcTestPartitionCrc();
+  EXPECT_EQ(kvs.Erase(keys[0]), Status::NOT_FOUND);
+  // Verify the flash has not changed
+  EXPECT_EQ(crc, CalcTestPartitionCrc());
+}
+
+void __attribute__((noinline)) StackHeavyPartialClean() {
+  CHECK_GE(test_partition.GetSectorCount(), 2);
+  FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+  FlashSubPartition test_partition_sector2(&test_partition, 1, 1);
+
+  KeyValueStore kvs1(&test_partition_sector1);
+  KeyValueStore kvs2(&test_partition_sector2);
+
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  ASSERT_OK(kvs1.Enable());
+  ASSERT_OK(kvs2.Enable());
+
+  int values1[3] = {100, 101, 102};
+  ASSERT_OK(kvs1.Put(keys[0], values1[0]));
+  ASSERT_OK(kvs1.Put(keys[1], values1[1]));
+  ASSERT_OK(kvs1.Put(keys[2], values1[2]));
+
+  int values2[3] = {200, 201, 202};
+  ASSERT_OK(kvs2.Put(keys[0], values2[0]));
+  ASSERT_OK(kvs2.Put(keys[1], values2[1]));
+  ASSERT_OK(kvs2.Erase(keys[1]));
+
+  kvs1.Disable();
+  kvs2.Disable();
+
+  // Key 0 is value1 in first sector, value 2 in second
+  // Key 1 is value1 in first sector, erased in second
+  // key 2 is only in first sector
+
+  uint64_t mark_clean_count = 5;
+  ASSERT_OK(PaddedWrite(&test_partition_sector1,
+                        RoundUpForAlignment(KeyValueStore::kHeaderSize),
+                        reinterpret_cast<uint8_t*>(&mark_clean_count),
+                        sizeof(uint64_t)));
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+  int value;
+  ASSERT_OK(kvs.Get(keys[0], &value));
+  ASSERT_EQ(values2[0], value);
+  ASSERT_EQ(kvs.Get(keys[1], &value), Status::NOT_FOUND);
+  ASSERT_OK(kvs.Get(keys[2], &value));
+  ASSERT_EQ(values1[2], value);
+
+  if (test_partition.GetSectorCount() == 2) {
+    EXPECT_EQ(kvs.PendingCleanCount(), 0u);
+    // Has forced a clean, mark again for next test
+    return;  // Not enough sectors to test 2 partial cleans.
+  } else {
+    EXPECT_EQ(kvs.PendingCleanCount(), 1u);
+  }
+
+  mark_clean_count--;
+  ASSERT_OK(PaddedWrite(&test_partition_sector2,
+                        RoundUpForAlignment(KeyValueStore::kHeaderSize),
+                        reinterpret_cast<uint8_t*>(&mark_clean_count),
+                        sizeof(uint64_t)));
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+  EXPECT_EQ(kvs.PendingCleanCount(), 2u);
+  ASSERT_OK(kvs.Get(keys[0], &value));
+  ASSERT_EQ(values1[0], value);
+  ASSERT_OK(kvs.Get(keys[1], &value));
+  ASSERT_EQ(values1[1], value);
+  ASSERT_OK(kvs.Get(keys[2], &value));
+  ASSERT_EQ(values1[2], value);
+}
+
+TEST_F(KeyValueStoreTest, PartialClean) {
+  if (CurrentTaskStackFree() < sizeof(KeyValueStore) * 2) {
+    LOG_ERROR("Not enough stack for test, skipping");
+    return;
+  }
+  StackHeavyPartialClean();
+}
+
+void __attribute__((noinline)) StackHeavyCleanAll() {
+  CHECK_GE(test_partition.GetSectorCount(), 2);
+  FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+
+  KeyValueStore kvs1(&test_partition_sector1);
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  ASSERT_OK(kvs1.Enable());
+
+  int values1[3] = {100, 101, 102};
+  ASSERT_OK(kvs1.Put(keys[0], values1[0]));
+  ASSERT_OK(kvs1.Put(keys[1], values1[1]));
+  ASSERT_OK(kvs1.Put(keys[2], values1[2] - 100));  //  force a rewrite
+  ASSERT_OK(kvs1.Put(keys[2], values1[2]));
+
+  kvs1.Disable();
+
+  uint64_t mark_clean_count = 5;
+  ASSERT_OK(PaddedWrite(&test_partition_sector1,
+                        RoundUpForAlignment(KeyValueStore::kHeaderSize),
+                        reinterpret_cast<uint8_t*>(&mark_clean_count),
+                        sizeof(uint64_t)));
+
+  // Reset KVS
+  kvs.Disable();
+  ASSERT_OK(kvs.Enable());
+  int value;
+  EXPECT_EQ(kvs.PendingCleanCount(), 1u);
+  ASSERT_OK(kvs.CleanAll());
+  EXPECT_EQ(kvs.PendingCleanCount(), 0u);
+  ASSERT_OK(kvs.Get(keys[0], &value));
+  ASSERT_EQ(values1[0], value);
+  ASSERT_OK(kvs.Get(keys[1], &value));
+  ASSERT_EQ(values1[1], value);
+  ASSERT_OK(kvs.Get(keys[2], &value));
+  ASSERT_EQ(values1[2], value);
+}
+
+TEST_F(KeyValueStoreTest, CleanAll) {
+  if (CurrentTaskStackFree() < sizeof(KeyValueStore) * 1) {
+    LOG_ERROR("Not enough stack for test, skipping");
+    return;
+  }
+  StackHeavyCleanAll();
+}
+
+void __attribute__((noinline)) StackHeavyPartialCleanLargeCounts() {
+  CHECK_GE(test_partition.GetSectorCount(), 2);
+  FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+  FlashSubPartition test_partition_sector2(&test_partition, 1, 1);
+
+  KeyValueStore kvs1(&test_partition_sector1);
+  KeyValueStore kvs2(&test_partition_sector2);
+
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  ASSERT_OK(kvs1.Enable());
+  ASSERT_OK(kvs2.Enable());
+
+  int values1[3] = {100, 101, 102};
+  ASSERT_OK(kvs1.Put(keys[0], values1[0]));
+  ASSERT_OK(kvs1.Put(keys[1], values1[1]));
+  ASSERT_OK(kvs1.Put(keys[2], values1[2]));
+
+  int values2[3] = {200, 201, 202};
+  ASSERT_OK(kvs2.Put(keys[0], values2[0]));
+  ASSERT_OK(kvs2.Put(keys[1], values2[1]));
+  ASSERT_OK(kvs2.Erase(keys[1]));
+
+  kvs1.Disable();
+  kvs2.Disable();
+  kvs.Disable();
+
+  // Key 0 is value1 in first sector, value 2 in second
+  // Key 1 is value1 in first sector, erased in second
+  // key 2 is only in first sector
+
+  uint64_t mark_clean_count = 4569877515;
+  ASSERT_OK(PaddedWrite(&test_partition_sector1,
+                        RoundUpForAlignment(KeyValueStore::kHeaderSize),
+                        reinterpret_cast<uint8_t*>(&mark_clean_count),
+                        sizeof(uint64_t)));
+
+  // Reset KVS
+  ASSERT_OK(kvs.Enable());
+  int value;
+  ASSERT_OK(kvs.Get(keys[0], &value));
+  ASSERT_EQ(values2[0], value);
+  ASSERT_EQ(kvs.Get(keys[1], &value), Status::NOT_FOUND);
+  ASSERT_OK(kvs.Get(keys[2], &value));
+  ASSERT_EQ(values1[2], value);
+
+  if (test_partition.GetSectorCount() == 2) {
+    EXPECT_EQ(kvs.PendingCleanCount(), 0u);
+    // Has forced a clean, mark again for next test
+    // Has forced a clean, mark again for next test
+    return;  // Not enough sectors to test 2 partial cleans.
+  } else {
+    EXPECT_EQ(kvs.PendingCleanCount(), 1u);
+  }
+  kvs.Disable();
+
+  mark_clean_count--;
+  ASSERT_OK(PaddedWrite(&test_partition_sector2,
+                        RoundUpForAlignment(KeyValueStore::kHeaderSize),
+                        reinterpret_cast<uint8_t*>(&mark_clean_count),
+                        sizeof(uint64_t)));
+  // Reset KVS
+  ASSERT_OK(kvs.Enable());
+  EXPECT_EQ(kvs.PendingCleanCount(), 2u);
+  ASSERT_OK(kvs.Get(keys[0], &value));
+  ASSERT_EQ(values1[0], value);
+  ASSERT_OK(kvs.Get(keys[1], &value));
+  ASSERT_EQ(values1[1], value);
+  ASSERT_OK(kvs.Get(keys[2], &value));
+  ASSERT_EQ(values1[2], value);
+}
+
+TEST_F(KeyValueStoreTest, PartialCleanLargeCounts) {
+  if (CurrentTaskStackFree() < sizeof(KeyValueStore) * 2) {
+    LOG_ERROR("Not enough stack for test, skipping");
+    return;
+  }
+  StackHeavyPartialCleanLargeCounts();
+}
+
+void __attribute__((noinline)) StackHeavyRecoverNoFreeSectors() {
+  CHECK_GE(test_partition.GetSectorCount(), 2);
+  FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+  FlashSubPartition test_partition_sector2(&test_partition, 1, 1);
+  FlashSubPartition test_partition_both(&test_partition, 0, 2);
+
+  KeyValueStore kvs1(&test_partition_sector1);
+  KeyValueStore kvs2(&test_partition_sector2);
+  KeyValueStore kvs_both(&test_partition_both);
+
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  ASSERT_OK(kvs1.Enable());
+  ASSERT_OK(kvs2.Enable());
+
+  int values[3] = {100, 101};
+  ASSERT_OK(kvs1.Put(keys[0], values[0]));
+  ASSERT_FALSE(kvs1.HasEmptySector());
+  ASSERT_OK(kvs2.Put(keys[1], values[1]));
+  ASSERT_FALSE(kvs2.HasEmptySector());
+
+  kvs1.Disable();
+  kvs2.Disable();
+
+  // Reset KVS
+  ASSERT_OK(kvs_both.Enable());
+  ASSERT_TRUE(kvs_both.HasEmptySector());
+  int value;
+  ASSERT_OK(kvs_both.Get(keys[0], &value));
+  ASSERT_EQ(values[0], value);
+  ASSERT_OK(kvs_both.Get(keys[1], &value));
+  ASSERT_EQ(values[1], value);
+}
+
+TEST_F(KeyValueStoreTest, RecoverNoFreeSectors) {
+  if (CurrentTaskStackFree() < sizeof(KeyValueStore) * 3) {
+    LOG_ERROR("Not enough stack for test, skipping");
+    return;
+  }
+  StackHeavyRecoverNoFreeSectors();
+}
+
+void __attribute__((noinline)) StackHeavyCleanOneSector() {
+  CHECK_GE(test_partition.GetSectorCount(), 2);
+  FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+  FlashSubPartition test_partition_sector2(&test_partition, 1, 1);
+
+  KeyValueStore kvs1(&test_partition_sector1);
+
+  test_partition.Erase(0, test_partition.GetSectorCount());
+
+  ASSERT_OK(kvs1.Enable());
+
+  int values[3] = {100, 101, 102};
+  ASSERT_OK(kvs1.Put(keys[0], values[0]));
+  ASSERT_OK(kvs1.Put(keys[1], values[1]));
+  ASSERT_OK(kvs1.Put(keys[2], values[2]));
+
+  kvs1.Disable();
+  kvs.Disable();
+
+  uint64_t mark_clean_count = 1;
+  ASSERT_OK(PaddedWrite(&test_partition_sector1,
+                        RoundUpForAlignment(KeyValueStore::kHeaderSize),
+                        reinterpret_cast<uint8_t*>(&mark_clean_count),
+                        sizeof(uint64_t)));
+
+  // Reset KVS
+  ASSERT_OK(kvs.Enable());
+
+  EXPECT_EQ(kvs.PendingCleanCount(), 1u);
+
+  bool all_sectors_have_been_cleaned = false;
+  ASSERT_OK(kvs.CleanOneSector(&all_sectors_have_been_cleaned));
+  EXPECT_EQ(all_sectors_have_been_cleaned, true);
+  EXPECT_EQ(kvs.PendingCleanCount(), 0u);
+  ASSERT_OK(kvs.CleanOneSector(&all_sectors_have_been_cleaned));
+  EXPECT_EQ(all_sectors_have_been_cleaned, true);
+  int value;
+  ASSERT_OK(kvs.Get(keys[0], &value));
+  ASSERT_EQ(values[0], value);
+  ASSERT_OK(kvs.Get(keys[1], &value));
+  ASSERT_EQ(values[1], value);
+  ASSERT_OK(kvs.Get(keys[2], &value));
+  ASSERT_EQ(values[2], value);
+}
+
+TEST_F(KeyValueStoreTest, CleanOneSector) {
+  if (CurrentTaskStackFree() < sizeof(KeyValueStore)) {
+    LOG_ERROR("Not enough stack for test, skipping");
+    return;
+  }
+  StackHeavyCleanOneSector();
+}
+
+#if USE_MEMORY_BUFFER
+
+TEST_F(KeyValueStoreTest, LargePartition) {
+  if (CurrentTaskStackFree() < sizeof(KeyValueStore)) {
+    LOG_ERROR("Not enough stack for test, skipping");
+    return;
+  }
+  large_test_partition.Erase(0, large_test_partition.GetSectorCount());
+  KeyValueStore large_kvs(&large_test_partition);
+  // Reset KVS
+  large_kvs.Disable();
+  ASSERT_OK(large_kvs.Enable());
+
+  const uint8_t kValue1 = 0xDA;
+  const uint8_t kValue2 = 0x12;
+  uint8_t value[1];
+  ASSERT_OK(large_kvs.Put(keys[0], &kValue1, sizeof(kValue1)));
+  EXPECT_EQ(large_kvs.KeyCount(), 1);
+  ASSERT_OK(large_kvs.Erase(keys[0]));
+  EXPECT_EQ(large_kvs.Get(keys[0], value, sizeof(value)), Status::NOT_FOUND);
+  ASSERT_OK(large_kvs.Put(keys[1], &kValue1, sizeof(kValue1)));
+  ASSERT_OK(large_kvs.Put(keys[2], &kValue2, sizeof(kValue2)));
+  ASSERT_OK(large_kvs.Erase(keys[1]));
+  EXPECT_OK(large_kvs.Get(keys[2], value, sizeof(value)));
+  EXPECT_EQ(kValue2, value[0]);
+  ASSERT_EQ(large_kvs.Get(keys[1], &value), Status::NOT_FOUND);
+  EXPECT_EQ(large_kvs.KeyCount(), 1);
+}
+#endif  // USE_MEMORY_BUFFER
+
+}  // namespace pw
diff --git a/pw_kvs/public/pw_kvs/flash.h b/pw_kvs/public/pw_kvs/flash.h
new file mode 100644
index 0000000..5cb80cd
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/flash.h
@@ -0,0 +1,33 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include "pw_kvs/devices/flash_memory.h"
+
+namespace pw {
+
+// Writes a buffer which is not guaranteed to be aligned, pads remaining
+// bytes with 0.
+Status PaddedWrite(FlashPartition* partition,
+                   FlashPartition::Address address,
+                   const uint8_t* buffer,
+                   uint16_t size);
+
+// Read into a buffer when size is not guaranteed to be aligned.
+Status UnalignedRead(FlashPartition* partition,
+                     uint8_t* buffer,
+                     FlashPartition::Address address,
+                     uint16_t size);
+
+}  // namespace pw
diff --git a/pw_kvs/public/pw_kvs/flash_memory.h b/pw_kvs/public/pw_kvs/flash_memory.h
new file mode 100644
index 0000000..707f128
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/flash_memory.h
@@ -0,0 +1,377 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <algorithm>
+
+#include "pw_kvs/assert.h"
+#include "pw_kvs/logging.h"
+#include "pw_kvs/peripherals/partition_table_entry.h"
+#include "pw_kvs/status.h"
+#include "pw_kvs/status_macros.h"
+
+namespace pw {
+class FlashMemory {
+ public:
+  // The flash address is in the range of: 0 to FlashSize.
+  typedef uint32_t Address;
+  constexpr FlashMemory(uint32_t sector_size,
+                        uint32_t sector_count,
+                        uint8_t alignment,
+                        uint32_t start_address = 0,
+                        uint32_t sector_start = 0,
+                        uint8_t erased_memory_content = 0xFF)
+      : sector_size_(sector_size),
+        flash_sector_count_(sector_count),
+        alignment_(alignment),
+        start_address_(start_address),
+        sector_start_(sector_start),
+        erased_memory_content_(erased_memory_content) {}
+
+  virtual Status Enable() = 0;
+  virtual Status Disable() = 0;
+  virtual bool IsEnabled() const = 0;
+  virtual Status SelfTest() { return Status::UNIMPLEMENTED; }
+
+  // Erase num_sectors starting at a given address. Blocking call.
+  // Address should be on a sector boundary.
+  // Returns: OK, on success.
+  //          TIMEOUT, on timeout.
+  //          INVALID_ARGUMENT, if address or sector count is invalid.
+  //          UNKNOWN, on HAL error
+  virtual Status Erase(Address flash_address, uint32_t num_sectors) = 0;
+
+  // Reads bytes from flash into buffer. Blocking call.
+  // Returns: OK, on success.
+  //          TIMEOUT, on timeout.
+  //          INVALID_ARGUMENT, if address or length is invalid.
+  //          UNKNOWN, on HAL error
+  virtual Status Read(uint8_t* destination_ram_address,
+                      Address source_flash_address,
+                      uint32_t len) = 0;
+
+  // Writes bytes to flash. Blocking call.
+  // Returns: OK, on success.
+  //          TIMEOUT, on timeout.
+  //          INVALID_ARGUMENT, if address or length is invalid.
+  //          UNKNOWN, on HAL error
+  virtual Status Write(Address destination_flash_address,
+                       const uint8_t* source_ram_address,
+                       uint32_t len) = 0;
+
+  // Convert an Address to an MCU pointer, this can be used for memory
+  // mapped reads. Return NULL if the memory is not memory mapped.
+  virtual uint8_t* FlashAddressToMcuAddress(Address address) const {
+    return nullptr;
+  }
+
+  // GetStartSector() is useful for FlashMemory instances where the
+  // sector start is not 0. (ex.: cases where there are portions of flash
+  // that should be handled independently).
+  constexpr uint32_t GetStartSector() const { return sector_start_; }
+  constexpr uint32_t GetSectorSizeBytes() const { return sector_size_; }
+  constexpr uint32_t GetSectorCount() const { return flash_sector_count_; }
+  constexpr uint8_t GetAlignmentBytes() const { return alignment_; }
+  constexpr uint32_t GetSizeBytes() const {
+    return sector_size_ * flash_sector_count_;
+  }
+  // Address of the start of flash (the address of sector 0)
+  constexpr uint32_t GetStartAddress() const { return start_address_; }
+  constexpr uint8_t GetErasedMemoryContent() const {
+    return erased_memory_content_;
+  }
+
+ private:
+  const uint32_t sector_size_;
+  const uint32_t flash_sector_count_;
+  const uint8_t alignment_;
+  const uint32_t start_address_;
+  const uint32_t sector_start_;
+  const uint8_t erased_memory_content_;
+};
+
+// Exposes a sub-sector sized region of flash memory that cannot be erased.
+// It can be thought of as one pseudo-sector that is sized exactly as provided.
+//
+// TODO(b/117553777): This makes a little more sense as a SubSectorPartition,
+// but PartitionTableEntry currently assumes all partitions fill entire sectors.
+// Revisit when PartitionTable is refactored.
+class FlashMemorySubSector : public FlashMemory {
+ public:
+  constexpr FlashMemorySubSector(FlashMemory* flash,
+                                 uint32_t start_address,
+                                 uint32_t size)
+      : FlashMemory(size,
+                    1,  // Round up to "1" sector.
+                    flash->GetAlignmentBytes(),
+                    start_address,
+                    // Calculate the sector for this start address.
+                    flash->GetStartSector() +
+                        ((start_address - flash->GetStartAddress()) /
+                         flash->GetSectorSizeBytes())),
+        flash_(*CHECK_NOTNULL(flash)),
+        base_offset_(start_address - flash->GetStartAddress()) {
+    // Make sure we're not specifying a region of flash larger than
+    // that which the underlying FlashMemory supports.
+    CHECK(start_address >= flash->GetStartAddress());
+    CHECK(size <= flash->GetSectorSizeBytes());
+    CHECK(start_address + size <=
+          flash->GetStartAddress() + flash->GetSizeBytes());
+    CHECK_EQ(0, start_address % flash->GetAlignmentBytes());
+    CHECK_EQ(0, size % flash->GetAlignmentBytes());
+  }
+
+  Status Enable() override { return flash_.Enable(); }
+  Status Disable() override { return flash_.Disable(); }
+  bool IsEnabled() const override { return flash_.IsEnabled(); }
+  Status SelfTest() override { return flash_.SelfTest(); }
+
+  Status Erase(Address flash_address, uint32_t num_sectors) override {
+    return Status::UNIMPLEMENTED;
+  }
+
+  Status Read(uint8_t* destination_ram_address,
+              Address source_flash_address,
+              uint32_t len) override {
+    return flash_.Read(destination_ram_address, source_flash_address, len);
+  }
+
+  Status Write(Address destination_flash_address,
+               const uint8_t* source_ram_address,
+               uint32_t len) override {
+    return flash_.Write(destination_flash_address, source_ram_address, len);
+  }
+
+  uint8_t* FlashAddressToMcuAddress(Address address) const override {
+    return flash_.FlashAddressToMcuAddress(base_offset_ + address);
+  }
+
+ private:
+  FlashMemory& flash_;
+  // Value to add to addresses to get to the underlying flash_ address.
+  const Address base_offset_;
+};
+
+class FlashPartition {
+ public:
+  // The flash address is in the range of: 0 to PartitionSize.
+  typedef uint32_t Address;
+
+  constexpr FlashPartition(
+      FlashMemory* flash,
+      uint32_t start_sector_index,
+      uint32_t sector_count,
+      PartitionPermission permission = PartitionPermission::kReadAndWrite)
+      : flash_(*flash),
+        start_sector_index_(start_sector_index),
+        sector_count_(sector_count),
+        permission_(permission) {}
+
+  constexpr FlashPartition(FlashMemory* flash, PartitionTableEntry entry)
+      : flash_(*flash),
+        start_sector_index_(entry.partition_start_sector_index),
+        sector_count_(entry.partition_end_sector_index -
+                      entry.partition_start_sector_index + 1),
+        permission_(entry.partition_permission) {}
+
+  // Erase num_sectors starting at a given address. Blocking call.
+  // Address should be on a sector boundary.
+  // Returns: OK, on success.
+  //          TIMEOUT, on timeout.
+  //          INVALID_ARGUMENT, if address or sector count is invalid.
+  //          PERMISSION_DENIED, if partition is read only.
+  //          UNKNOWN, on HAL error
+  virtual Status Erase(Address address, uint32_t num_sectors) {
+    RETURN_STATUS_IF(permission_ == PartitionPermission::kReadOnly,
+                     Status::PERMISSION_DENIED);
+    RETURN_IF_ERROR(CheckBounds(address, num_sectors * GetSectorSizeBytes()));
+    return flash_.Erase(PartitionToFlashAddress(address), num_sectors);
+  }
+
+  // Reads bytes from flash into buffer. Blocking call.
+  // Returns: OK, on success.
+  //          TIMEOUT, on timeout.
+  //          INVALID_ARGUMENT, if address or length is invalid.
+  //          UNKNOWN, on HAL error
+  virtual Status Read(uint8_t* destination_ram_address,
+                      Address source_flash_address,
+                      uint32_t len) {
+    RETURN_IF_ERROR(CheckBounds(source_flash_address, len));
+    return flash_.Read(destination_ram_address,
+                       PartitionToFlashAddress(source_flash_address),
+                       len);
+  }
+
+  // Writes bytes to flash. Blocking call.
+  // Returns: OK, on success.
+  //          TIMEOUT, on timeout.
+  //          INVALID_ARGUMENT, if address or length is invalid.
+  //          PERMISSION_DENIED, if partition is read only.
+  //          UNKNOWN, on HAL error
+  virtual Status Write(Address destination_flash_address,
+                       const uint8_t* source_ram_address,
+                       uint32_t len) {
+    RETURN_STATUS_IF(permission_ == PartitionPermission::kReadOnly,
+                     Status::PERMISSION_DENIED);
+    RETURN_IF_ERROR(CheckBounds(destination_flash_address, len));
+    return flash_.Write(PartitionToFlashAddress(destination_flash_address),
+                        source_ram_address,
+                        len);
+  }
+
+  // Check to see if chunk of flash memory is erased. Address and len need to
+  // be aligned with FlashMemory.
+  // Returns: OK, on success.
+  //          TIMEOUT, on timeout.
+  //          INVALID_ARGUMENT, if address or length is invalid.
+  //          UNKNOWN, on HAL error
+  virtual Status IsChunkErased(Address source_flash_address,
+                               uint32_t len,
+                               bool* is_erased) {
+    // Max alignment is artifical to keep the stack usage low for this
+    // function. Using 16 because it's the alignment of encrypted flash.
+    const uint8_t kMaxAlignment = 16;
+    // Relying on Read() to check address and len arguments.
+    RETURN_STATUS_IF(!is_erased, Status::INVALID_ARGUMENT);
+    uint8_t alignment = GetAlignmentBytes();
+    RETURN_STATUS_IF(alignment > kMaxAlignment, Status::INVALID_ARGUMENT);
+    RETURN_STATUS_IF(kMaxAlignment % alignment, Status::INVALID_ARGUMENT);
+    RETURN_STATUS_IF(len % alignment, Status::INVALID_ARGUMENT);
+
+    uint8_t buffer[kMaxAlignment];
+    uint8_t erased_pattern_buffer[kMaxAlignment];
+    size_t offset = 0;
+    memset(erased_pattern_buffer,
+           flash_.GetErasedMemoryContent(),
+           sizeof(erased_pattern_buffer));
+    *is_erased = false;
+    while (len > 0) {
+      // Check earlier that len is aligned, no need to round up
+      uint16_t read_size = std::min(static_cast<uint32_t>(sizeof(buffer)), len);
+      RETURN_IF_ERROR(Read(buffer, source_flash_address + offset, read_size));
+      if (memcmp(buffer, erased_pattern_buffer, read_size)) {
+        // Detected memory chunk is not entirely erased
+        return Status::OK;
+      }
+      offset += read_size;
+      len -= read_size;
+    }
+    *is_erased = true;
+    return Status::OK;
+  }
+
+  constexpr uint32_t GetSectorSizeBytes() const {
+    return flash_.GetSectorSizeBytes();
+  }
+
+  uint32_t GetSizeBytes() const {
+    return GetSectorCount() * GetSectorSizeBytes();
+  }
+
+  virtual uint8_t GetAlignmentBytes() const {
+    return flash_.GetAlignmentBytes();
+  }
+
+  virtual uint32_t GetSectorCount() const { return sector_count_; }
+
+  // Convert a FlashMemory::Address to an MCU pointer, this can be used for
+  // memory mapped reads. Return NULL if the memory is not memory mapped.
+  uint8_t* PartitionAddressToMcuAddress(Address address) const {
+    return flash_.FlashAddressToMcuAddress(PartitionToFlashAddress(address));
+  }
+
+  FlashMemory::Address PartitionToFlashAddress(Address address) const {
+    return flash_.GetStartAddress() +
+           (start_sector_index_ - flash_.GetStartSector()) *
+               GetSectorSizeBytes() +
+           address;
+  }
+
+ protected:
+  Status CheckBounds(Address address, uint32_t len) const {
+    if (address + len > GetSizeBytes()) {
+      LOG(ERROR) << "Attempted out-of-bound flash memory access (address:"
+                 << address << " length:" << len << ")";
+      return Status::INVALID_ARGUMENT;
+    }
+    return Status::OK;
+  }
+
+ private:
+  FlashMemory& flash_;
+  const uint32_t start_sector_index_;
+  const uint32_t sector_count_;
+  const PartitionPermission permission_;
+};
+
+// FlashSubPartition defines a new partition which maps itself as a smaller
+// piece of another partition. This can used when a partition has special
+// behaviours (for example encrypted flash).
+// For example, this will be the first sector of test_partition:
+//    FlashSubPartition test_partition_sector1(&test_partition, 0, 1);
+class FlashSubPartition : public FlashPartition {
+ public:
+  constexpr FlashSubPartition(FlashPartition* parent_partition,
+                              uint32_t start_sector_index,
+                              uint32_t sector_count)
+      : FlashPartition(*parent_partition),
+        partition_(parent_partition),
+        start_sector_index_(start_sector_index),
+        sector_count_(sector_count) {}
+
+  Status Erase(Address address, uint32_t num_sectors) override {
+    RETURN_IF_ERROR(CheckBounds(address, num_sectors * GetSectorSizeBytes()));
+    return partition_->Erase(ParentAddress(address), num_sectors);
+  }
+
+  Status Read(uint8_t* destination_ram_address,
+              Address source_flash_address,
+              uint32_t len) override {
+    RETURN_IF_ERROR(CheckBounds(source_flash_address, len));
+    return partition_->Read(
+        destination_ram_address, ParentAddress(source_flash_address), len);
+  }
+
+  Status Write(Address destination_flash_address,
+               const uint8_t* source_ram_address,
+               uint32_t len) override {
+    RETURN_IF_ERROR(CheckBounds(destination_flash_address, len));
+    return partition_->Write(
+        ParentAddress(destination_flash_address), source_ram_address, len);
+  }
+
+  Status IsChunkErased(Address source_flash_address,
+                       uint32_t len,
+                       bool* is_erased) override {
+    RETURN_IF_ERROR(CheckBounds(source_flash_address, len));
+    return partition_->IsChunkErased(
+        ParentAddress(source_flash_address), len, is_erased);
+  }
+
+  uint8_t GetAlignmentBytes() const override {
+    return partition_->GetAlignmentBytes();
+  }
+
+  uint32_t GetSectorCount() const override { return sector_count_; }
+
+ private:
+  Address ParentAddress(Address address) const {
+    return address + start_sector_index_ * partition_->GetSectorSizeBytes();
+  }
+  FlashPartition* partition_;
+  const uint32_t start_sector_index_;
+  const uint32_t sector_count_;
+};
+
+}  // namespace pw
diff --git a/pw_kvs/public/pw_kvs/in_memory_fake_flash.h b/pw_kvs/public/pw_kvs/in_memory_fake_flash.h
new file mode 100644
index 0000000..a67c770
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/in_memory_fake_flash.h
@@ -0,0 +1,92 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <array>
+
+#include "pw_kvs/devices/flash_memory.h"
+#include "pw_kvs/status.h"
+#include "pw_kvs/status_macros.h"
+
+namespace pw {
+
+// This creates a buffer which mimics the behaviour of flash (requires erase,
+// before write, checks alignments, and is addressed in sectors).
+template <uint32_t kSectorSize, uint16_t kSectorCount>
+class InMemoryFakeFlash : public FlashMemory {
+ public:
+  InMemoryFakeFlash(uint8_t alignment = 1)  // default 8 bit alignment
+      : FlashMemory(kSectorSize, kSectorCount, alignment) {}
+
+  // Always enabled
+  Status Enable() override { return Status::OK; }
+  Status Disable() override { return Status::OK; }
+  bool IsEnabled() const override { return true; }
+
+  // Erase num_sectors starting at a given address. Blocking call.
+  // Address should be on a sector boundary.
+  // Returns: OK, on success.
+  //          INVALID_ARGUMENT, if address or sector count is invalid.
+  //          UNKNOWN, on HAL error
+  Status Erase(Address addr, uint32_t num_sectors) override {
+    RETURN_STATUS_IF(addr % GetSectorSizeBytes() != 0,
+                     Status::INVALID_ARGUMENT);
+    RETURN_STATUS_IF(
+        addr / GetSectorSizeBytes() + num_sectors > GetSectorCount(),
+        Status::UNKNOWN);
+    RETURN_STATUS_IF(addr % GetAlignmentBytes() != 0, Status::INVALID_ARGUMENT);
+    memset(&buffer_[addr], 0xFF, GetSectorSizeBytes() * num_sectors);
+    return Status::OK;
+  }
+
+  // Reads bytes from flash into buffer. Blocking call.
+  // Returns: OK, on success.
+  //          INVALID_ARGUMENT, if address or length is invalid.
+  //          UNKNOWN, on HAL error
+  Status Read(uint8_t* dest_ram_addr,
+              Address source_flash_addr,
+              uint32_t len) override {
+    RETURN_STATUS_IF(
+        (source_flash_addr + len) >= GetSectorCount() * GetSizeBytes(),
+        Status::INVALID_ARGUMENT);
+    memcpy(dest_ram_addr, &buffer_[source_flash_addr], len);
+    return Status::OK;
+  }
+
+  // Writes bytes to flash. Blocking call.
+  // Returns: OK, on success.
+  //          INVALID_ARGUMENT, if address or length is invalid.
+  //          UNKNOWN, on HAL error
+  Status Write(Address dest_flash_addr,
+               const uint8_t* source_ram_addr,
+               uint32_t len) override {
+    RETURN_STATUS_IF(
+        (dest_flash_addr + len) >= GetSectorCount() * GetSizeBytes(),
+        Status::INVALID_ARGUMENT);
+    RETURN_STATUS_IF(dest_flash_addr % GetAlignmentBytes() != 0,
+                     Status::INVALID_ARGUMENT);
+    RETURN_STATUS_IF(len % GetAlignmentBytes() != 0, Status::INVALID_ARGUMENT);
+    // Check in erased state
+    for (unsigned i = 0; i < len; i++) {
+      RETURN_STATUS_IF(buffer_[dest_flash_addr + i] != 0xFF, Status::UNKNOWN);
+    }
+    memcpy(&buffer_[dest_flash_addr], source_ram_addr, len);
+    return Status::OK;
+  }
+
+ private:
+  std::array<uint8_t, kSectorCount * kSectorSize> buffer_;
+};
+
+}  // namespace pw
diff --git a/pw_kvs/public/pw_kvs/key_value_store.h b/pw_kvs/public/pw_kvs/key_value_store.h
new file mode 100644
index 0000000..dca6507
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/key_value_store.h
@@ -0,0 +1,366 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <algorithm>
+#include <type_traits>
+
+#include "pw_kvs/devices/flash_memory.h"
+#include "pw_kvs/logging.h"
+#include "pw_kvs/os/mutex.h"
+#include "pw_kvs/status.h"
+
+namespace pw {
+
+// This object is very large and should not be placed on the stack.
+class KeyValueStore {
+ public:
+  KeyValueStore(FlashPartition* partition) : partition_(*partition) {}
+
+  // Enable the KVS, scans the sectors of the partition for any current KVS
+  // data. Erases and initializes any sectors which are not initialized.
+  // Checks the CRC of all data in the KVS; on failure the corrupted data is
+  // lost and Enable will return Status::DATA_LOSS, but the KVS will still load
+  // other data and will be enabled.
+  Status Enable();
+  bool IsEnabled() const { return enabled_; }
+  void Disable() {
+    if (enabled_ == false) {
+      return;
+    }
+    os::MutexLock lock(&lock_);
+    enabled_ = false;
+  }
+
+  // Erase a key value element.
+  // key is a null terminated c string.
+  // Returns OK if erased
+  //         NOT_FOUND if key was not found.
+  Status Erase(const char* key);
+
+  // Copy the first size bytes of key's value into value buffer.
+  // key is a null terminated c string. Size is the amount of bytes to read,
+  // Optional offset argument supports reading size bytes starting at the
+  // specified offset.
+  // Returns OK if successful
+  //         NOT_FOUND if key was not found
+  //         DATA_LOSS if the value failed crc check
+  Status Get(const char* key, void* value, uint16_t size, uint16_t offset = 0);
+
+  // Interpret the key's value as the given type and return it.
+  // Returns OK if successful
+  //         NOT_FOUND if key was not found
+  //         DATA_LOSS if the value failed crc check
+  //         INVALID_ARGUMENT if size of value != sizeof(T)
+  template <typename T>
+  Status Get(const char* key, T* value) {
+    static_assert(std::is_trivially_copyable<T>(), "KVS values must copyable");
+    static_assert(!std::is_pointer<T>(), "KVS values cannot be pointers");
+    uint16_t value_size = 0;
+    RETURN_IF_ERROR(GetValueSize(key, &value_size));
+    RETURN_STATUS_IF(value_size != sizeof(T), Status::INVALID_ARGUMENT);
+    return Get(key, value, sizeof(T));
+  }
+
+  // Writes the key value store to the partition. If the key already exists it
+  // will be deleted before writing the new value.
+  // key is a null terminated c string.
+  // Returns OK if successful
+  //         RESOURCE_EXHAUSTED if there is not enough available space
+  Status Put(const char* key, const void* value, uint16_t size);
+
+  // Writes the value to the partition. If the key already exists it will be
+  // deleted before writing the new value.
+  // key is a null terminated c string.
+  // Returns OK if successful
+  //         RESOURCE_EXHAUSTED if there is not enough available space
+  template <typename T>
+  Status Put(const char* key, const T& value) {
+    static_assert(std::is_trivially_copyable<T>(), "KVS values must copyable");
+    static_assert(!std::is_pointer<T>(), "KVS values cannot be pointers");
+    return Put(key, &value, sizeof(T));
+  }
+
+  // Gets the size of the value stored for provided key.
+  // Returns OK if successful
+  //         INVALID_ARGUMENT if args are invalid.
+  //         FAILED_PRECONDITION if KVS is not enabled.
+  //         NOT_FOUND if key was not found.
+  Status GetValueSize(const char* key, uint16_t* value_size);
+
+  // Tests if the proposed key value entry can be stored in the KVS.
+  bool CanFitEntry(uint16_t key_len, uint16_t value_len) {
+    return kSectorInvalid != FindSpace(ChunkSize(key_len, value_len));
+  }
+
+  // CleanAll cleans each sector which is currently marked for cleaning.
+  // Note: if any data is invalid/corrupt it could be lost.
+  Status CleanAll() {
+    os::MutexLock lock(&lock_);
+    return CleanAllInternal();
+  }
+  size_t PendingCleanCount() {
+    os::MutexLock lock(&lock_);
+    size_t ret = 0;
+    for (size_t i = 0; i < SectorCount(); i++) {
+      ret += sector_space_remaining_[i] == 0 ? 1 : 0;
+    }
+    return ret;
+  }
+
+  // Clean a single sector and return, if all sectors are clean it will set
+  // all_sectors_have_been_cleaned to true and return.
+  Status CleanOneSector(bool* all_sectors_have_been_cleaned);
+
+  // For debugging, logging, and testing. (Don't use in regular code)
+  // Note: a key_index is not valid after an element is erased or updated.
+  uint8_t KeyCount() const;
+  const char* GetKey(uint8_t idx) const;
+  uint16_t GetValueSize(uint8_t idx) const;
+  size_t GetMaxKeys() const { return kListCapacityMax; }
+  bool HasEmptySector() const { return HaveEmptySectorImpl(); }
+
+  static constexpr size_t kHeaderSize = 8;  // Sector and KVS Header size
+  static constexpr uint16_t MaxValueLength() { return kChunkValueLengthMax; }
+  KeyValueStore(const KeyValueStore&) = delete;
+  KeyValueStore& operator=(const KeyValueStore&) = delete;
+
+ private:
+  using KeyIndex = uint8_t;
+  using SectorIndex = uint32_t;
+
+  static constexpr uint16_t kVersion = 4;
+  static constexpr KeyIndex kListCapacityMax = cfg::kKvsMaxKeyCount;
+  static constexpr SectorIndex kSectorCountMax = cfg::kKvsMaxSectorCount;
+
+  // Number of bits for fields in KVSHeader:
+  static constexpr int kChunkHeaderKeyFieldNumBits = 4;
+  static constexpr int kChunkHeaderChunkFieldNumBits = 12;
+
+  static constexpr uint16_t kSectorReadyValue = 0xABCD;
+  static constexpr uint16_t kChunkSyncValue = 0x55AA;
+
+  static constexpr uint16_t kChunkLengthMax =
+      ((1 << kChunkHeaderChunkFieldNumBits) - 1);
+  static constexpr uint16_t kChunkKeyLengthMax =
+      ((1 << kChunkHeaderKeyFieldNumBits) - 1);
+  static constexpr uint16_t kChunkValueLengthMax =
+      (kChunkLengthMax - kChunkKeyLengthMax);
+
+  static constexpr SectorIndex kSectorInvalid = 0xFFFFFFFFul;
+  static constexpr FlashPartition::Address kAddressInvalid = 0xFFFFFFFFul;
+  static constexpr uint64_t kSectorCleanNotPending = 0xFFFFFFFFFFFFFFFFull;
+
+  // TODO: Use BitPacker if/when have more flags.
+  static constexpr uint16_t kFlagsIsErasedMask = 0x0001;
+  static constexpr uint16_t kMaxAlignmentBytes = 128;
+
+  // This packs into 16 bytes.
+  struct KvsSectorHeaderMeta {
+    uint16_t synchronize_token;
+    uint16_t version;
+    uint16_t alignment_bytes;  // alignment used for each chunk in this sector.
+    uint16_t padding;          // padding to support uint64_t alignment.
+  } __attribute__((__packed__));
+  static_assert(sizeof(KvsSectorHeaderMeta) == kHeaderSize,
+                "Invalid KvsSectorHeaderMeta size");
+
+  // sector_clean_pending is broken off from KvsSectorHeaderMeta to support
+  // larger than sizeof(KvsSectorHeaderMeta) flash write alignments.
+  struct KvsSectorHeaderCleaning {
+    // When this sector is not marked for cleaning this will be in the erased
+    // state. When marked for clean this value indicates the order the sectors
+    // were marked for cleaning, and therfore which data is the newest.
+    // To remain backwards compatible with v2 and v3 KVS this is 8 bytes, if the
+    // alignment is greater than 8 we will check the entire alignment is in the
+    // default erased state.
+    uint64_t sector_clean_order;
+  };
+  static_assert(sizeof(KvsSectorHeaderCleaning) == kHeaderSize,
+                "Invalid KvsSectorHeaderCleaning size");
+
+  // This packs into 8 bytes, to support uint64_t alignment.
+  struct KvsHeader {
+    uint16_t synchronize_token;
+    uint16_t crc;  // Crc of the key + data (Ignoring any padding bytes)
+    // flags is a Bitmask: bits 15-1 reserved = 0,
+    // bit 0: is_erased [0 when not erased, 1 when erased]
+    uint16_t flags;
+    // On little endian the length fields map to memory as follows:
+    // byte array: [ 0x(c0to3 k0to3) 0x(c8to11 c4to7) ]
+    // Key length does not include trailing zero. That is not stored.
+    uint16_t key_len : kChunkHeaderKeyFieldNumBits;
+    // Chunk length is the total length of the chunk before alignment.
+    // That way we can work out the length of the value as:
+    // (chunk length - key length - size of chunk header).
+    uint16_t chunk_len : kChunkHeaderChunkFieldNumBits;
+  } __attribute__((__packed__));
+  static_assert(sizeof(KvsHeader) == kHeaderSize, "Invalid KvsHeader size");
+
+  struct KeyMap {
+    char key[kChunkKeyLengthMax + 1];  // + 1 for null terminator
+    FlashPartition::Address address;
+    uint16_t chunk_len;
+    bool is_erased;
+  };
+
+  // NOTE: All public APIs handle the locking, the internal methods assume the
+  // lock has already been acquired.
+
+  SectorIndex AddressToSectorIndex(FlashPartition::Address address) const {
+    return address / partition_.GetSectorSizeBytes();
+  }
+
+  FlashPartition::Address SectorIndexToAddress(SectorIndex index) const {
+    return index * partition_.GetSectorSizeBytes();
+  }
+
+  // Returns kAddressInvalid if no space is found, otherwise the address.
+  FlashPartition::Address FindSpace(uint16_t requested_size) const;
+
+  // Attempts to rewrite a key's value by appending the new value to the same
+  // sector. If the sector is full the value is written to another sector, and
+  // the sector is marked for cleaning.
+  // Returns RESOURCE_EXHAUSTED if no space is available, OK otherwise.
+  Status RewriteValue(KeyIndex key_index,
+                      const uint8_t* value,
+                      uint16_t size,
+                      bool is_erased = false);
+
+  bool ValueMatches(KeyIndex key_index,
+                    const uint8_t* value,
+                    uint16_t size,
+                    bool is_erased);
+
+  // ResetSector erases the sector and writes the sector header.
+  Status ResetSector(SectorIndex sector_index);
+  Status WriteKeyValue(FlashPartition::Address address,
+                       const char* key,
+                       const uint8_t* value,
+                       uint16_t size,
+                       bool is_erased = false);
+  uint32_t SectorSpaceRemaining(SectorIndex sector_index) const;
+
+  // Returns idx if key is found, otherwise kListCapacityMax.
+  KeyIndex FindKeyInMap(const char* key) const;
+  bool IsKeyInMap(const char* key) const {
+    return FindKeyInMap(key) != kListCapacityMax;
+  }
+
+  void RemoveFromMap(KeyIndex key_index);
+  Status AppendToMap(const char* key,
+                     FlashPartition::Address address,
+                     uint16_t chunk_len,
+                     bool is_erased = false);
+  void UpdateMap(KeyIndex key_index,
+                 FlashPartition::Address address,
+                 uint16_t chunk_len,
+                 bool is_erased = false);
+  uint16_t CalculateCrc(const char* key,
+                        uint16_t key_size,
+                        const uint8_t* value,
+                        uint16_t value_size) const;
+
+  // Calculates the CRC by reading the value from flash in chunks.
+  Status CalculateCrcFromValueAddress(const char* key,
+                                      uint16_t key_size,
+                                      FlashPartition::Address value_address,
+                                      uint16_t value_size,
+                                      uint16_t* crc_ret);
+
+  uint16_t RoundUpForAlignment(uint16_t size) const {
+    if (size % alignment_bytes_ != 0) {
+      return size + alignment_bytes_ - size % alignment_bytes_;
+    }
+    return size;
+  }
+
+  // The buffer is chosen to be no larger then the config value, and
+  // be a multiple of the flash alignment.
+  size_t TempBufferAlignedSize() const {
+    CHECK_GE(sizeof(temp_buffer_), partition_.GetAlignmentBytes());
+    return sizeof(temp_buffer_) -
+           (sizeof(temp_buffer_) % partition_.GetAlignmentBytes());
+  }
+
+  // Moves a key/value chunk from one address to another, all fields expected to
+  // be aligned.
+  Status MoveChunk(FlashPartition::Address dest_address,
+                   FlashPartition::Address src_address,
+                   uint16_t size);
+
+  // Size of a chunk including header, key, value, and alignment padding.
+  uint16_t ChunkSize(uint16_t key_len, uint16_t chunk_len) const {
+    return RoundUpForAlignment(sizeof(KvsHeader)) +
+           RoundUpForAlignment(key_len) + RoundUpForAlignment(chunk_len);
+  }
+
+  // Sectors should be cleaned when full, every valid (Most recent, not erased)
+  // chunk of data is moved to another sector and the sector is erased.
+  // TODO: Clean sectors with lots of invalid data, without a rewrite
+  // or erase triggering it.
+  Status MarkSectorForClean(SectorIndex sector);
+  Status CleanSector(SectorIndex sector);
+  Status CleanAllInternal();
+  Status GarbageCollectImpl(bool clean_pending_sectors,
+                            bool exit_when_have_free_sector);
+  Status FullGarbageCollect();
+  Status EnforceFreeSector();
+
+  SectorIndex SectorCount() const {
+    return std::min(partition_.GetSectorCount(), kSectorCountMax);
+  }
+
+  size_t SectorSpaceAvailableWhenEmpty() const {
+    return partition_.GetSectorSizeBytes() -
+           RoundUpForAlignment(sizeof(KvsSectorHeaderMeta)) -
+           RoundUpForAlignment(sizeof(KvsSectorHeaderCleaning));
+  }
+
+  bool HaveEmptySectorImpl(SectorIndex skip_sector = kSectorInvalid) const {
+    for (SectorIndex i = 0; i < SectorCount(); i++) {
+      if (i != skip_sector &&
+          sector_space_remaining_[i] == SectorSpaceAvailableWhenEmpty()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  bool IsInLastFreeSector(FlashPartition::Address address) {
+    return sector_space_remaining_[AddressToSectorIndex(address)] ==
+               SectorSpaceAvailableWhenEmpty() &&
+           !HaveEmptySectorImpl(AddressToSectorIndex(address));
+  }
+
+  FlashPartition& partition_;
+  os::Mutex lock_;
+  bool enabled_ = false;
+  uint8_t alignment_bytes_ = 0;
+  uint64_t next_sector_clean_order_ = 0;
+
+  // Free space available in each sector, set to 0 when clean is pending/active
+  uint32_t sector_space_remaining_[kSectorCountMax] = {0};
+  uint64_t sector_clean_order_[kSectorCountMax] = {kSectorCleanNotPending};
+  KeyMap key_map_[kListCapacityMax] = {{{0}}};
+  KeyIndex map_size_ = 0;
+
+  // +1 for nul-terminator since keys are stored as Length + Value and no nul
+  // termination but we are using them as nul-terminated strings through
+  // loading-up and comparing the keys.
+  char temp_key_buffer_[kChunkKeyLengthMax + 1u] = {0};
+  uint8_t temp_buffer_[cfg::kKvsBufferSize] = {0};
+};
+
+}  // namespace pw
diff --git a/pw_kvs/public/pw_kvs/partition_table_entry.h b/pw_kvs/public/pw_kvs/partition_table_entry.h
new file mode 100644
index 0000000..9aa111d
--- /dev/null
+++ b/pw_kvs/public/pw_kvs/partition_table_entry.h
@@ -0,0 +1,51 @@
+// Copyright 2020 The Pigweed Authors
+//
+// Licensed under the Apache License, Version 2.0 (the "License"); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//     https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+#pragma once
+
+#include <cstdint>
+
+namespace pw {
+
+namespace partition_table {
+
+enum DataType { kRawData, kKvs };
+
+}  // namespace partition_table
+
+enum class PartitionPermission : bool {
+  kReadOnly = true,
+  kReadAndWrite = false,
+};
+
+struct PartitionTableEntry {
+  constexpr PartitionTableEntry(
+      const uint32_t start_sector_index,
+      const uint32_t end_sector_index,
+      const uint8_t partition_identifier,
+      partition_table::DataType datatype,
+      PartitionPermission permission = PartitionPermission::kReadAndWrite)
+      : partition_start_sector_index(start_sector_index),
+        partition_end_sector_index(end_sector_index),
+        partition_id(partition_identifier),
+        data_type(datatype),
+        partition_permission(permission) {}
+
+  const uint32_t partition_start_sector_index;
+  const uint32_t partition_end_sector_index;
+  const uint8_t partition_id;
+  const partition_table::DataType data_type;
+  const PartitionPermission partition_permission;
+};
+
+}  // namespace pw