Project import generated by Copybara.

GitOrigin-RevId: 26b71fbfe58e7f60e045cec3e0f306e615590605
Change-Id: I0b6c07e747e5dd4e843f470951295cc9e04224c2
diff --git a/BUILD b/BUILD
index d26eb23..db6ef52 100644
--- a/BUILD
+++ b/BUILD
@@ -1,3 +1,4 @@
+
 # Copyright 2024 Google LLC
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
diff --git a/common/Cargo.lock b/common/Cargo.lock
index 758eb2d..496db0d 100644
--- a/common/Cargo.lock
+++ b/common/Cargo.lock
@@ -34,47 +34,48 @@
 
 [[package]]
 name = "anstream"
-version = "0.6.13"
+version = "0.6.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
 dependencies = [
  "anstyle",
  "anstyle-parse",
  "anstyle-query",
  "anstyle-wincon",
  "colorchoice",
+ "is_terminal_polyfill",
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle"
-version = "1.0.6"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
 dependencies = [
  "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.2"
+version = "3.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
 dependencies = [
  "anstyle",
  "windows-sys 0.52.0",
@@ -82,9 +83,9 @@
 
 [[package]]
 name = "anyhow"
-version = "1.0.82"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
+checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
 
 [[package]]
 name = "arbitrary"
@@ -94,9 +95,9 @@
 
 [[package]]
 name = "autocfg"
-version = "1.2.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
 
 [[package]]
 name = "bit-set"
@@ -163,12 +164,13 @@
 
 [[package]]
 name = "cc"
-version = "1.0.94"
+version = "1.0.97"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7"
+checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
 dependencies = [
  "jobserver",
  "libc",
+ "once_cell",
 ]
 
 [[package]]
@@ -281,9 +283,9 @@
 
 [[package]]
 name = "colorchoice"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
 
 [[package]]
 name = "combine"
@@ -466,9 +468,9 @@
 
 [[package]]
 name = "fastrand"
-version = "2.0.2"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
 
 [[package]]
 name = "file-header"
@@ -491,9 +493,9 @@
 
 [[package]]
 name = "getrandom"
-version = "0.2.14"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
 dependencies = [
  "cfg-if",
  "libc",
@@ -585,6 +587,12 @@
 ]
 
 [[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
 name = "itertools"
 version = "0.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -635,9 +643,9 @@
 
 [[package]]
 name = "jobserver"
-version = "0.1.30"
+version = "0.1.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2"
+checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
 dependencies = [
  "libc",
 ]
@@ -662,9 +670,9 @@
 
 [[package]]
 name = "libc"
-version = "0.2.153"
+version = "0.2.154"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
 
 [[package]]
 name = "libfuzzer-sys"
@@ -719,9 +727,9 @@
 
 [[package]]
 name = "lock_api"
-version = "0.4.11"
+version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
 dependencies = [
  "autocfg",
  "scopeguard",
@@ -757,9 +765,9 @@
 
 [[package]]
 name = "num-traits"
-version = "0.2.18"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 dependencies = [
  "autocfg",
  "libm",
@@ -849,9 +857,9 @@
 
 [[package]]
 name = "prettyplease"
-version = "0.2.19"
+version = "0.2.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ac2cf0f2e4f42b49f5ffd07dae8d746508ef7526c13940e5f524012ae6c6550"
+checksum = "5f12335488a2f3b0a83b14edad48dca9879ce89b2edd10e80237e4e852dd645e"
 dependencies = [
  "proc-macro2",
  "syn",
@@ -859,9 +867,9 @@
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.81"
+version = "1.0.82"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
+checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
 dependencies = [
  "unicode-ident",
 ]
@@ -1020,9 +1028,9 @@
 
 [[package]]
 name = "rustix"
-version = "0.38.32"
+version = "0.38.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
 dependencies = [
  "bitflags",
  "errno",
@@ -1045,9 +1053,9 @@
 
 [[package]]
 name = "ryu"
-version = "1.0.17"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
 
 [[package]]
 name = "same-file"
@@ -1066,18 +1074,18 @@
 
 [[package]]
 name = "serde"
-version = "1.0.198"
+version = "1.0.200"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
+checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.198"
+version = "1.0.200"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
+checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1124,9 +1132,9 @@
 
 [[package]]
 name = "syn"
-version = "2.0.60"
+version = "2.0.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
+checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1147,18 +1155,18 @@
 
 [[package]]
 name = "thiserror"
-version = "1.0.58"
+version = "1.0.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
+checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.58"
+version = "1.0.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
+checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1306,11 +1314,11 @@
 
 [[package]]
 name = "winapi-util"
-version = "0.1.6"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
 dependencies = [
- "winapi",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
diff --git a/nearby/presence/CMakeLists.txt b/nearby/CMakeLists.txt
similarity index 92%
rename from nearby/presence/CMakeLists.txt
rename to nearby/CMakeLists.txt
index bb099de..240cc3c 100644
--- a/nearby/presence/CMakeLists.txt
+++ b/nearby/CMakeLists.txt
@@ -14,12 +14,12 @@
 
 cmake_minimum_required(VERSION 3.14)
 
-project(NearbyPresenceProtocol)
+project(Nearby)
 
 set(CMAKE_CXX_STANDARD 20)
 
-set(BETO_CORE_ROOT ${CMAKE_SOURCE_DIR}/../..)
-set(NEARBY_ROOT ${CMAKE_SOURCE_DIR}/..)
+set(BETO_CORE_ROOT ${CMAKE_SOURCE_DIR}/..)
+set(NEARBY_ROOT ${CMAKE_SOURCE_DIR})
 set(THIRD_PARTY_DIR ${BETO_CORE_ROOT}/third_party)
 
 set(CMAKE_C_FLAGS_DEBUG "-DDEBUG")
@@ -104,5 +104,6 @@
     )
 endif ()
 
-add_subdirectory(np_cpp_ffi)
-add_subdirectory(ldt_np_adv_ffi/c)
+add_subdirectory(presence/np_cpp_ffi)
+add_subdirectory(presence/ldt_np_adv_ffi/c)
+add_subdirectory(connections/ukey2/ukey2_c_ffi/cpp/)
diff --git a/nearby/Cargo.lock b/nearby/Cargo.lock
index e50e7b7..6eb2999 100644
--- a/nearby/Cargo.lock
+++ b/nearby/Cargo.lock
@@ -21,9 +21,9 @@
 
 [[package]]
 name = "aes"
-version = "0.8.3"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac1f845298e95f983ff1944b728ae08b8cebab80d684f0a832ed0fc74dfa27e2"
+checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
 dependencies = [
  "cfg-if",
  "cipher",
@@ -61,9 +61,9 @@
 
 [[package]]
 name = "aho-corasick"
-version = "1.0.2"
+version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
+checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916"
 dependencies = [
  "memchr",
 ]
@@ -91,47 +91,48 @@
 
 [[package]]
 name = "anstream"
-version = "0.6.12"
+version = "0.6.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "96b09b5178381e0874812a9b157f7fe84982617e48f71f4e3235482775e5b540"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
 dependencies = [
  "anstyle",
  "anstyle-parse",
  "anstyle-query",
  "anstyle-wincon",
  "colorchoice",
+ "is_terminal_polyfill",
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle"
-version = "1.0.1"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a30da5c5f2d5e72842e00bcb57657162cdabef0931f40e2deb9b4140440cecd"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.1"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.0.0"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b"
+checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
 dependencies = [
- "windows-sys 0.48.0",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.2"
+version = "3.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
 dependencies = [
  "anstyle",
  "windows-sys 0.52.0",
@@ -139,9 +140,18 @@
 
 [[package]]
 name = "anyhow"
-version = "1.0.80"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1"
+checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
+
+[[package]]
+name = "arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110"
+dependencies = [
+ "derive_arbitrary",
+]
 
 [[package]]
 name = "array_ref"
@@ -164,9 +174,9 @@
 
 [[package]]
 name = "autocfg"
-version = "1.1.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
 
 [[package]]
 name = "base16ct"
@@ -194,9 +204,9 @@
 
 [[package]]
 name = "bitflags"
-version = "2.4.2"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
+checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
 
 [[package]]
 name = "blake2"
@@ -238,9 +248,9 @@
 
 [[package]]
 name = "bstr"
-version = "1.6.0"
+version = "1.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05"
+checksum = "05efc5cfd9110c8416e471df0e96702d58690178e206e61b7173706673c93706"
 dependencies = [
  "memchr",
  "serde",
@@ -252,10 +262,10 @@
 dependencies = [
  "anyhow",
  "chrono",
- "clap 4.5.1",
- "cmd-runner",
+ "clap 4.5.4",
+ "cmd_runner",
  "crossbeam",
- "env_logger",
+ "env_logger 0.10.2",
  "file-header",
  "glob",
  "globset",
@@ -268,26 +278,26 @@
  "tempfile",
  "thiserror",
  "walkdir",
- "which",
+ "xshell",
 ]
 
 [[package]]
 name = "bumpalo"
-version = "3.13.0"
+version = "3.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1"
+checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
 
 [[package]]
 name = "byteorder"
-version = "1.4.3"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
 name = "bytes"
-version = "1.5.0"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
+checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9"
 
 [[package]]
 name = "cast"
@@ -311,7 +321,7 @@
 checksum = "da6bc11b07529f16944307272d5bd9b22530bc7d05751717c9d416586cedab49"
 dependencies = [
  "clap 3.2.25",
- "heck",
+ "heck 0.4.1",
  "indexmap",
  "log",
  "proc-macro2",
@@ -325,9 +335,14 @@
 
 [[package]]
 name = "cc"
-version = "1.0.79"
+version = "1.0.97"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f"
+checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
+dependencies = [
+ "jobserver",
+ "libc",
+ "once_cell",
+]
 
 [[package]]
 name = "cesu8"
@@ -343,21 +358,23 @@
 
 [[package]]
 name = "chrono"
-version = "0.4.34"
+version = "0.4.38"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b"
+checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
 dependencies = [
  "android-tzdata",
  "iana-time-zone",
+ "js-sys",
  "num-traits",
- "windows-targets 0.52.3",
+ "wasm-bindgen",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
 name = "ciborium"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "effd91f6c78e5a4ace8a5d3c0b6bfaec9e2baaef55f3efc00e45fb2e477ee926"
+checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e"
 dependencies = [
  "ciborium-io",
  "ciborium-ll",
@@ -366,15 +383,15 @@
 
 [[package]]
 name = "ciborium-io"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cdf919175532b369853f5d5e20b26b43112613fd6fe7aee757e35f7a44642656"
+checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757"
 
 [[package]]
 name = "ciborium-ll"
-version = "0.2.1"
+version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "defaa24ecc093c77630e6c15e17c51f5e187bf35ee514f4e2d67baaa96dae22b"
+checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9"
 dependencies = [
  "ciborium-io",
  "half",
@@ -407,9 +424,9 @@
 
 [[package]]
 name = "clap"
-version = "4.5.1"
+version = "4.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c918d541ef2913577a0f9566e9ce27cb35b6df072075769e0b26cb5a554520da"
+checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -417,26 +434,26 @@
 
 [[package]]
 name = "clap_builder"
-version = "4.5.1"
+version = "4.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f3e7391dad68afb0c2ede1bf619f579a3dc9c2ec67f089baa397123a2f3d1eb"
+checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4"
 dependencies = [
  "anstream",
  "anstyle",
  "clap_lex 0.7.0",
- "strsim 0.11.0",
+ "strsim 0.11.1",
 ]
 
 [[package]]
 name = "clap_derive"
-version = "4.5.0"
+version = "4.5.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47"
+checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64"
 dependencies = [
- "heck",
+ "heck 0.5.0",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.61",
 ]
 
 [[package]]
@@ -455,25 +472,31 @@
 checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
 
 [[package]]
-name = "cmd-runner"
+name = "cmd_runner"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "chrono",
+ "clap 4.5.4",
+ "file-header",
+ "globset",
+ "log",
  "owo-colors",
  "shell-escape",
+ "xshell",
 ]
 
 [[package]]
 name = "colorchoice"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
 
 [[package]]
 name = "combine"
-version = "4.6.6"
+version = "4.6.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
 dependencies = [
  "bytes",
  "memchr",
@@ -481,30 +504,30 @@
 
 [[package]]
 name = "const-oid"
-version = "0.9.4"
+version = "0.9.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747"
+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
 
 [[package]]
 name = "core-foundation-sys"
-version = "0.8.4"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
 
 [[package]]
 name = "cpufeatures"
-version = "0.2.9"
+version = "0.2.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a17b76ff3a4162b0b27f354a0c87015ddad39d35f9c0c36607a3bdd175dde1f1"
+checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504"
 dependencies = [
  "libc",
 ]
 
 [[package]]
 name = "crc32fast"
-version = "1.3.2"
+version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d"
+checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa"
 dependencies = [
  "cfg-if",
 ]
@@ -518,10 +541,10 @@
  "anes",
  "cast",
  "ciborium",
- "clap 4.5.1",
+ "clap 4.5.4",
  "criterion-plot",
  "is-terminal",
- "itertools",
+ "itertools 0.10.5",
  "num-traits",
  "once_cell",
  "oorandom",
@@ -542,16 +565,15 @@
 checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1"
 dependencies = [
  "cast",
- "itertools",
+ "itertools 0.10.5",
 ]
 
 [[package]]
 name = "crossbeam"
-version = "0.8.2"
+version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2801af0d36612ae591caa9568261fddce32ce6e08a7275ea334a06a4ad021a2c"
+checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8"
 dependencies = [
- "cfg-if",
  "crossbeam-channel",
  "crossbeam-deque",
  "crossbeam-epoch",
@@ -561,65 +583,61 @@
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.8"
+version = "0.5.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a33c2bf77f2df06183c3aa30d1e96c0695a313d4f9c453cc3762a6db39f99200"
+checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95"
 dependencies = [
- "cfg-if",
  "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-deque"
-version = "0.8.3"
+version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef"
+checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d"
 dependencies = [
- "cfg-if",
  "crossbeam-epoch",
  "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-epoch"
-version = "0.9.15"
+version = "0.9.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
 dependencies = [
- "autocfg",
- "cfg-if",
  "crossbeam-utils",
- "memoffset",
- "scopeguard",
 ]
 
 [[package]]
 name = "crossbeam-queue"
-version = "0.3.8"
+version = "0.3.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1cfb3ea8a53f37c40dea2c7bedcbd88bdfae54f5e2175d6ecaff1c988353add"
+checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35"
 dependencies = [
- "cfg-if",
  "crossbeam-utils",
 ]
 
 [[package]]
 name = "crossbeam-utils"
-version = "0.8.16"
+version = "0.8.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a22b2d63d4d1dc0b7f1b6b2747dd0088008a9be28b6ddf0b1e7d335e3037294"
-dependencies = [
- "cfg-if",
-]
+checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345"
+
+[[package]]
+name = "crunchy"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
 
 [[package]]
 name = "crypto-bigint"
-version = "0.5.2"
+version = "0.5.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cf4c2f4e1afd912bc40bfd6fed5d9dc1f288e0ba01bfcc835cc5bc3eb13efe15"
+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
 dependencies = [
  "generic-array",
- "rand_core 0.6.4",
+ "rand_core",
  "subtle",
  "zeroize",
 ]
@@ -631,7 +649,7 @@
 checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
 dependencies = [
  "generic-array",
- "rand_core 0.6.4",
+ "rand_core",
  "typenum",
 ]
 
@@ -686,7 +704,7 @@
  "p256",
  "rand",
  "rand_chacha",
- "rand_core 0.6.4",
+ "rand_core",
  "sec1",
  "sha2",
  "subtle",
@@ -716,6 +734,16 @@
 ]
 
 [[package]]
+name = "crypto_provider_test-fuzz"
+version = "0.0.0"
+dependencies = [
+ "crypto_provider",
+ "crypto_provider_default",
+ "derive_fuzztest",
+ "libfuzzer-sys",
+]
+
+[[package]]
 name = "ctr"
 version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -726,9 +754,9 @@
 
 [[package]]
 name = "curve25519-dalek"
-version = "4.1.1"
+version = "4.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e89b8c6a2e4b1f45971ad09761aafb85514a84744b67a95e32c3cc1352d1f65c"
+checksum = "0a677b8922c94e01bdbb12126b0bc852f00447528dee1782229af9c720c3f348"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -743,26 +771,55 @@
 
 [[package]]
 name = "curve25519-dalek-derive"
-version = "0.1.0"
+version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fdaf97f4804dcebfa5862639bc9ce4121e82140bec2a987ac5140294865b5b"
+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.61",
 ]
 
 [[package]]
 name = "der"
-version = "0.7.7"
+version = "0.7.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0c7ed52955ce76b1554f509074bb357d3fb8ac9b51288a65a3fd480d1dfba946"
+checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
 dependencies = [
  "const-oid",
  "zeroize",
 ]
 
 [[package]]
+name = "derive_arbitrary"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.61",
+]
+
+[[package]]
+name = "derive_fuzztest"
+version = "0.1.0"
+dependencies = [
+ "arbitrary",
+ "derive_fuzztest_macro",
+ "quickcheck",
+]
+
+[[package]]
+name = "derive_fuzztest_macro"
+version = "0.1.0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.61",
+]
+
+[[package]]
 name = "diff"
 version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -781,9 +838,9 @@
 
 [[package]]
 name = "ed25519"
-version = "2.2.1"
+version = "2.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963"
+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
 dependencies = [
  "pkcs8",
  "signature",
@@ -797,7 +854,7 @@
 dependencies = [
  "curve25519-dalek",
  "ed25519",
- "rand_core 0.6.4",
+ "rand_core",
  "serde",
  "sha2",
  "subtle",
@@ -806,15 +863,15 @@
 
 [[package]]
 name = "either"
-version = "1.9.0"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
+checksum = "a47c1c47d2f5964e29c61246e81db715514cd532db6b5116a25ea3c03d6780a2"
 
 [[package]]
 name = "elliptic-curve"
-version = "0.13.5"
+version = "0.13.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b"
+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
 dependencies = [
  "base16ct",
  "crypto-bigint",
@@ -823,7 +880,7 @@
  "generic-array",
  "group",
  "hkdf",
- "rand_core 0.6.4",
+ "rand_core",
  "sec1",
  "subtle",
  "zeroize",
@@ -831,6 +888,16 @@
 
 [[package]]
 name = "env_logger"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a19187fea3ac7e84da7dacf48de0c45d63c6a76f9490dae389aead16c243fce3"
+dependencies = [
+ "log",
+ "regex",
+]
+
+[[package]]
+name = "env_logger"
 version = "0.10.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
@@ -854,9 +921,9 @@
 
 [[package]]
 name = "fastrand"
-version = "2.0.1"
+version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
+checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
 
 [[package]]
 name = "ff"
@@ -864,15 +931,15 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449"
 dependencies = [
- "rand_core 0.6.4",
+ "rand_core",
  "subtle",
 ]
 
 [[package]]
 name = "fiat-crypto"
-version = "0.2.2"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a481586acf778f1b1455424c343f71124b048ffa5f4fc3f8f6ae9dc432dcb3c7"
+checksum = "38793c55593b33412e3ae40c2c9781ffaa6f438f6f8c10f24e71846fbd7ae01e"
 
 [[package]]
 name = "file-header"
@@ -889,21 +956,15 @@
 
 [[package]]
 name = "flate2"
-version = "1.0.26"
+version = "1.0.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b9429470923de8e8cbd4d2dc513535400b4b3fef0319fb5c4e1f520a7bef743"
+checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
 dependencies = [
  "crc32fast",
  "miniz_oxide",
 ]
 
 [[package]]
-name = "fnv"
-version = "1.0.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
-
-[[package]]
 name = "generic-array"
 version = "0.14.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -916,9 +977,9 @@
 
 [[package]]
 name = "getrandom"
-version = "0.2.10"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427"
+checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
 dependencies = [
  "cfg-if",
  "libc",
@@ -927,9 +988,9 @@
 
 [[package]]
 name = "ghash"
-version = "0.5.0"
+version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d930750de5717d2dd0b8c0d42c076c0e884c81a73e6cab859bbd2339c71e3e40"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
 dependencies = [
  "opaque-debug",
  "polyval",
@@ -943,15 +1004,15 @@
 
 [[package]]
 name = "globset"
-version = "0.4.12"
+version = "0.4.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aca8bbd8e0707c1887a8bbb7e6b40e228f251ff5d62c8220a4a7a53c73aff006"
+checksum = "57da3b9b5b85bd66f31093f8c408b90a74431672542466497dcbdfdc02034be1"
 dependencies = [
  "aho-corasick",
  "bstr",
- "fnv",
  "log",
- "regex",
+ "regex-automata",
+ "regex-syntax",
 ]
 
 [[package]]
@@ -961,22 +1022,24 @@
 checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
 dependencies = [
  "ff",
- "rand_core 0.6.4",
+ "rand_core",
  "subtle",
 ]
 
 [[package]]
 name = "half"
-version = "1.8.2"
+version = "2.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7"
+checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888"
+dependencies = [
+ "cfg-if",
+ "crunchy",
+]
 
 [[package]]
 name = "handle_map"
 version = "0.1.0"
 dependencies = [
- "criterion",
- "lazy_static",
  "lock_adapter",
 ]
 
@@ -1007,6 +1070,12 @@
 checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
 
 [[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
 name = "hermit-abi"
 version = "0.1.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1017,9 +1086,9 @@
 
 [[package]]
 name = "hermit-abi"
-version = "0.3.2"
+version = "0.3.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b"
+checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024"
 
 [[package]]
 name = "hex"
@@ -1035,9 +1104,9 @@
 
 [[package]]
 name = "hkdf"
-version = "0.12.3"
+version = "0.12.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "791a029f6b9fc27657f6f188ec6e5e43f6911f6f878e0dc5501396e09809d437"
+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
 dependencies = [
  "hmac",
 ]
@@ -1052,6 +1121,15 @@
 ]
 
 [[package]]
+name = "home"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5"
+dependencies = [
+ "windows-sys 0.52.0",
+]
+
+[[package]]
 name = "humantime"
 version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1059,16 +1137,16 @@
 
 [[package]]
 name = "iana-time-zone"
-version = "0.1.57"
+version = "0.1.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613"
+checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
 dependencies = [
  "android_system_properties",
  "core-foundation-sys",
  "iana-time-zone-haiku",
  "js-sys",
  "wasm-bindgen",
- "windows",
+ "windows-core",
 ]
 
 [[package]]
@@ -1102,16 +1180,22 @@
 
 [[package]]
 name = "is-terminal"
-version = "0.4.9"
+version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b"
+checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b"
 dependencies = [
- "hermit-abi 0.3.2",
- "rustix",
- "windows-sys 0.48.0",
+ "hermit-abi 0.3.9",
+ "libc",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
 name = "itertools"
 version = "0.10.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1121,20 +1205,19 @@
 ]
 
 [[package]]
-name = "itoa"
-version = "1.0.9"
+name = "itertools"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
 
 [[package]]
-name = "java-locator"
-version = "0.1.5"
+name = "itoa"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90003f2fd9c52f212c21d8520f1128da0080bad6fff16b68fe6e7f2f0c3780c2"
-dependencies = [
- "glob",
- "lazy_static",
-]
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
 
 [[package]]
 name = "jni"
@@ -1145,9 +1228,7 @@
  "cesu8",
  "cfg-if",
  "combine",
- "java-locator",
  "jni-sys",
- "libloading",
  "log",
  "thiserror",
  "walkdir",
@@ -1161,10 +1242,19 @@
 checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130"
 
 [[package]]
-name = "js-sys"
-version = "0.3.64"
+name = "jobserver"
+version = "0.1.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5f195fe497f702db0f318b07fdd68edb16955aed830df8363d837542f8f935a"
+checksum = "d2b099aaa34a9751c5bf0878add70444e1ed2dd73f347be99003d4577277de6e"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -1186,7 +1276,7 @@
  "anyhow",
  "base64",
  "blake2",
- "clap 4.5.1",
+ "clap 4.5.4",
  "criterion",
  "crypto_provider",
  "crypto_provider_default",
@@ -1206,6 +1296,32 @@
 ]
 
 [[package]]
+name = "ldt-fuzz"
+version = "0.1.0"
+dependencies = [
+ "arbitrary",
+ "crypto_provider_rustcrypto",
+ "derive_fuzztest",
+ "ldt",
+ "libfuzzer-sys",
+ "xts_aes",
+]
+
+[[package]]
+name = "ldt-np-adv-fuzz"
+version = "0.1.0"
+dependencies = [
+ "arbitrary",
+ "crypto_provider_rustcrypto",
+ "derive_fuzztest",
+ "ldt",
+ "ldt_np_adv",
+ "libfuzzer-sys",
+ "np_hkdf",
+ "xts_aes",
+]
+
+[[package]]
 name = "ldt_np_adv"
 version = "0.1.0"
 dependencies = [
@@ -1225,6 +1341,7 @@
  "rand_pcg",
  "serde_json",
  "test_helper",
+ "test_vector_hkdf",
  "xts_aes",
 ]
 
@@ -1265,25 +1382,26 @@
 
 [[package]]
 name = "libc"
-version = "0.2.153"
+version = "0.2.154"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
 
 [[package]]
-name = "libloading"
-version = "0.7.4"
+name = "libfuzzer-sys"
+version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
+checksum = "a96cfd5557eb82f2b83fed4955246c988d331975a002961b07c81584d107e7f7"
 dependencies = [
- "cfg-if",
- "winapi",
+ "arbitrary",
+ "cc",
+ "once_cell",
 ]
 
 [[package]]
 name = "license"
-version = "3.2.0"
+version = "3.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "778718185117620a06e95d2b1e57d50166b1d6bfad93c8abfc1b3344c863ad8c"
+checksum = "3bba2f02ee1d13cd4bea565658939cd851d70e391f34f7c27b45b2077df3a2e4"
 dependencies = [
  "reword",
  "serde",
@@ -1305,9 +1423,9 @@
 
 [[package]]
 name = "lock_api"
-version = "0.4.10"
+version = "0.4.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
 dependencies = [
  "autocfg",
  "scopeguard",
@@ -1315,24 +1433,15 @@
 
 [[package]]
 name = "log"
-version = "0.4.20"
+version = "0.4.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
 
 [[package]]
 name = "memchr"
-version = "2.6.4"
+version = "2.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167"
-
-[[package]]
-name = "memoffset"
-version = "0.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c"
-dependencies = [
- "autocfg",
-]
+checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
 
 [[package]]
 name = "minimal-lexical"
@@ -1342,9 +1451,9 @@
 
 [[package]]
 name = "miniz_oxide"
-version = "0.7.1"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e7810e0be55b428ada41041c41f32c9f1a42817901b4ccf45fa3d4b6561e74c7"
+checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7"
 dependencies = [
  "adler",
 ]
@@ -1383,6 +1492,7 @@
  "strum",
  "strum_macros",
  "test_helper",
+ "test_vector_hkdf",
  "tinyvec",
  "xts_aes",
 ]
@@ -1394,6 +1504,7 @@
  "array_view",
  "crypto_provider",
  "np_adv",
+ "np_hkdf",
  "sink",
 ]
 
@@ -1430,6 +1541,8 @@
  "np_adv",
  "np_adv_dynamic",
  "np_hkdf",
+ "strum",
+ "strum_macros",
 ]
 
 [[package]]
@@ -1446,54 +1559,56 @@
  "rand_ext",
  "serde_json",
  "test_helper",
+ "test_vector_hkdf",
  "xts_aes",
 ]
 
 [[package]]
-name = "num-bigint"
-version = "0.4.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0"
+name = "np_java_ffi"
+version = "0.1.0"
 dependencies = [
- "autocfg",
+ "crypto_provider_default",
+ "handle_map",
+ "jni",
+ "np_adv",
+ "np_ffi_core",
+ "pourover",
+ "pourover_macro",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c165a9ab64cf766f73521c0dd2cfdff64f488b8f0b3e621face3462d3db536d7"
+dependencies = [
  "num-integer",
  "num-traits",
 ]
 
 [[package]]
 name = "num-integer"
-version = "0.1.45"
+version = "0.1.46"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
 dependencies = [
- "autocfg",
  "num-traits",
 ]
 
 [[package]]
 name = "num-traits"
-version = "0.2.16"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f30b0abd723be7e2ffca1272140fac1a2f084c77ec3e123c192b66af1ee9e6c2"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 dependencies = [
  "autocfg",
 ]
 
 [[package]]
-name = "num_cpus"
-version = "1.16.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43"
-dependencies = [
- "hermit-abi 0.3.2",
- "libc",
-]
-
-[[package]]
 name = "once_cell"
-version = "1.18.0"
+version = "1.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
 
 [[package]]
 name = "oorandom"
@@ -1503,15 +1618,15 @@
 
 [[package]]
 name = "opaque-debug"
-version = "0.3.0"
+version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
 
 [[package]]
 name = "os_str_bytes"
-version = "6.5.1"
+version = "6.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d5d9eb14b174ee9aa2ef96dc2b94637a2d4b6e7cb873c7e171f0c20c6cf3eac"
+checksum = "e2355d85b9a3786f481747ced0e0ff2ba35213a1f9bd406ed906554d7af805a1"
 
 [[package]]
 name = "owo-colors"
@@ -1541,9 +1656,9 @@
 
 [[package]]
 name = "platforms"
-version = "3.0.2"
+version = "3.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3d7ddaed09e0eb771a79ab0fd64609ba0afb0a8366421957936ad14cbd13630"
+checksum = "db23d408679286588f4d4644f965003d056e3dd5abcaaa938116871d7ce2fee7"
 
 [[package]]
 name = "plotters"
@@ -1575,9 +1690,9 @@
 
 [[package]]
 name = "polyval"
-version = "0.6.1"
+version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d52cff9d1d4dee5fe6d03729099f4a310a41179e0a10dbf542039873f2e826fb"
+checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -1597,16 +1712,10 @@
 name = "pourover_macro"
 version = "0.1.0"
 dependencies = [
- "pourover_macro_core",
-]
-
-[[package]]
-name = "pourover_macro_core"
-version = "0.1.0"
-dependencies = [
+ "nom",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.61",
 ]
 
 [[package]]
@@ -1617,18 +1726,18 @@
 
 [[package]]
 name = "primeorder"
-version = "0.13.2"
+version = "0.13.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3c2fcef82c0ec6eefcc179b978446c399b3cdf73c392c35604e399eee6df1ee3"
+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
 dependencies = [
  "elliptic-curve",
 ]
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.78"
+version = "1.0.82"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
+checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
 dependencies = [
  "unicode-ident",
 ]
@@ -1685,10 +1794,21 @@
 ]
 
 [[package]]
-name = "quote"
-version = "1.0.35"
+name = "quickcheck"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef"
+checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
+dependencies = [
+ "env_logger 0.8.4",
+ "log",
+ "rand",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
 dependencies = [
  "proc-macro2",
 ]
@@ -1701,7 +1821,7 @@
 dependencies = [
  "libc",
  "rand_chacha",
- "rand_core 0.6.4",
+ "rand_core",
 ]
 
 [[package]]
@@ -1711,17 +1831,11 @@
 checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
 dependencies = [
  "ppv-lite86",
- "rand_core 0.6.4",
+ "rand_core",
 ]
 
 [[package]]
 name = "rand_core"
-version = "0.5.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19"
-
-[[package]]
-name = "rand_core"
 version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
@@ -1730,14 +1844,6 @@
 ]
 
 [[package]]
-name = "rand_core_05_adapter"
-version = "0.1.0"
-dependencies = [
- "rand",
- "rand_core 0.5.1",
-]
-
-[[package]]
 name = "rand_ext"
 version = "0.1.0"
 dependencies = [
@@ -1753,14 +1859,14 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "59cad018caf63deb318e5a4586d99a24424a364f40f1e5778c29aca23f4fc73e"
 dependencies = [
- "rand_core 0.6.4",
+ "rand_core",
 ]
 
 [[package]]
 name = "rayon"
-version = "1.7.0"
+version = "1.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b"
+checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa"
 dependencies = [
  "either",
  "rayon-core",
@@ -1768,21 +1874,19 @@
 
 [[package]]
 name = "rayon-core"
-version = "1.11.0"
+version = "1.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d"
+checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2"
 dependencies = [
- "crossbeam-channel",
  "crossbeam-deque",
  "crossbeam-utils",
- "num_cpus",
 ]
 
 [[package]]
 name = "regex"
-version = "1.10.2"
+version = "1.10.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343"
+checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1792,9 +1896,9 @@
 
 [[package]]
 name = "regex-automata"
-version = "0.4.3"
+version = "0.4.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f"
+checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea"
 dependencies = [
  "aho-corasick",
  "memchr",
@@ -1803,15 +1907,15 @@
 
 [[package]]
 name = "regex-syntax"
-version = "0.8.2"
+version = "0.8.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f"
+checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56"
 
 [[package]]
 name = "relative-path"
-version = "1.9.2"
+version = "1.9.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e898588f33fdd5b9420719948f9f2a32c922a246964576f71ba7f24f80610fbc"
+checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2"
 
 [[package]]
 name = "reword"
@@ -1845,7 +1949,7 @@
  "regex",
  "relative-path",
  "rustc_version",
- "syn 2.0.50",
+ "syn 2.0.61",
  "unicode-ident",
 ]
 
@@ -1858,7 +1962,7 @@
  "quote",
  "rand",
  "rustc_version",
- "syn 2.0.50",
+ "syn 2.0.61",
 ]
 
 [[package]]
@@ -1872,11 +1976,11 @@
 
 [[package]]
 name = "rustix"
-version = "0.38.31"
+version = "0.38.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949"
+checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
 dependencies = [
- "bitflags 2.4.2",
+ "bitflags 2.5.0",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -1885,15 +1989,15 @@
 
 [[package]]
 name = "rustversion"
-version = "1.0.14"
+version = "1.0.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4"
+checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0"
 
 [[package]]
 name = "ryu"
-version = "1.0.15"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
 
 [[package]]
 name = "same-file"
@@ -1925,35 +2029,35 @@
 
 [[package]]
 name = "semver"
-version = "1.0.18"
+version = "1.0.23"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
+checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b"
 
 [[package]]
 name = "serde"
-version = "1.0.197"
+version = "1.0.200"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
+checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.197"
+version = "1.0.200"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
+checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.61",
 ]
 
 [[package]]
 name = "serde_json"
-version = "1.0.114"
+version = "1.0.116"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
+checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
 dependencies = [
  "itoa",
  "ryu",
@@ -1979,9 +2083,12 @@
 
 [[package]]
 name = "signature"
-version = "2.1.0"
+version = "2.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500"
+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
+dependencies = [
+ "rand_core",
+]
 
 [[package]]
 name = "sink"
@@ -2007,9 +2114,9 @@
 
 [[package]]
 name = "spki"
-version = "0.7.2"
+version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a"
+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
 dependencies = [
  "base64ct",
  "der",
@@ -2023,9 +2130,9 @@
 
 [[package]]
 name = "strsim"
-version = "0.11.0"
+version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
 
 [[package]]
 name = "strum"
@@ -2039,11 +2146,11 @@
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
 dependencies = [
- "heck",
+ "heck 0.4.1",
  "proc-macro2",
  "quote",
  "rustversion",
- "syn 2.0.50",
+ "syn 2.0.61",
 ]
 
 [[package]]
@@ -2065,9 +2172,9 @@
 
 [[package]]
 name = "syn"
-version = "2.0.50"
+version = "2.0.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb"
+checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2076,9 +2183,9 @@
 
 [[package]]
 name = "tempfile"
-version = "3.10.0"
+version = "3.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67"
+checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1"
 dependencies = [
  "cfg-if",
  "fastrand",
@@ -2088,9 +2195,9 @@
 
 [[package]]
 name = "termcolor"
-version = "1.2.0"
+version = "1.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "be55cf8942feac5c765c2c993422806843c9a9a45d4d5c407ad6dd2ea95eb9b6"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
 dependencies = [
  "winapi-util",
 ]
@@ -2100,33 +2207,42 @@
 version = "0.1.0"
 dependencies = [
  "hex",
+ "itertools 0.12.1",
  "serde_json",
 ]
 
 [[package]]
+name = "test_vector_hkdf"
+version = "0.1.0"
+dependencies = [
+ "crypto_provider",
+ "crypto_provider_default",
+]
+
+[[package]]
 name = "textwrap"
-version = "0.16.0"
+version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
+checksum = "23d434d3f8967a09480fb04132ebe0a3e088c173e6d0ee7897abbdf4eab0f8b9"
 
 [[package]]
 name = "thiserror"
-version = "1.0.57"
+version = "1.0.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b"
+checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.57"
+version = "1.0.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81"
+checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.61",
 ]
 
 [[package]]
@@ -2156,9 +2272,9 @@
 
 [[package]]
 name = "typenum"
-version = "1.16.0"
+version = "1.17.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
+checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825"
 
 [[package]]
 name = "ukey2_c_ffi"
@@ -2172,7 +2288,6 @@
  "rand",
  "rand_chacha",
  "ukey2_connections",
- "ukey2_rs",
 ]
 
 [[package]]
@@ -2191,6 +2306,19 @@
 ]
 
 [[package]]
+name = "ukey2_connections-fuzz"
+version = "0.1.0"
+dependencies = [
+ "arbitrary",
+ "crypto_provider_rustcrypto",
+ "derive_fuzztest",
+ "libfuzzer-sys",
+ "rand_chacha",
+ "ukey2_connections",
+ "ukey2_rs",
+]
+
+[[package]]
 name = "ukey2_jni"
 version = "0.1.0"
 dependencies = [
@@ -2203,7 +2331,6 @@
  "rand",
  "rand_chacha",
  "ukey2_connections",
- "ukey2_rs",
 ]
 
 [[package]]
@@ -2232,23 +2359,22 @@
 name = "ukey2_shell"
 version = "0.1.0"
 dependencies = [
- "clap 4.5.1",
+ "clap 4.5.4",
  "crypto_provider_rustcrypto",
  "ukey2_connections",
- "ukey2_rs",
 ]
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.11"
+version = "1.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
 
 [[package]]
 name = "unicode-segmentation"
-version = "1.10.1"
+version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
+checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
 
 [[package]]
 name = "universal-hash"
@@ -2274,9 +2400,9 @@
 
 [[package]]
 name = "walkdir"
-version = "2.3.3"
+version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
 dependencies = [
  "same-file",
  "winapi-util",
@@ -2290,9 +2416,9 @@
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.87"
+version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7706a72ab36d8cb1f80ffbf0e071533974a60d0a308d01a5d0375bf60499a342"
+checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
 dependencies = [
  "cfg-if",
  "wasm-bindgen-macro",
@@ -2300,24 +2426,24 @@
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.87"
+version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd"
+checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
 dependencies = [
  "bumpalo",
  "log",
  "once_cell",
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.61",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.87"
+version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dee495e55982a3bd48105a7b947fd2a9b4a8ae3010041b9e0faab3f9cd028f1d"
+checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -2325,28 +2451,28 @@
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.87"
+version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b"
+checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn 2.0.50",
+ "syn 2.0.61",
  "wasm-bindgen-backend",
  "wasm-bindgen-shared",
 ]
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.87"
+version = "0.2.92"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca6ad05a4870b2bf5fe995117d3728437bd27d7cd5f06f13c17443ef369775a1"
+checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
 
 [[package]]
 name = "web-sys"
-version = "0.3.64"
+version = "0.3.69"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9b85cbef8c220a6abc02aefd892dfc0fc23afb1c6a426316ec33253a3877249b"
+checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
@@ -2354,13 +2480,14 @@
 
 [[package]]
 name = "which"
-version = "4.4.0"
+version = "4.4.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
 dependencies = [
  "either",
- "libc",
+ "home",
  "once_cell",
+ "rustix",
 ]
 
 [[package]]
@@ -2381,11 +2508,11 @@
 
 [[package]]
 name = "winapi-util"
-version = "0.1.5"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
 dependencies = [
- "winapi",
+ "windows-sys 0.52.0",
 ]
 
 [[package]]
@@ -2395,12 +2522,12 @@
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
-name = "windows"
-version = "0.48.0"
+name = "windows-core"
+version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
 dependencies = [
- "windows-targets 0.48.1",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
@@ -2414,20 +2541,11 @@
 
 [[package]]
 name = "windows-sys"
-version = "0.48.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
-dependencies = [
- "windows-targets 0.48.1",
-]
-
-[[package]]
-name = "windows-sys"
 version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
 dependencies = [
- "windows-targets 0.52.3",
+ "windows-targets 0.52.5",
 ]
 
 [[package]]
@@ -2447,32 +2565,18 @@
 
 [[package]]
 name = "windows-targets"
-version = "0.48.1"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
 dependencies = [
- "windows_aarch64_gnullvm 0.48.0",
- "windows_aarch64_msvc 0.48.0",
- "windows_i686_gnu 0.48.0",
- "windows_i686_msvc 0.48.0",
- "windows_x86_64_gnu 0.48.0",
- "windows_x86_64_gnullvm 0.48.0",
- "windows_x86_64_msvc 0.48.0",
-]
-
-[[package]]
-name = "windows-targets"
-version = "0.52.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f"
-dependencies = [
- "windows_aarch64_gnullvm 0.52.3",
- "windows_aarch64_msvc 0.52.3",
- "windows_i686_gnu 0.52.3",
- "windows_i686_msvc 0.52.3",
- "windows_x86_64_gnu 0.52.3",
- "windows_x86_64_gnullvm 0.52.3",
- "windows_x86_64_msvc 0.52.3",
+ "windows_aarch64_gnullvm 0.52.5",
+ "windows_aarch64_msvc 0.52.5",
+ "windows_i686_gnu 0.52.5",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.5",
+ "windows_x86_64_gnu 0.52.5",
+ "windows_x86_64_gnullvm 0.52.5",
+ "windows_x86_64_msvc 0.52.5",
 ]
 
 [[package]]
@@ -2483,15 +2587,9 @@
 
 [[package]]
 name = "windows_aarch64_gnullvm"
-version = "0.48.0"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
-
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.52.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
 
 [[package]]
 name = "windows_aarch64_msvc"
@@ -2501,15 +2599,9 @@
 
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.48.0"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
-
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.52.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
 
 [[package]]
 name = "windows_i686_gnu"
@@ -2519,15 +2611,15 @@
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.48.0"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
 
 [[package]]
-name = "windows_i686_gnu"
-version = "0.52.3"
+name = "windows_i686_gnullvm"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
 
 [[package]]
 name = "windows_i686_msvc"
@@ -2537,15 +2629,9 @@
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.48.0"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
-
-[[package]]
-name = "windows_i686_msvc"
-version = "0.52.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
 
 [[package]]
 name = "windows_x86_64_gnu"
@@ -2555,15 +2641,9 @@
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.48.0"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
-
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.52.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
 
 [[package]]
 name = "windows_x86_64_gnullvm"
@@ -2573,15 +2653,9 @@
 
 [[package]]
 name = "windows_x86_64_gnullvm"
-version = "0.48.0"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
-
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.52.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
 
 [[package]]
 name = "windows_x86_64_msvc"
@@ -2591,15 +2665,9 @@
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.48.0"
+version = "0.52.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
-
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.52.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
 
 [[package]]
 name = "wycheproof"
@@ -2615,12 +2683,40 @@
 
 [[package]]
 name = "x25519-dalek"
-version = "2.0.0"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb66477291e7e8d2b0ff1bcb900bf29489a9692816d79874bea351e7a8b6de96"
+checksum = "c7e468321c81fb07fa7f4c636c3972b9100f0346e5b6a9f2bd0603a52f7ed277"
 dependencies = [
  "curve25519-dalek",
- "rand_core 0.6.4",
+ "rand_core",
+]
+
+[[package]]
+name = "xshell"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6db0ab86eae739efd1b054a8d3d16041914030ac4e01cd1dca0cf252fd8b6437"
+dependencies = [
+ "xshell-macros",
+]
+
+[[package]]
+name = "xshell-macros"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852"
+
+[[package]]
+name = "xts-aes-fuzz"
+version = "0.0.0"
+dependencies = [
+ "arbitrary",
+ "crypto_provider",
+ "crypto_provider_rustcrypto",
+ "derive_fuzztest",
+ "ldt_tbc",
+ "libfuzzer-sys",
+ "xts_aes",
 ]
 
 [[package]]
@@ -2656,6 +2752,6 @@
 
 [[package]]
 name = "zeroize"
-version = "1.6.0"
+version = "1.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9"
+checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d"
diff --git a/nearby/connections/ukey2/ukey2_c_ffi/cpp/CMakeLists.txt b/nearby/connections/ukey2/ukey2_c_ffi/cpp/CMakeLists.txt
index a8b7b9d..61f19ee 100644
--- a/nearby/connections/ukey2/ukey2_c_ffi/cpp/CMakeLists.txt
+++ b/nearby/connections/ukey2/ukey2_c_ffi/cpp/CMakeLists.txt
@@ -14,14 +14,8 @@
 
 cmake_minimum_required(VERSION 3.14)
 
-project(Ukey2)
-
-enable_testing()
-
 include_directories(
     ${CMAKE_SOURCE_DIR}/ukey2_c_ffi/cpp/)
-
-include(GoogleTest)
 include(ExternalProject)
 
 set_directory_properties(PROPERTIES EP_PREFIX ${CMAKE_BINARY_DIR}/target/tmp)
@@ -30,30 +24,10 @@
     DOWNLOAD_COMMAND ""
     CONFIGURE_COMMAND ""
     BUILD_COMMAND cargo build COMMAND cargo build --release --lib
-    BINARY_DIR "${CMAKE_SOURCE_DIR}/ukey2_c_ffi"
+    BINARY_DIR "${NEARBY_ROOT}/connections/ukey2/ukey2_c_ffi"
     INSTALL_COMMAND "")
 
-# required for designated initializers on MSVC
-set(CMAKE_CXX_STANDARD 20)
-
-if(UNIX)
-    add_compile_options(-Wall -Wextra -Wimplicit-fallthrough -Wextra-semi
-            -Wno-missing-field-initializers -Wno-unused-parameter -Wno-psabi
-            -Wshadow
-            -Wsign-compare)
-elseif(MSVC)
-    add_compile_options(-W4 -MD)
-endif()
-
-include(FetchContent)
-FetchContent_Declare(
-  googletest
-  GIT_REPOSITORY https://github.com/google/googletest.git
-  GIT_TAG release-1.12.1
-)
-FetchContent_MakeAvailable(googletest)
-
-add_executable(ffi_test
+add_executable(ukey2_ffi_test
   ukey2_test.cc
   ukey2_glue.cc
   ukey2_ffi.h
@@ -63,24 +37,24 @@
 
 if(UNIX)
     target_link_libraries(
-        ffi_test
-        "${CMAKE_SOURCE_DIR}/../../../../target/release/${ukey2_c_ffi_FILENAME}"
+        ukey2_ffi_test
+        "${NEARBY_ROOT}/target/release/${ukey2_c_ffi_FILENAME}"
         GTest::gtest_main
         dl pthread
     )
 elseif(MSVC)
     # MSVC requires linking to a static lib, which rust kindly generates
     target_link_libraries(
-        ffi_test
-        "${CMAKE_SOURCE_DIR}/../../../../target/release/${ukey2_c_ffi_FILENAME}${CMAKE_STATIC_LIBRARY_SUFFIX}"
+        ukey2_ffi_test
+        "${NEARBY_ROOT}/target/release/${ukey2_c_ffi_FILENAME}${CMAKE_STATIC_LIBRARY_SUFFIX}"
         GTest::gtest_main
     )
     # MSVC requires that the dynamic lib be visible to the binary, and copying to the binary dir is an easy way to do that
-    get_target_property(ffi_test_BINARY_DIR ffi_test BINARY_DIR)
+    get_target_property(ukey2_ffi_test_BINARY_DIR ukey2_ffi_test BINARY_DIR)
     add_custom_command(
-        TARGET ffi_test POST_BUILD
-        COMMAND "${CMAKE_COMMAND}" -E copy "${CMAKE_SOURCE_DIR}/../../../../target/release/${ukey2_c_ffi_FILENAME}" "${ffi_test_BINARY_DIR}/."
+        TARGET ukey2_ffi_test POST_BUILD
+        COMMAND "${CMAKE_COMMAND}" -E copy "${NEARBY_ROOT}/target/release/${ukey2_c_ffi_FILENAME}" "${ukey2_ffi_test_BINARY_DIR}/."
     )
 endif()
 
-gtest_discover_tests(ffi_test)
+gtest_discover_tests(ukey2_ffi_test)
diff --git a/nearby/presence/ldt_np_adv_ffi/c/CMakeLists.txt b/nearby/presence/ldt_np_adv_ffi/c/CMakeLists.txt
index 476b228..db16cc9 100644
--- a/nearby/presence/ldt_np_adv_ffi/c/CMakeLists.txt
+++ b/nearby/presence/ldt_np_adv_ffi/c/CMakeLists.txt
@@ -32,7 +32,7 @@
         BINARY_DIR "${NEARBY_ROOT}/target/"
         INSTALL_COMMAND "")
 
-include_directories(${CMAKE_SOURCE_DIR}/ldt_np_adv_ffi/c/include/)
+include_directories(${NEARBY_ROOT}/presence/ldt_np_adv_ffi/c/include/)
 
 add_subdirectory(sample)
 
diff --git a/nearby/presence/ldt_np_adv_ffi/c/tests/CMakeLists.txt b/nearby/presence/ldt_np_adv_ffi/c/tests/CMakeLists.txt
index a4cbc24..8bd42b7 100644
--- a/nearby/presence/ldt_np_adv_ffi/c/tests/CMakeLists.txt
+++ b/nearby/presence/ldt_np_adv_ffi/c/tests/CMakeLists.txt
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-file(COPY ${CMAKE_SOURCE_DIR}/ldt_np_adv/resources/test/np_adv_test_vectors.json
+file(COPY ${NEARBY_ROOT}/presence/ldt_np_adv/resources/test/np_adv_test_vectors.json
      DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
 
 add_executable(
diff --git a/nearby/presence/np_adv/Cargo.toml b/nearby/presence/np_adv/Cargo.toml
index 5a72ecd..6670192 100644
--- a/nearby/presence/np_adv/Cargo.toml
+++ b/nearby/presence/np_adv/Cargo.toml
@@ -23,7 +23,7 @@
 tinyvec.workspace = true
 
 [features]
-default = []
+default = ["alloc"]
 devtools = []
 testing = []
 alloc = ["crypto_provider/alloc"]
diff --git a/nearby/presence/np_c_ffi/include/c/np_c_ffi.h b/nearby/presence/np_c_ffi/include/c/np_c_ffi.h
index 0eb9ecd..d84a49a 100644
--- a/nearby/presence/np_c_ffi/include/c/np_c_ffi.h
+++ b/nearby/presence/np_c_ffi/include/c/np_c_ffi.h
@@ -1179,14 +1179,6 @@
  */
 typedef struct {
   uint64_t handle_id;
-} np_ffi_V0AdvertisementBuilderHandle;
-
-/**
- * A handle to a builder for V0 advertisements.
- */
-typedef struct {
-  np_ffi_AdvertisementBuilderKind kind;
-  np_ffi_V0AdvertisementBuilderHandle handle;
 } np_ffi_V0AdvertisementBuilder;
 
 /**
diff --git a/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h b/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h
index ba3ed34..0206f80 100644
--- a/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h
+++ b/nearby/presence/np_c_ffi/include/cpp/np_cpp_ffi_types.h
@@ -869,14 +869,8 @@
 };
 
 /// A `#[repr(C)]` handle to a value of type `V0AdvertisementBuilderInternals`
-struct V0AdvertisementBuilderHandle {
-  uint64_t handle_id;
-};
-
-/// A handle to a builder for V0 advertisements.
 struct V0AdvertisementBuilder {
-  AdvertisementBuilderKind kind;
-  V0AdvertisementBuilderHandle handle;
+  uint64_t handle_id;
 };
 
 /// The result of attempting to serialize the contents
diff --git a/nearby/presence/np_c_ffi/src/serialize/v0.rs b/nearby/presence/np_c_ffi/src/serialize/v0.rs
index 867c33b..e1d99ea 100644
--- a/nearby/presence/np_c_ffi/src/serialize/v0.rs
+++ b/nearby/presence/np_c_ffi/src/serialize/v0.rs
@@ -52,7 +52,7 @@
 pub extern "C" fn np_ffi_deallocate_v0_advertisement_builder(
     adv_builder: V0AdvertisementBuilder,
 ) -> DeallocateResult {
-    adv_builder.deallocate()
+    adv_builder.deallocate_handle()
 }
 
 /// Creates a new V0 advertisement builder for a public advertisement.
diff --git a/nearby/presence/np_cpp_ffi/CMakeLists.txt b/nearby/presence/np_cpp_ffi/CMakeLists.txt
index 7071bbd..b2bef4a 100644
--- a/nearby/presence/np_cpp_ffi/CMakeLists.txt
+++ b/nearby/presence/np_cpp_ffi/CMakeLists.txt
@@ -20,10 +20,10 @@
 
 target_include_directories(nearby_protocol PUBLIC
         include/
-        ${CMAKE_SOURCE_DIR}/np_c_ffi/include/cpp/)
+        ${NEARBY_ROOT}/presence/np_c_ffi/include/cpp/)
 
 target_include_directories(nearby_protocol PRIVATE
-        ${CMAKE_SOURCE_DIR}/np_c_ffi/include/cpp/internal/)
+        ${NEARBY_ROOT}/presence/np_c_ffi/include/cpp/internal/)
 
 add_subdirectory(sample)
 
diff --git a/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_deserialization_tests.cc b/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_deserialization_tests.cc
index 42819d6..59d6b69 100644
--- a/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_deserialization_tests.cc
+++ b/nearby/presence/np_cpp_ffi/tests/v0_unencrypted_deserialization_tests.cc
@@ -135,7 +135,7 @@
 
   ASSERT_EQ(de.GetKind(), nearby_protocol::V0DataElementKind::Actions);
   auto actions = de.AsActions();
-  ASSERT_EQ(actions.GetAsU32(), 0x40400000);
+  ASSERT_EQ(actions.GetAsU32(), 0x40400000U);
 
   ASSERT_TRUE(actions.HasAction(nearby_protocol::ActionType::CrossDevSdk));
   ASSERT_TRUE(actions.HasAction(nearby_protocol::ActionType::NearbyShare));
diff --git a/nearby/presence/np_ffi_core/src/common.rs b/nearby/presence/np_ffi_core/src/common.rs
index 592e1cd..da7cb5c 100644
--- a/nearby/presence/np_ffi_core/src/common.rs
+++ b/nearby/presence/np_ffi_core/src/common.rs
@@ -208,6 +208,12 @@
     }
 }
 
+impl<const N: usize> From<[u8; N]> for FixedSizeArray<N> {
+    fn from(arr: [u8; N]) -> Self {
+        Self(arr)
+    }
+}
+
 impl<const N: usize> ByteBuffer<N> {
     /// Constructs a byte-buffer from a Rust-side-derived
     /// ArrayView, which is assumed to be trusted to be
diff --git a/nearby/presence/np_ffi_core/src/deserialize.rs b/nearby/presence/np_ffi_core/src/deserialize.rs
index dffa8d1..b5e9c91 100644
--- a/nearby/presence/np_ffi_core/src/deserialize.rs
+++ b/nearby/presence/np_ffi_core/src/deserialize.rs
@@ -284,6 +284,12 @@
         }
     }
 
+    /// Unwraps the buffer of decrypted bytes. This is for Rust usage. This takes ownership of the
+    /// handle and deallocates it.
+    pub fn take_buffer(self) -> Result<Box<[u8]>, HandleNotPresentError> {
+        self.deallocate().map(|internals| internals.decrypted_bytes)
+    }
+
     /// Frees the underlying decrypted metadata buffer. This takes ownership of the handle.
     pub fn deallocate_metadata(self) -> DeallocateResult {
         self.deallocate().map(|_| ()).into()
diff --git a/nearby/presence/np_ffi_core/src/deserialize/v1.rs b/nearby/presence/np_ffi_core/src/deserialize/v1.rs
index af54fec..6007975 100644
--- a/nearby/presence/np_ffi_core/src/deserialize/v1.rs
+++ b/nearby/presence/np_ffi_core/src/deserialize/v1.rs
@@ -209,6 +209,36 @@
         };
         section.get_de(de_index)
     }
+
+    /// Gets identity details for the legible section at the given index. Similar to
+    /// `get_section().get_identity_details()` but will only lock the HandleMap once. This function
+    /// uses this handle but does not take ownership of it.
+    pub fn get_section_identity_details(
+        &self,
+        legible_section_index: u8,
+    ) -> GetV1IdentityDetailsResult {
+        let Ok(sections) = self.get() else {
+            return GetV1IdentityDetailsResult::Error;
+        };
+        let Some(section) = sections.get_section_internals(legible_section_index) else {
+            return GetV1IdentityDetailsResult::Error;
+        };
+        section.get_identity_details()
+    }
+
+    /// Decrypts metadata for the legible section at the given index. Similar to
+    /// `get_section().decrypt_metadata()` but will only lock the HandleMap once. This function
+    /// uses this handle but does not take ownership of it. The caller is given owenership of the
+    /// handle in the result if present.
+    pub fn decrypt_section_metadata(&self, legible_section_index: u8) -> DecryptMetadataResult {
+        let Ok(sections) = self.get() else {
+            return DecryptMetadataResult::Error;
+        };
+        let Some(section) = sections.get_section_internals(legible_section_index) else {
+            return DecryptMetadataResult::Error;
+        };
+        section.decrypt_metadata()
+    }
 }
 
 /// Discriminant for `GetV1SectionResult`
diff --git a/nearby/presence/np_ffi_core/src/serialize/v0.rs b/nearby/presence/np_ffi_core/src/serialize/v0.rs
index 736925b..e1b8103 100644
--- a/nearby/presence/np_ffi_core/src/serialize/v0.rs
+++ b/nearby/presence/np_ffi_core/src/serialize/v0.rs
@@ -15,31 +15,31 @@
 
 use crate::common::*;
 use crate::credentials::V0BroadcastCredential;
-use crate::serialize::AdvertisementBuilderKind;
 use crate::utils::FfiEnum;
 use crate::v0::V0DataElement;
 use crypto_provider_default::CryptoProviderImpl;
 use handle_map::{declare_handle_map, HandleLike, HandleMapFullError};
 use np_adv_dynamic::legacy::BoxedAdvConstructionError;
 
-/// A handle to a builder for V0 advertisements.
-#[derive(Clone, Copy)]
+/// A `#[repr(C)]` handle to a value of type `V0AdvertisementBuilderInternals`
 #[repr(C)]
+#[derive(Clone, Copy, PartialEq, Eq)]
 pub struct V0AdvertisementBuilder {
-    kind: AdvertisementBuilderKind,
-    handle: V0AdvertisementBuilderHandle,
+    handle_id: u64,
 }
 
-impl V0AdvertisementBuilder {
-    /// Gets the kind of advertisement builder (public/encrypted).
-    pub fn kind(&self) -> AdvertisementBuilderKind {
-        self.kind
-    }
+declare_handle_map!(
+    advertisement_builder,
+    crate::common::default_handle_map_dimensions(),
+    super::V0AdvertisementBuilder,
+    super::V0AdvertisementBuilderInternals
+);
 
+impl V0AdvertisementBuilder {
     /// Attempts to add the given data element to the V0 advertisement builder behind this handle.
     /// This function does not take ownership of the handle.
     pub fn add_de(&self, de: V0DataElement) -> Result<AddV0DEResult, InvalidStackDataStructure> {
-        match self.handle.get_mut() {
+        match self.get_mut() {
             Ok(mut adv_builder_write_guard) => adv_builder_write_guard.add_de(de),
             Err(_) => Ok(AddV0DEResult::InvalidAdvertisementBuilderHandle),
         }
@@ -47,15 +47,15 @@
     /// Attempts to serialize the contents of the advertisement builder behind this handle to
     /// bytes. This function takes ownership of the handle.
     pub fn into_advertisement(self) -> SerializeV0AdvertisementResult {
-        match self.handle.deallocate() {
+        match self.deallocate() {
             Ok(adv_builder) => adv_builder.into_advertisement(),
             Err(_) => SerializeV0AdvertisementResult::InvalidAdvertisementBuilderHandle,
         }
     }
     /// Attempts to deallocate the V0 advertisement builder behind this handle. This function takes
     /// ownership of the handle.
-    pub fn deallocate(self) -> DeallocateResult {
-        self.handle.deallocate().map(|_| ()).into()
+    pub fn deallocate_handle(self) -> DeallocateResult {
+        self.deallocate().map(|_| ()).into()
     }
 }
 
@@ -113,9 +113,7 @@
 /// Creates a new V0 advertisement builder for a public advertisement. The caller is given
 /// ownership of the created handle.
 pub fn create_v0_public_advertisement_builder() -> CreateV0AdvertisementBuilderResult {
-    V0AdvertisementBuilderHandle::allocate(V0AdvertisementBuilderInternals::new_public)
-        .map(|handle| V0AdvertisementBuilder { kind: AdvertisementBuilderKind::Public, handle })
-        .into()
+    V0AdvertisementBuilder::allocate(V0AdvertisementBuilderInternals::new_public).into()
 }
 
 /// Creates a new V0 advertisement builder for an encrypted advertisement. The caller is given
@@ -124,10 +122,9 @@
     broadcast_cred: V0BroadcastCredential,
     salt: FixedSizeArray<2>,
 ) -> CreateV0AdvertisementBuilderResult {
-    V0AdvertisementBuilderHandle::allocate(move || {
+    V0AdvertisementBuilder::allocate(move || {
         V0AdvertisementBuilderInternals::new_ldt(broadcast_cred, salt.into_array())
     })
-    .map(|handle| V0AdvertisementBuilder { kind: AdvertisementBuilderKind::Encrypted, handle })
     .into()
 }
 
@@ -186,6 +183,7 @@
     pub(crate) fn new_public() -> Self {
         Self::new(np_adv::legacy::serialize::UnencryptedEncoder.into())
     }
+
     pub(crate) fn new_ldt(broadcast_cred: V0BroadcastCredential, salt: [u8; 2]) -> Self {
         // TODO: What do about salts? Need to prevent re-use fo the same salt,
         // but have no current rich representation of used salts...
@@ -194,11 +192,13 @@
         let identity = np_adv::legacy::serialize::LdtEncoder::new(salt, &internal_broadcast_cred);
         Self::new(identity.into())
     }
+
     fn new(encoder: np_adv_dynamic::legacy::BoxedEncoder<CryptoProviderImpl>) -> Self {
         let adv_builder =
             np_adv_dynamic::legacy::BoxedAdvBuilder::<CryptoProviderImpl>::new(encoder);
         Self { adv_builder }
     }
+
     fn add_de(&mut self, de: V0DataElement) -> Result<AddV0DEResult, InvalidStackDataStructure> {
         let to_boxed = np_adv_dynamic::legacy::ToBoxedSerializeDataElement::try_from(de)?;
         use np_adv::legacy::serialize::AddDataElementError;
@@ -213,6 +213,7 @@
             )) => Ok(AddV0DEResult::InsufficientAdvertisementSpace),
         }
     }
+
     fn into_advertisement(self) -> SerializeV0AdvertisementResult {
         self.adv_builder
             .into_advertisement()
@@ -230,20 +231,6 @@
     }
 }
 
-/// A `#[repr(C)]` handle to a value of type `V0AdvertisementBuilderInternals`
-#[repr(C)]
-#[derive(Clone, Copy, PartialEq, Eq)]
-struct V0AdvertisementBuilderHandle {
-    handle_id: u64,
-}
-
-declare_handle_map!(
-    advertisement_builder,
-    crate::common::default_handle_map_dimensions(),
-    super::V0AdvertisementBuilderHandle,
-    super::V0AdvertisementBuilderInternals
-);
-
 /// Result code for the operation of adding a DE to a V0
 /// advertisement builder.
 #[derive(Clone, Copy)]
diff --git a/nearby/presence/np_java_ffi/build.gradle.kts b/nearby/presence/np_java_ffi/build.gradle.kts
index 69d73c4..c502cdb 100644
--- a/nearby/presence/np_java_ffi/build.gradle.kts
+++ b/nearby/presence/np_java_ffi/build.gradle.kts
@@ -50,5 +50,10 @@
 
 tasks.test {
   useJUnitPlatform()
-  jvmArgs = mutableListOf("-Djava.library.path=$projectDir/../../target/debug")
+  jvmArgs = mutableListOf(
+      // libnp_java_ffi.so
+      "-Djava.library.path=$projectDir/../../target/debug",
+      // ByteBuddy agent for mocks
+      "-XX:+EnableDynamicAgentLoading"
+  )
 }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializeResult.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializeResult.java
index 865df41..c0dc922 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializeResult.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializeResult.java
@@ -20,6 +20,7 @@
 
 import androidx.annotation.IntDef;
 import androidx.annotation.Nullable;
+import com.google.android.nearby.presence.rust.credential.CredentialBook;
 import java.lang.annotation.Retention;
 
 /**
@@ -30,7 +31,8 @@
  * reasons. That condition is reported on the advertisement object itself since a V1 advertisement
  * may be partially legible (some sections are legible, not all).
  */
-public final class DeserializeResult implements AutoCloseable {
+public final class DeserializeResult<M extends CredentialBook.MatchedMetadata>
+    implements AutoCloseable {
 
   @IntDef({
     Kind.UNKNOWN_ERROR,
@@ -92,11 +94,11 @@
    * @return the contained V0 advertisement or {@code null} if not present
    */
   @Nullable
-  public DeserializedV0Advertisement getAsV0() {
+  public DeserializedV0Advertisement<M> getAsV0() {
     if (this.kind != Kind.V0_ADVERTISEMENT) {
       return null;
     }
-    return (DeserializedV0Advertisement) this.advertisement;
+    return (DeserializedV0Advertisement<M>) this.advertisement;
   }
 
   /**
@@ -105,7 +107,7 @@
    * @return the contained V1 advertisement or {@code null} if not present
    */
   @Nullable
-  public DeserializedV1Advertisement getAsV1() {
+  public DeserializedV1Advertisement<M> getAsV1() {
     if (this.kind != Kind.V1_ADVERTISEMENT) {
       return null;
     }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV0Advertisement.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV0Advertisement.java
index e4a0688..cb557c5 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV0Advertisement.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV0Advertisement.java
@@ -17,6 +17,7 @@
 package com.google.android.nearby.presence.rust;
 
 import androidx.annotation.Nullable;
+import com.google.android.nearby.presence.rust.credential.CredentialBook;
 import java.util.Iterator;
 
 /**
@@ -24,8 +25,8 @@
  * handle. If this class is closed then the underlying handle will be closed too. Methods on this
  * class should not be called if {@link #close()} has already been called.
  */
-public final class DeserializedV0Advertisement extends DeserializedAdvertisement {
-
+public final class DeserializedV0Advertisement<M extends CredentialBook.MatchedMetadata>
+    extends DeserializedAdvertisement {
   public static boolean isLegibleIdentity(@IdentityKind int identity) {
     return identity > 0;
   }
@@ -33,6 +34,7 @@
   private final int numDataElements;
   private final @Nullable V0Payload payload;
   private final @IdentityKind int identity;
+  private final CredentialBook<M> credentialBook;
 
   /** Create an illegible instance with the given error identity. */
   /* package */ DeserializedV0Advertisement(@IdentityKind int illegibleIdentity) {
@@ -43,14 +45,19 @@
     this.numDataElements = 0;
     this.payload = null;
     this.identity = illegibleIdentity;
+    this.credentialBook = null;
   }
 
   /** Create a legible instance with the given information. */
   /* package */ DeserializedV0Advertisement(
-      int numDataElements, V0Payload payload, @IdentityKind int identity) {
+      int numDataElements,
+      V0Payload payload,
+      @IdentityKind int identity,
+      CredentialBook<M> credentialBook) {
     this.numDataElements = numDataElements;
     this.payload = payload;
     this.identity = identity;
+    this.credentialBook = credentialBook;
   }
 
   /**
@@ -59,8 +66,11 @@
    * on the native side.
    */
   /* package */ DeserializedV0Advertisement(
-      int numDataElements, long payload, @IdentityKind int identity) {
-    this(numDataElements, new V0Payload(payload), identity);
+      int numDataElements,
+      long payload,
+      @IdentityKind int identity,
+      CredentialBook<M> credentialBook) {
+    this(numDataElements, new V0Payload(payload), identity, credentialBook);
   }
 
   /** Check if this advertisement is legible */
@@ -122,6 +132,56 @@
     }
   }
 
+  /**
+   * Gets the identity token for the credential this advertisement was deserialized with. This will
+   * return {@code null} if this advertisement is not an encrypted advertisement.
+   */
+  @Nullable
+  public byte[] getIdentityToken() {
+    ensureLegible("get identity token");
+
+    V0Payload.IdentityDetails details = payload.getIdentityDetails();
+    return (details != null) ? details.getIdentityToken() : null;
+  }
+
+  /**
+   * Gets the salt this advertisement was encrypted with. This will return {@code null} if this
+   * advertisement is not an encrypted advertisement.
+   */
+  @Nullable
+  public byte[] getSalt() {
+    ensureLegible("get salt");
+
+    V0Payload.IdentityDetails details = payload.getIdentityDetails();
+    return (details != null) ? details.getSalt() : null;
+  }
+
+  /**
+   * Gets the metadata matched to the credential this advertisement was deserialized with. This will
+   * return {@code null} if this advertisement is not an encrypted advertisement.
+   */
+  @Nullable
+  public M getMatchedMetadata() {
+    ensureLegible("get matched metadata");
+
+    V0Payload.IdentityDetails details = payload.getIdentityDetails();
+    if (details == null) {
+      return null;
+    }
+
+    return credentialBook.getMatchedMetadata(details.getCredentialId());
+  }
+
+  /**
+   * Gets the metadata matched to the credential this advertisement was deserialized with. This will
+   * return {@code null} if this advertisement is not an encrypted advertisement or the metadata was
+   * not able to be decrypted.
+   */
+  @Nullable
+  public byte[] getDecryptedMetadata() {
+    return payload.getDecryptedMetadata();
+  }
+
   /** Iterator instance for data elements in DeserializedV0Advertisement. */
   private static final class DataElementIterator implements Iterator<V0DataElement> {
     private final V0Payload payload;
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Advertisement.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Advertisement.java
index d1ecdcd..8f4c3a3 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Advertisement.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Advertisement.java
@@ -16,6 +16,7 @@
 
 package com.google.android.nearby.presence.rust;
 
+import com.google.android.nearby.presence.rust.credential.CredentialBook;
 import java.util.Iterator;
 
 /**
@@ -23,18 +24,24 @@
  * LegibleV1Sections} handle. If this class is closed then the underlying handle will be closed too.
  * Methods on this class should not be called if {@link #close()} has already been called.
  */
-public final class DeserializedV1Advertisement extends DeserializedAdvertisement {
+public final class DeserializedV1Advertisement<M extends CredentialBook.MatchedMetadata>
+    extends DeserializedAdvertisement {
 
   private final int numLegibleSections;
   private final int numUndecryptableSections;
   private final LegibleV1Sections legibleSections;
+  private final CredentialBook<M> credentialBook;
 
   /** Create a legible instance with the given information. */
   /* package */ DeserializedV1Advertisement(
-      int numLegibleSections, int numUndecryptableSections, LegibleV1Sections legibleSections) {
+      int numLegibleSections,
+      int numUndecryptableSections,
+      LegibleV1Sections legibleSections,
+      CredentialBook<M> credentialBook) {
     this.numLegibleSections = numLegibleSections;
     this.numUndecryptableSections = numUndecryptableSections;
     this.legibleSections = legibleSections;
+    this.credentialBook = credentialBook;
   }
 
   /** Get the number of legible sections in this advertisement */
@@ -55,25 +62,29 @@
    * @throws IndexOutOfBoundsException if the index is invalid
    * @return The section at {@code index}
    */
-  public DeserializedV1Section getSection(int index) {
-    return legibleSections.getSection(index);
+  public DeserializedV1Section<M> getSection(int index) {
+    return legibleSections.getSection(index, credentialBook);
   }
 
   /** Get an iterable of this advertisement's legible sections. */
   public Iterable<DeserializedV1Section> getSections() {
-    return () -> new SectionIterator(numLegibleSections, legibleSections);
+    return () -> new SectionIterator(numLegibleSections, legibleSections, credentialBook);
   }
 
   /** Iterator instance for sections in DeserializedV1Advertisement. */
-  private static final class SectionIterator implements Iterator<DeserializedV1Section> {
+  private static final class SectionIterator<M extends CredentialBook.MatchedMetadata>
+      implements Iterator<DeserializedV1Section> {
     private final LegibleV1Sections legibleSections;
     private final int numSections;
+    private final CredentialBook<M> credentialBook;
 
     private int position = 0;
 
-    public SectionIterator(int numSections, LegibleV1Sections legibleSections) {
+    public SectionIterator(
+        int numSections, LegibleV1Sections legibleSections, CredentialBook<M> credentialBook) {
       this.numSections = numSections;
       this.legibleSections = legibleSections;
+      this.credentialBook = credentialBook;
     }
 
     @Override
@@ -83,7 +94,7 @@
 
     @Override
     public DeserializedV1Section next() {
-      return legibleSections.getSection(position++);
+      return legibleSections.getSection(position++, credentialBook);
     }
   }
 
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Section.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Section.java
index 1667366..d320963 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Section.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/DeserializedV1Section.java
@@ -16,23 +16,40 @@
 
 package com.google.android.nearby.presence.rust;
 
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.Nullable;
+import com.google.android.nearby.presence.rust.credential.CredentialBook;
+import java.lang.annotation.Retention;
 import java.util.Iterator;
 
-public final class DeserializedV1Section {
+public final class DeserializedV1Section<M extends CredentialBook.MatchedMetadata> {
+
+  @IntDef({VerificationMode.MIC, VerificationMode.SIGNATURE})
+  @Retention(SOURCE)
+  public @interface VerificationMode {
+    public static final int MIC = 0;
+    public static final int SIGNATURE = 1;
+  }
+
   private final LegibleV1Sections legibleSections;
   private final int legibleSectionsIndex;
   private final int numDataElements;
   private final @IdentityKind int identityTag;
+  private final CredentialBook<M> credentialBook;
 
   /* package */ DeserializedV1Section(
       LegibleV1Sections legibleSections,
       int legibleSectionsIndex,
       int numDataElements,
-      @IdentityKind int identityTag) {
+      @IdentityKind int identityTag,
+      CredentialBook<M> credentialBook) {
     this.legibleSections = legibleSections;
     this.legibleSectionsIndex = legibleSectionsIndex;
     this.numDataElements = numDataElements;
     this.identityTag = identityTag;
+    this.credentialBook = credentialBook;
   }
 
   /** Gets the identity kind for this section. */
@@ -68,6 +85,53 @@
     }
   }
 
+  /**
+   * Gets the identity token for the credential this advertisement was deserialized with. This will
+   * return {@code null} if this advertisement is not an encrypted advertisement.
+   */
+  @Nullable
+  public byte[] getIdentityToken() {
+    LegibleV1Sections.IdentityDetails details =
+        legibleSections.getSectionIdentityDetails(legibleSectionsIndex);
+    return (details != null) ? details.getIdentityToken() : null;
+  }
+
+  /**
+   * Gets the verification mode for this advertisement. This will return {@code null} if this
+   * advertisement is not an encrypted advertisement.
+   */
+  @Nullable
+  @VerificationMode
+  public Integer getVerificationMode() {
+    LegibleV1Sections.IdentityDetails details =
+        legibleSections.getSectionIdentityDetails(legibleSectionsIndex);
+    return (details != null) ? details.getVerificationMode() : null;
+  }
+
+  /**
+   * Gets the metadata matched to the credential this advertisement was deserialized with. This will
+   * return {@code null} if this advertisement is not an encrypted advertisement.
+   */
+  @Nullable
+  public M getMatchedMetadata() {
+    LegibleV1Sections.IdentityDetails details =
+        legibleSections.getSectionIdentityDetails(legibleSectionsIndex);
+    if (details == null) {
+      return null;
+    }
+    return credentialBook.getMatchedMetadata(details.getCredentialId());
+  }
+
+  /**
+   * Gets the metadata matched to the credential this advertisement was deserialized with. This will
+   * return {@code null} if this advertisement is not an encrypted advertisement or the metadata was
+   * not able to be decrypted.
+   */
+  @Nullable
+  public byte[] getDecryptedMetadata() {
+    return legibleSections.getSectionDecryptedMetadata(legibleSectionsIndex);
+  }
+
   private static final class DataElementIterator implements Iterator<V1DataElement> {
     private final LegibleV1Sections legibleSections;
     private final int legibleSectionsIndex;
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/Handle.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/Handle.java
index a200358..537098b 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/Handle.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/Handle.java
@@ -27,7 +27,7 @@
 public abstract class Handle {
 
   /** Thrown when an invalid handle is used */
-  public static final class InvalidHandleException extends Exception {
+  public static final class InvalidHandleException extends RuntimeException {
     public InvalidHandleException() {
       super("The given handle is no longer valid");
     }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/LegibleV1Sections.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/LegibleV1Sections.java
index 5175ce6..4a089c6 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/LegibleV1Sections.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/LegibleV1Sections.java
@@ -16,8 +16,12 @@
 
 package com.google.android.nearby.presence.rust;
 
+import static com.google.android.nearby.presence.rust.DeserializedV1Section.VerificationMode;
+
 import androidx.annotation.Nullable;
+import com.google.android.nearby.presence.rust.credential.CredentialBook;
 import java.lang.ref.Cleaner;
+import java.util.Arrays;
 
 /** Internal handle for a V1 deserialized advertisement. */
 public final class LegibleV1Sections extends OwnedHandle {
@@ -26,6 +30,38 @@
     System.loadLibrary(NpAdv.LIBRARY_NAME);
   }
 
+  /** Internal data class to pass identity data back from native code. */
+  public static final class IdentityDetails {
+    @VerificationMode private final int verificationMode;
+
+    /**
+     * @see com.google.android.nearby.presence.rust.credential.CredentialBook.Builder
+     */
+    private final int credentialId;
+
+    private final byte[] identityToken;
+
+    public IdentityDetails(
+        @VerificationMode int verificationMode, int credentialId, byte[] identityToken) {
+      this.verificationMode = verificationMode;
+      this.credentialId = credentialId;
+      this.identityToken = Arrays.copyOf(identityToken, identityToken.length);
+    }
+
+    @VerificationMode
+    public int getVerificationMode() {
+      return verificationMode;
+    }
+
+    public int getCredentialId() {
+      return credentialId;
+    }
+
+    public byte[] getIdentityToken() {
+      return identityToken;
+    }
+  }
+
   /**
    * Create a LegibleV1Sections handle from the raw handle id. This will use the default cleaner
    * form {@code NpAdv#getCleaner()}. This is expected to be called from native code.
@@ -46,12 +82,13 @@
    * @throws IndexOutOfBoundsException if the given index is out of range for this advertisement
    * @return The section at that index
    */
-  public DeserializedV1Section getSection(int index) {
-    DeserializedV1Section section = nativeGetSection(index);
+  public <M extends CredentialBook.MatchedMetadata> DeserializedV1Section<M> getSection(
+      int index, CredentialBook<M> credentialBook) {
+    DeserializedV1Section section = nativeGetSection(index, credentialBook);
     if (section == null) {
       throw new IndexOutOfBoundsException();
     }
-    return section;
+    return (DeserializedV1Section<M>) section;
   }
 
   /**
@@ -71,10 +108,26 @@
   }
 
   @Nullable
-  private native DeserializedV1Section nativeGetSection(int index);
+  public IdentityDetails getSectionIdentityDetails(int sectionIndex) {
+    return nativeGetSectionIdentityDetails(sectionIndex);
+  }
+
+  @Nullable
+  public byte[] getSectionDecryptedMetadata(int sectionIndex) {
+    return nativeGetSectionDecryptedMetadata(sectionIndex);
+  }
+
+  @Nullable
+  private native DeserializedV1Section nativeGetSection(int index, CredentialBook credentialBook);
 
   @Nullable
   private native V1DataElement nativeGetSectionDataElement(int sectionIndex, int deIndex);
 
+  @Nullable
+  private native IdentityDetails nativeGetSectionIdentityDetails(int sectionIndex);
+
+  @Nullable
+  private native byte[] nativeGetSectionDecryptedMetadata(int sectionIndex);
+
   private static native void deallocate(long handleId);
 }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/NpAdv.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/NpAdv.java
index 21abb89..a349ebc 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/NpAdv.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/NpAdv.java
@@ -29,7 +29,10 @@
  * <h3>Supported Features:</h3>
  *
  * <ul>
+ *   <li>Create credential books: {@link CredentialBook#builder()}
  *   <li>Deserialize advertisements: {@link #deserializeAdvertisement}
+ *   <li>Serialize advertisements: {@link
+ *       com.google.android.nearby.presence.rust.V0AdvertisementBuilder}
  * </ul>
  */
 public final class NpAdv {
@@ -50,9 +53,9 @@
    * @return A result containing the advertisement if it was able to be deserialized.
    */
   public static <M extends CredentialBook.MatchedMetadata>
-      DeserializeResult deserializeAdvertisement(
+      DeserializeResult<M> deserializeAdvertisement(
           byte[] serviceData, CredentialBook<M> credentialBook) {
-    DeserializeResult result = nativeDeserializeAdvertisement(serviceData, credentialBook.getId());
+    DeserializeResult result = nativeDeserializeAdvertisement(serviceData, credentialBook);
     if (result == null) {
       result = new DeserializeResult(DeserializeResult.Kind.UNKNOWN_ERROR);
     }
@@ -87,5 +90,5 @@
 
   @Nullable
   private static native DeserializeResult nativeDeserializeAdvertisement(
-      byte[] serviceData, long credentialBook);
+      byte[] serviceData, CredentialBook credentialBook);
 }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/SerializationException.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/SerializationException.java
new file mode 100644
index 0000000..4fa4f4f
--- /dev/null
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/SerializationException.java
@@ -0,0 +1,57 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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
+ *
+ *     http://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.
+ */
+
+package com.google.android.nearby.presence.rust;
+
+/** Base class for exceptions that can occur during serialization. */
+public abstract class SerializationException extends Exception {
+
+  private SerializationException(String message) {
+    super(message);
+  }
+
+  public static final class InvalidDataElementException extends RuntimeException {
+    public InvalidDataElementException(String reason) {
+      super(String.format("Data element is invalid: %s", reason));
+    }
+  }
+
+  public static final class InsufficientSpaceException extends SerializationException {
+    public InsufficientSpaceException() {
+      super("There isn't enough space remaining in the advertisement");
+    }
+  }
+
+  public static final class LdtEncryptionException extends SerializationException {
+    public LdtEncryptionException() {
+      super(
+          "Serializing the advertisement to bytes failed because the data in the advertisement"
+              + " wasn't of an appropriate size for LDT encryption to succeed.");
+    }
+  }
+
+  /**
+   * Advertisement has an invalid length. This means it's empty; the length requirement is {@code
+   * length >= 1 byte}.
+   */
+  public static final class UnencryptedSizeException extends SerializationException {
+    public UnencryptedSizeException() {
+      super(
+          "Serializing an unencrypted adv failed because the adv data didn't meet the length"
+              + " requirements.");
+    }
+  }
+}
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0AdvertisementBuilder.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0AdvertisementBuilder.java
new file mode 100644
index 0000000..e597303
--- /dev/null
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0AdvertisementBuilder.java
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * 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
+ *
+ *     http://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.
+ */
+
+package com.google.android.nearby.presence.rust;
+
+import com.google.android.nearby.presence.rust.SerializationException.InsufficientSpaceException;
+import com.google.android.nearby.presence.rust.SerializationException.InvalidDataElementException;
+import com.google.android.nearby.presence.rust.credential.V0BroadcastCredential;
+import java.lang.ref.Cleaner;
+
+/**
+ * A builder for V0 advertisements. Create a new instance with {@link #newPublic()} for a public
+ * advertisement or {@link #newEncrypted()} for an encrypted advertisement.
+ */
+public final class V0AdvertisementBuilder implements AutoCloseable {
+
+  /** Create a builder for a public advertisement. */
+  public static V0AdvertisementBuilder newPublic() {
+    return newPublic(NpAdv.getCleaner());
+  }
+
+  /** Create a builder for an encrypted advertisement. */
+  public static V0AdvertisementBuilder newEncrypted(V0BroadcastCredential credential, byte[] salt) {
+    return newEncrypted(NpAdv.getCleaner(), credential, salt);
+  }
+
+  /** Create a builder for a public advertisement with a specific Cleaner. */
+  public static V0AdvertisementBuilder newPublic(Cleaner cleaner) {
+    return new V0AdvertisementBuilder(new V0BuilderHandle(cleaner));
+  }
+
+  /** Create a builder for an encrypted advertisement with a specific Cleaner. */
+  public static V0AdvertisementBuilder newEncrypted(
+      Cleaner cleaner, V0BroadcastCredential credential, byte[] salt) {
+    return new V0AdvertisementBuilder(new V0BuilderHandle(cleaner, credential, salt));
+  }
+
+  private final V0BuilderHandle builder;
+
+  private V0AdvertisementBuilder(V0BuilderHandle builder) {
+    this.builder = builder;
+  }
+
+  /**
+   * Add a data element to the advertisement. If it cannot be added an exception will be thrown. A
+   * thrown exception will not invalidate this builder.
+   *
+   * @throws InvalidDataElementException when the given data element is not valid (e.g. TX power out
+   *     of range)
+   * @throws InsufficientSpaceException when the data element will not fit in the remaining space.
+   */
+  public void addDataElement(V0DataElement dataElement) throws InsufficientSpaceException {
+    builder.addDataElement(dataElement);
+  }
+
+  /**
+   * Build this advertisement into a byte buffer. This will consume the builder when called.
+   *
+   * @throws LdtEncryptionException when the advertisement cannot be encrypted.
+   * @throws UnencryptedSizeException when the advertisement is empty.
+   */
+  public byte[] build()
+      throws SerializationException.LdtEncryptionException,
+          SerializationException.UnencryptedSizeException {
+    return builder.build();
+  }
+
+  @Override
+  public void close() {
+    builder.close();
+  }
+
+  /** Internal builder handle object. */
+  private static final class V0BuilderHandle extends OwnedHandle {
+    static {
+      System.loadLibrary(NpAdv.LIBRARY_NAME);
+    }
+
+    public V0BuilderHandle(Cleaner cleaner) {
+      super(allocatePublic(), cleaner, V0BuilderHandle::deallocate);
+    }
+
+    public V0BuilderHandle(Cleaner cleaner, V0BroadcastCredential credential, byte[] salt) {
+      super(allocatePrivate(credential, salt), cleaner, V0BuilderHandle::deallocate);
+    }
+
+    public void addDataElement(V0DataElement dataElement) {
+      // Call the appropriate native add call based on the data element type.
+      dataElement.visit(
+          new V0DataElement.Visitor() {
+            public void visitTxPower(V0DataElement.TxPower txPower) {
+              nativeAddTxPowerDataElement(txPower);
+            }
+
+            public void visitV0Actions(V0DataElement.V0Actions v0Actions) {
+              nativeAddV0ActionsDataElement(v0Actions);
+            }
+          });
+    }
+
+    public byte[] build()
+        throws SerializationException.LdtEncryptionException,
+            SerializationException.UnencryptedSizeException {
+      // `nativeBuild` takes ownership so we leak the Java side here.
+      leak();
+      return nativeBuild();
+    }
+
+    private static native long allocatePublic();
+
+    private static native long allocatePrivate(V0BroadcastCredential credential, byte[] salt);
+
+    private native void nativeAddTxPowerDataElement(V0DataElement.TxPower txPower);
+
+    private native void nativeAddV0ActionsDataElement(V0DataElement.V0Actions v0Actions);
+
+    private native byte[] nativeBuild()
+        throws SerializationException.LdtEncryptionException,
+            SerializationException.UnencryptedSizeException;
+
+    private static native void deallocate(long handleId);
+  }
+}
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java
index 4575f5a..c2c6325 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0DataElement.java
@@ -86,7 +86,21 @@
     private final @IdentityKind int identityKind;
     private final int actionBits;
 
-    public V0Actions(@IdentityKind int identityKind, int actionBits) {
+    /**
+     * Create an actions data element.
+     *
+     * @throws IllegalArgumentException when {@code identityKind} is not {@link
+     *     IdentityKind#PLAINTEXT} or {@link IdentityKind#DECRYPTED}, or an {@code actions} value is
+     *     not a valid {@link V0ActionType}.
+     * @throws IllegalStateException when an action is not a valid action for the given identity
+     *     kind.
+     */
+    public V0Actions(@IdentityKind int identityKind, @V0ActionType int... actions) {
+      this(identityKind, nativeMergeActions(identityKind, actions));
+    }
+
+    /** Used by native code. */
+    V0Actions(@IdentityKind int identityKind, int actionBits) {
       this.identityKind = identityKind;
       this.actionBits = actionBits;
     }
@@ -109,6 +123,10 @@
       v.visitV0Actions(this);
     }
 
-    private static native boolean nativeHasAction(int identityKind, int actionBits, int action);
+    private static native boolean nativeHasAction(
+        @IdentityKind int identityKind, int actionBits, @V0ActionType int action);
+
+    private static native int nativeMergeActions(
+        @IdentityKind int identityKind, @V0ActionType int[] actions);
   }
 }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0Payload.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0Payload.java
index 722f37c..e6abea8 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0Payload.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/V0Payload.java
@@ -18,6 +18,7 @@
 
 import androidx.annotation.Nullable;
 import java.lang.ref.Cleaner;
+import java.util.Arrays;
 
 /**
  * Internal handle type for deserialized V0 advertisements. It provides access to the native data
@@ -29,6 +30,35 @@
     System.loadLibrary(NpAdv.LIBRARY_NAME);
   }
 
+  /** Internal data class to pass identity data back from native code. */
+  public static final class IdentityDetails {
+    /**
+     * @see com.google.android.nearby.presence.rust.credential.CredentialBook.Builder
+     */
+    private final int credentialId;
+
+    private final byte[] identityToken;
+    private final byte[] salt;
+
+    public IdentityDetails(int credentialId, byte[] identityToken, byte[] salt) {
+      this.credentialId = credentialId;
+      this.identityToken = Arrays.copyOf(identityToken, identityToken.length);
+      this.salt = Arrays.copyOf(salt, salt.length);
+    }
+
+    public int getCredentialId() {
+      return credentialId;
+    }
+
+    public byte[] getIdentityToken() {
+      return identityToken;
+    }
+
+    public byte[] getSalt() {
+      return salt;
+    }
+  }
+
   /**
    * Create a V0Payload handle from the raw handle id. This will use the default cleaner form {@code
    * NpAdv#getCleaner()}. This is expected to be called from native code.
@@ -58,7 +88,23 @@
   }
 
   @Nullable
+  public IdentityDetails getIdentityDetails() {
+    return nativeGetIdentityDetails(this.handleId);
+  }
+
+  @Nullable
+  public byte[] getDecryptedMetadata() {
+    return nativeGetDecryptedMetadata(this.handleId);
+  }
+
+  @Nullable
   private static native V0DataElement nativeGetDataElement(long handleId, int index);
 
+  @Nullable
+  private static native IdentityDetails nativeGetIdentityDetails(long handleId);
+
+  @Nullable
+  private static native byte[] nativeGetDecryptedMetadata(long handleId);
+
   private static native void deallocate(long handleId);
 }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialBook.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialBook.java
index cdcc50b..953c6e9 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialBook.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/CredentialBook.java
@@ -16,6 +16,7 @@
 
 package com.google.android.nearby.presence.rust.credential;
 
+import androidx.annotation.Nullable;
 import com.google.android.nearby.presence.rust.NpAdv;
 import com.google.android.nearby.presence.rust.OwnedHandle;
 import java.lang.ref.Cleaner;
@@ -57,13 +58,23 @@
     }
   }
 
-  /** Builder for {@code CredentialBook} */
+  /**
+   * Builder for {@code CredentialBook}. This manages passing credentials into the slab and tracking
+   * the associated metadata for each credential.
+   *
+   * <p>Credential ids will be assigned in order starting at {@code 0}. This allows them to be
+   * mapped to their metadata object using an array. This lookup is implemented in {@link
+   * CredentialBook#getMatchedMetadata()} for internal use. Clients of the library should use the
+   * {@code getMatchedMetadata()} method on their {@code DeserializedAdvertisement} instance.
+   */
   public static final class Builder<M extends MatchedMetadata> {
     private Cleaner cleaner;
     private CredentialSlab slab;
 
-    // Each credential should be given an id of its metadata index so that this array is an
-    // id-to-metadata map.
+    /**
+     * Each credential should be given an id of its metadata index so that this array is an
+     * id-to-metadata map.
+     */
     private ArrayList<M> matchDataList;
 
     /**
@@ -134,6 +145,18 @@
     this.matchData = matchData;
   }
 
+  /**
+   * Lookup the matched metadata given its credential id. This is meant for internal use. This will
+   * return {@code null} if the credential id is not valid.
+   */
+  @Nullable
+  public M getMatchedMetadata(int credentialId) {
+    if (credentialId < 0 || credentialId >= matchData.size()) {
+      return null;
+    }
+    return matchData.get(credentialId);
+  }
+
   private static native long allocate(long slabHandleId);
 
   private static native void deallocate(long handleId);
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/Utils.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/Utils.java
index c38bbcf..4f070e4 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/Utils.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/Utils.java
@@ -22,13 +22,13 @@
 final class Utils {
 
   /**
-   * Create a copy of a 32-byte array of key data. Will throw {@code IllegalArgumentException} if
-   * the array is not exactly 32 bytes.
+   * Create a copy of a {@code n}-byte array of data. Will throw {@code IllegalArgumentException} if
+   * the array is not exactly {@code n} bytes.
    */
-  public static byte[] copyKeyBytes(byte[] key) {
-    if (key.length != 32) {
+  public static byte[] copyBytes(byte[] key, int n) {
+    if (key.length != n) {
       throw new IllegalArgumentException(
-          String.format("Expected key length to be 32 bytes, got %s bytes", key.length));
+          String.format("Expected key length to be %s bytes, got %s bytes", n, key.length));
     }
     return Arrays.copyOf(key, key.length);
   }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0BroadcastCredential.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0BroadcastCredential.java
new file mode 100644
index 0000000..9e8474b
--- /dev/null
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0BroadcastCredential.java
@@ -0,0 +1,34 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * 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
+ *
+ *     http://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.
+ */
+
+package com.google.android.nearby.presence.rust.credential;
+
+import static com.google.android.nearby.presence.rust.credential.Utils.copyBytes;
+
+/** A V0 broadcast credential in a format that is ready to be passed to native code. */
+public final class V0BroadcastCredential {
+  private final byte[] keySeed;
+  private final byte[] identityToken;
+
+  /**
+   * Create the credential. {@code keySeed} is exactly 32 bytes. {@code identityToken} is exactly 14
+   * bytes
+   */
+  public V0BroadcastCredential(byte[] keySeed, byte[] identityToken) {
+    this.keySeed = copyBytes(keySeed, 32);
+    this.identityToken = copyBytes(identityToken, 14);
+  }
+}
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0DiscoveryCredential.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0DiscoveryCredential.java
index be8734b..522b7d8 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0DiscoveryCredential.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V0DiscoveryCredential.java
@@ -16,7 +16,7 @@
 
 package com.google.android.nearby.presence.rust.credential;
 
-import static com.google.android.nearby.presence.rust.credential.Utils.copyKeyBytes;
+import static com.google.android.nearby.presence.rust.credential.Utils.copyBytes;
 
 /** A V0 discovery credential in a format that is ready to be passed to native code. */
 public final class V0DiscoveryCredential {
@@ -25,7 +25,7 @@
 
   /** Create the credential. Each array is exactly 32 bytes. */
   public V0DiscoveryCredential(byte[] keySeed, byte[] identityTokenHmac) {
-    this.keySeed = copyKeyBytes(keySeed);
-    this.identityTokenHmac = copyKeyBytes(identityTokenHmac);
+    this.keySeed = copyBytes(keySeed, 32);
+    this.identityTokenHmac = copyBytes(identityTokenHmac, 32);
   }
 }
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1BroadcastCredential.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1BroadcastCredential.java
new file mode 100644
index 0000000..5c2547a
--- /dev/null
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1BroadcastCredential.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * 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
+ *
+ *     http://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.
+ */
+
+package com.google.android.nearby.presence.rust.credential;
+
+import static com.google.android.nearby.presence.rust.credential.Utils.copyBytes;
+
+/** A V1 broadcast credential in a format that is ready to be passed to native code. */
+public final class V1BroadcastCredential {
+  private final byte[] keySeed;
+  private final byte[] identityToken;
+  private final byte[] privateKey;
+
+  /**
+   * Create the credential. {@code keySeed} and {@code privateKey} are exactly 32 bytes. {@code
+   * identityToken} is exactly 16 bytes
+   */
+  public V1BroadcastCredential(byte[] keySeed, byte[] identityToken, byte[] privateKey) {
+    this.keySeed = copyBytes(keySeed, 32);
+    this.identityToken = copyBytes(identityToken, 16);
+    this.privateKey = copyBytes(privateKey, 32);
+  }
+}
diff --git a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1DiscoveryCredential.java b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1DiscoveryCredential.java
index c2afefa..2fbf608 100644
--- a/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1DiscoveryCredential.java
+++ b/nearby/presence/np_java_ffi/java/com/google/android/nearby/presence/rust/credential/V1DiscoveryCredential.java
@@ -16,7 +16,7 @@
 
 package com.google.android.nearby.presence.rust.credential;
 
-import static com.google.android.nearby.presence.rust.credential.Utils.copyKeyBytes;
+import static com.google.android.nearby.presence.rust.credential.Utils.copyBytes;
 
 /** A V1 discovery credential in a format that is ready to be passed to native code. */
 public final class V1DiscoveryCredential {
@@ -33,12 +33,12 @@
       byte[] expectedMicExtendedSaltIdentityTokenHmac,
       byte[] expectedSignatureIdentityTokenHmac,
       byte[] pubKey) {
-    this.keySeed = copyKeyBytes(keySeed);
+    this.keySeed = copyBytes(keySeed, 32);
     this.expectedMicShortSaltIdentityTokenHmac =
-        copyKeyBytes(expectedMicShortSaltIdentityTokenHmac);
+        copyBytes(expectedMicShortSaltIdentityTokenHmac, 32);
     this.expectedMicExtendedSaltIdentityTokenHmac =
-        copyKeyBytes(expectedMicExtendedSaltIdentityTokenHmac);
-    this.expectedSignatureIdentityTokenHmac = copyKeyBytes(expectedSignatureIdentityTokenHmac);
-    this.pubKey = copyKeyBytes(pubKey);
+        copyBytes(expectedMicExtendedSaltIdentityTokenHmac, 32);
+    this.expectedSignatureIdentityTokenHmac = copyBytes(expectedSignatureIdentityTokenHmac, 32);
+    this.pubKey = copyBytes(pubKey, 32);
   }
 }
diff --git a/nearby/presence/np_java_ffi/src/class.rs b/nearby/presence/np_java_ffi/src/class.rs
index 9c07137..ea53df7 100644
--- a/nearby/presence/np_java_ffi/src/class.rs
+++ b/nearby/presence/np_java_ffi/src/class.rs
@@ -42,21 +42,32 @@
 mod legible_v1_sections;
 mod np_adv;
 mod owned_handle;
+mod serialization_exception;
+mod v0_advertisement_builder;
+mod v0_broadcast_credential;
 mod v0_discovery_credential;
 mod v0_payload;
+mod v1_broadcast_credential;
 mod v1_discovery_credential;
 
 pub mod v0_data_element;
 pub mod v1_data_element;
 
+pub use credential_book::CredentialBook;
 pub use deserialization_exception::{InvalidFormatException, InvalidHeaderException};
 pub use deserialize_result::{DeserializeResult, DeserializeResultError};
 pub use deserialized_v0_advertisement::{DeserializedV0Advertisement, V0AdvertisementError};
 pub use deserialized_v1_advertisement::DeserializedV1Advertisement;
-pub use deserialized_v1_section::DeserializedV1Section;
+pub use deserialized_v1_section::{DeserializedV1Section, VerificationMode};
 pub use handle::InvalidHandleException;
 pub use identity_kind::IdentityKind;
 pub use legible_v1_sections::LegibleV1Sections;
 pub use owned_handle::NoSpaceLeftException;
+pub use serialization_exception::{
+    InsufficientSpaceException, InvalidDataElementException, LdtEncryptionException,
+    UnencryptedSizeException,
+};
+pub use v0_broadcast_credential::V0BroadcastCredential;
 pub use v0_discovery_credential::V0DiscoveryCredential;
+pub use v1_broadcast_credential::V1BroadcastCredential;
 pub use v1_discovery_credential::V1DiscoveryCredential;
diff --git a/nearby/presence/np_java_ffi/src/class/credential_book.rs b/nearby/presence/np_java_ffi/src/class/credential_book.rs
index ea371da..f213f8d 100644
--- a/nearby/presence/np_java_ffi/src/class/credential_book.rs
+++ b/nearby/presence/np_java_ffi/src/class/credential_book.rs
@@ -12,15 +12,46 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use jni::{objects::JClass, sys::jlong, JNIEnv};
+use jni::{
+    objects::{JClass, JObject},
+    sys::jlong,
+    JNIEnv,
+};
 
 use crate::class::{InvalidHandleException, NoSpaceLeftException};
 use handle_map::{Handle, HandleLike};
 use np_ffi_core::credentials::{
     create_credential_book_from_slab, deallocate_credential_book, CreateCredentialBookResult,
-    CredentialBook, CredentialSlab,
+    CredentialBook as CredentialBookHandle, CredentialSlab,
 };
-use pourover::jni_method;
+use pourover::{desc::ClassDesc, jni_method};
+
+static CREDENTIAL_BOOK_CLASS: ClassDesc =
+    ClassDesc::new("com/google/android/nearby/presence/rust/credential/CredentialBook");
+
+/// Rust representation of `class CredentialBook`.
+#[repr(transparent)]
+pub struct CredentialBook<Obj>(pub Obj);
+
+impl<'local, Obj: AsRef<JObject<'local>>> CredentialBook<Obj> {
+    /// Get the raw handle id
+    pub fn get_id<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<jlong> {
+        pourover::call_method!(env, &CREDENTIAL_BOOK_CLASS, "getId", "()J", self.as_obj(),)
+    }
+
+    /// Get the associated Rust handle from this Java handle object
+    pub fn get_handle<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+    ) -> jni::errors::Result<CredentialBookHandle> {
+        self.get_id(env).map(|id| Handle::from_id(id as u64)).map(CredentialBookHandle::from_handle)
+    }
+
+    /// Get a reference to the inner `jni` crate [`JObject`].
+    pub fn as_obj(&self) -> &JObject<'local> {
+        self.0.as_ref()
+    }
+}
 
 #[jni_method(
     package = "com.google.android.nearby.presence.rust.credential",
@@ -60,6 +91,7 @@
     handle_id: jlong,
 ) {
     // Swallow errors here since there's nothing meaningful to do.
-    let _ =
-        deallocate_credential_book(CredentialBook::from_handle(Handle::from_id(handle_id as u64)));
+    let _ = deallocate_credential_book(CredentialBookHandle::from_handle(Handle::from_id(
+        handle_id as u64,
+    )));
 }
diff --git a/nearby/presence/np_java_ffi/src/class/deserialized_v0_advertisement.rs b/nearby/presence/np_java_ffi/src/class/deserialized_v0_advertisement.rs
index 1d91ae9..ba47ff7 100644
--- a/nearby/presence/np_java_ffi/src/class/deserialized_v0_advertisement.rs
+++ b/nearby/presence/np_java_ffi/src/class/deserialized_v0_advertisement.rs
@@ -20,7 +20,7 @@
 use np_ffi_core::deserialize::v0::{DeserializedV0IdentityKind, V0Payload};
 use pourover::desc::ClassDesc;
 
-use crate::class::IdentityKind;
+use crate::class::{CredentialBook, IdentityKind};
 
 static DESERIALIZED_V0_ADVERTISEMENT_CLASS: ClassDesc =
     ClassDesc::new("com/google/android/nearby/presence/rust/DeserializedV0Advertisement");
@@ -35,30 +35,32 @@
         env: &mut JNIEnv<'local>,
         error: V0AdvertisementError,
     ) -> jni::errors::Result<Self> {
-        let error = IdentityKind::error_for_v0(env, error)?;
+        let error = IdentityKind::from(error).to_java(env)?;
 
         pourover::call_constructor!(env, &DESERIALIZED_V0_ADVERTISEMENT_CLASS, "(I)V", error)
             .map(Self)
     }
 
     /// Create a legible advertisment.
-    pub fn construct(
+    pub fn construct<'book>(
         env: &mut JNIEnv<'local>,
         num_des: u8,
         v0_payload: V0Payload,
         identity: DeserializedV0IdentityKind,
+        credential_book: CredentialBook<impl AsRef<JObject<'book>>>,
     ) -> jni::errors::Result<Self> {
         let num_des = jint::from(num_des);
         let payload_handle = v0_payload.get_as_handle().get_id() as jlong;
-        let identity = IdentityKind::value_for_v0(env, identity)?;
+        let identity = IdentityKind::from(identity).to_java(env)?;
 
         pourover::call_constructor!(
             env,
             &DESERIALIZED_V0_ADVERTISEMENT_CLASS,
-            "(IJI)V",
+            "(IJILcom/google/android/nearby/presence/rust/credential/CredentialBook;)V",
             num_des,
             payload_handle,
-            identity
+            identity,
+            credential_book.as_obj()
         )
         .map(Self)
     }
diff --git a/nearby/presence/np_java_ffi/src/class/deserialized_v1_advertisement.rs b/nearby/presence/np_java_ffi/src/class/deserialized_v1_advertisement.rs
index e537052..3b0af45 100644
--- a/nearby/presence/np_java_ffi/src/class/deserialized_v1_advertisement.rs
+++ b/nearby/presence/np_java_ffi/src/class/deserialized_v1_advertisement.rs
@@ -15,6 +15,8 @@
 use jni::{objects::JObject, sys::jint, JNIEnv};
 use pourover::desc::ClassDesc;
 
+use crate::class::CredentialBook;
+
 static DESERIALIZED_V1_ADVERTISEMENT_CLASS: ClassDesc =
     ClassDesc::new("com/google/android/nearby/presence/rust/DeserializedV1Advertisement");
 
@@ -24,19 +26,21 @@
 
 impl<'local> DeserializedV1Advertisement<JObject<'local>> {
     /// Create a new advertisement.
-    pub fn construct(
+    pub fn construct<'sections, 'book>(
         env: &mut JNIEnv<'local>,
         num_legible_sections: jint,
         num_undecryptable_sections: jint,
-        legible_sections: super::LegibleV1Sections<impl AsRef<JObject<'local>>>,
+        legible_sections: super::LegibleV1Sections<impl AsRef<JObject<'sections>>>,
+        credential_book: CredentialBook<impl AsRef<JObject<'book>>>,
     ) -> jni::errors::Result<Self> {
         pourover::call_constructor!(
             env,
             &DESERIALIZED_V1_ADVERTISEMENT_CLASS,
-            "(IILcom/google/android/nearby/presence/rust/LegibleV1Sections;)V",
+            "(IILcom/google/android/nearby/presence/rust/LegibleV1Sections;Lcom/google/android/nearby/presence/rust/credential/CredentialBook;)V",
             num_legible_sections,
             num_undecryptable_sections,
-            legible_sections.as_obj()
+            legible_sections.as_obj(),
+            credential_book.as_obj(),
         )
         .map(Self)
     }
diff --git a/nearby/presence/np_java_ffi/src/class/deserialized_v1_section.rs b/nearby/presence/np_java_ffi/src/class/deserialized_v1_section.rs
index b3f6dba..3b6b5ac 100644
--- a/nearby/presence/np_java_ffi/src/class/deserialized_v1_section.rs
+++ b/nearby/presence/np_java_ffi/src/class/deserialized_v1_section.rs
@@ -13,10 +13,11 @@
 // limitations under the License.
 
 use jni::{objects::JObject, sys::jint, JNIEnv};
-use np_ffi_core::deserialize::v1::DeserializedV1IdentityKind;
-use pourover::desc::ClassDesc;
+use np_ffi_core::{deserialize::v1::DeserializedV1IdentityKind, v1::V1VerificationMode};
+use pourover::desc::{ClassDesc, StaticFieldDesc};
+use std::sync::RwLock;
 
-use crate::class::{IdentityKind, LegibleV1Sections};
+use crate::class::{CredentialBook, IdentityKind, LegibleV1Sections};
 
 static DESERIALIZED_V1_SECTION_CLASS: ClassDesc =
     ClassDesc::new("com/google/android/nearby/presence/rust/DeserializedV1Section");
@@ -27,24 +28,93 @@
 
 impl<'local> DeserializedV1Section<JObject<'local>> {
     /// Create a new deserialized section
-    pub fn construct<'a>(
+    pub fn construct<'handle, 'book>(
         env: &mut JNIEnv<'local>,
-        legible_sections_handle: LegibleV1Sections<impl AsRef<JObject<'a>>>,
+        legible_sections_handle: LegibleV1Sections<impl AsRef<JObject<'handle>>>,
         legible_section_index: u8,
         num_des: u8,
         identity_kind: DeserializedV1IdentityKind,
+        credential_book: CredentialBook<impl AsRef<JObject<'book>>>,
     ) -> jni::errors::Result<Self> {
-        let identity = IdentityKind::value_for_v1(env, identity_kind)?;
+        let identity = IdentityKind::from(identity_kind).to_java(env)?;
 
         pourover::call_constructor!(
             env,
             &DESERIALIZED_V1_SECTION_CLASS,
-            "(Lcom/google/android/nearby/presence/rust/LegibleV1Sections;III)V",
+            "(Lcom/google/android/nearby/presence/rust/LegibleV1Sections;IIILcom/google/android/nearby/presence/rust/credential/CredentialBook;)V",
             legible_sections_handle.as_obj(),
             jint::from(legible_section_index),
             jint::from(num_des),
-            identity
+            identity,
+            credential_book.as_obj()
         )
         .map(Self)
     }
 }
+
+static VERIFICATION_MODE_CLASS: ClassDesc = ClassDesc::new(
+    "com/google/android/nearby/presence/rust/DeserializedV1Section$VerificationMode",
+);
+
+/// Rust representation of `@VerificationMode`. These are `jints` on the Java side, so this type can't
+/// be instantiated.
+pub enum VerificationMode {}
+
+impl VerificationMode {
+    /// Convert a Rust verification mode enum to the Java `jint` representation.
+    pub fn value_for<'local>(
+        env: &mut JNIEnv<'local>,
+        mode: V1VerificationMode,
+    ) -> jni::errors::Result<jint> {
+        match mode {
+            V1VerificationMode::Mic => Self::mic(env),
+            V1VerificationMode::Signature => Self::signature(env),
+        }
+    }
+
+    /// Fetch the `SIGNATURE` constant
+    pub fn signature<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
+        static SIGNATURE: StaticFieldDesc = VERIFICATION_MODE_CLASS.static_field("SIGNATURE", "I");
+        static VALUE: RwLock<Option<jint>> = RwLock::new(None);
+        Self::lookup_static_value(env, &SIGNATURE, &VALUE)
+    }
+
+    /// Fetch the `MIC` constant
+    pub fn mic<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
+        static MIC: StaticFieldDesc = VERIFICATION_MODE_CLASS.static_field("MIC", "I");
+        static VALUE: RwLock<Option<jint>> = RwLock::new(None);
+        Self::lookup_static_value(env, &MIC, &VALUE)
+    }
+
+    /// Look up the given field and cache it in the given cache. The lookup will only be performed
+    /// once if successful. This uses `RwLock` instead of `OnceCell` since the fallible `OnceCell`
+    /// APIs are nightly only.
+    fn lookup_static_value<'local>(
+        env: &mut JNIEnv<'local>,
+        field: &StaticFieldDesc,
+        cache: &RwLock<Option<jint>>,
+    ) -> jni::errors::Result<jint> {
+        use jni::signature::{JavaType, Primitive};
+
+        // Read from cache
+        if let Some(value) = *cache.read().unwrap_or_else(|poison| poison.into_inner()) {
+            return Ok(value);
+        }
+
+        // Get exclusive access to the cache for the lookup
+        let mut guard = cache.write().unwrap_or_else(|poison| poison.into_inner());
+
+        // In case of races, only lookup the value once
+        if let Some(value) = *guard {
+            return Ok(value);
+        }
+
+        let value = env
+            .get_static_field_unchecked(field.cls(), field, JavaType::Primitive(Primitive::Int))
+            .and_then(|ret| ret.i())?;
+
+        *guard = Some(value);
+
+        Ok(value)
+    }
+}
diff --git a/nearby/presence/np_java_ffi/src/class/identity_kind.rs b/nearby/presence/np_java_ffi/src/class/identity_kind.rs
index 8fce071..0ef415d 100644
--- a/nearby/presence/np_java_ffi/src/class/identity_kind.rs
+++ b/nearby/presence/np_java_ffi/src/class/identity_kind.rs
@@ -26,13 +26,46 @@
 static IDENTITY_KIND_CLASS: ClassDesc =
     ClassDesc::new("com/google/android/nearby/presence/rust/IdentityKind");
 
-/// Rust representation of `@IdentityKind`. These are `jints` on the Java side, so this type can't
-/// be instantiated.
-pub enum IdentityKind {}
+/// Rust representation of `@interface IdentityKind`.
+#[derive(Copy, Clone, PartialEq, Eq)]
+pub enum IdentityKind {
+    /// An illegible identity
+    NoMatchingCredentials,
+    /// A public identity
+    Plaintext,
+    /// An encrypted identity
+    Decrypted,
+}
 
 impl IdentityKind {
+    /// Convert a Java `int` to the Rust version. Will return `None` if the given `value` is not
+    /// valid.
+    pub fn from_java<'local>(
+        env: &mut JNIEnv<'local>,
+        value: jint,
+    ) -> jni::errors::Result<Option<Self>> {
+        if value == Self::no_matching_credentials(env)? {
+            Ok(Some(Self::NoMatchingCredentials))
+        } else if value == Self::plaintext(env)? {
+            Ok(Some(Self::Plaintext))
+        } else if value == Self::decrypted(env)? {
+            Ok(Some(Self::Decrypted))
+        } else {
+            Ok(None)
+        }
+    }
+
+    /// Convert to a Java `int` value.
+    pub fn to_java<'local>(&self, env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
+        match self {
+            Self::NoMatchingCredentials => Self::no_matching_credentials(env),
+            Self::Plaintext => Self::plaintext(env),
+            Self::Decrypted => Self::decrypted(env),
+        }
+    }
+
     /// Fetch the `NO_MATCHING_CREDENTIALS` constant
-    pub fn no_matching_credentials<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
+    fn no_matching_credentials<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
         static NO_MATCHING_CREDENTIALS: StaticFieldDesc =
             IDENTITY_KIND_CLASS.static_field("NO_MATCHING_CREDENTIALS", "I");
         static VALUE: RwLock<Option<jint>> = RwLock::new(None);
@@ -40,14 +73,14 @@
     }
 
     /// Fetch the `PLAINTEXT` constant
-    pub fn plaintext<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
+    fn plaintext<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
         static PLAINTEXT: StaticFieldDesc = IDENTITY_KIND_CLASS.static_field("PLAINTEXT", "I");
         static VALUE: RwLock<Option<jint>> = RwLock::new(None);
         Self::lookup_static_value(env, &PLAINTEXT, &VALUE)
     }
 
     /// Fetch the `DECRYPTED` constant
-    pub fn decrypted<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
+    fn decrypted<'local>(env: &mut JNIEnv<'local>) -> jni::errors::Result<jint> {
         static DECRYPTED: StaticFieldDesc = IDENTITY_KIND_CLASS.static_field("DECRYPTED", "I");
         static VALUE: RwLock<Option<jint>> = RwLock::new(None);
         Self::lookup_static_value(env, &DECRYPTED, &VALUE)
@@ -82,36 +115,30 @@
 
         Ok(value)
     }
+}
 
-    /// Get the Java representation of [`V0AdvertisementError`].
-    pub fn error_for_v0<'local>(
-        env: &mut JNIEnv<'local>,
-        identity: V0AdvertisementError,
-    ) -> jni::errors::Result<jint> {
-        match identity {
-            V0AdvertisementError::NoMatchingCredentials => Self::no_matching_credentials(env),
+impl From<V0AdvertisementError> for IdentityKind {
+    fn from(err: V0AdvertisementError) -> Self {
+        match err {
+            V0AdvertisementError::NoMatchingCredentials => Self::NoMatchingCredentials,
         }
     }
+}
 
-    /// Get the Java representation of [`DeserializedV0IdentityKind`].
-    pub fn value_for_v0<'local>(
-        env: &mut JNIEnv<'local>,
-        identity: DeserializedV0IdentityKind,
-    ) -> jni::errors::Result<jint> {
-        match identity {
-            DeserializedV0IdentityKind::Plaintext => Self::plaintext(env),
-            DeserializedV0IdentityKind::Decrypted => Self::decrypted(env),
+impl From<DeserializedV0IdentityKind> for IdentityKind {
+    fn from(kind: DeserializedV0IdentityKind) -> Self {
+        match kind {
+            DeserializedV0IdentityKind::Plaintext => Self::Plaintext,
+            DeserializedV0IdentityKind::Decrypted => Self::Decrypted,
         }
     }
+}
 
-    /// Get the Java representation of [`DeserializedV1IdentityKind`].
-    pub fn value_for_v1<'local>(
-        env: &mut JNIEnv<'local>,
-        identity: DeserializedV1IdentityKind,
-    ) -> jni::errors::Result<jint> {
-        match identity {
-            DeserializedV1IdentityKind::Plaintext => Self::plaintext(env),
-            DeserializedV1IdentityKind::Decrypted => Self::decrypted(env),
+impl From<DeserializedV1IdentityKind> for IdentityKind {
+    fn from(kind: DeserializedV1IdentityKind) -> Self {
+        match kind {
+            DeserializedV1IdentityKind::Plaintext => Self::Plaintext,
+            DeserializedV1IdentityKind::Decrypted => Self::Decrypted,
         }
     }
 }
diff --git a/nearby/presence/np_java_ffi/src/class/legible_v1_sections.rs b/nearby/presence/np_java_ffi/src/class/legible_v1_sections.rs
index 911c78d..6bc9225 100644
--- a/nearby/presence/np_java_ffi/src/class/legible_v1_sections.rs
+++ b/nearby/presence/np_java_ffi/src/class/legible_v1_sections.rs
@@ -12,20 +12,29 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
+use handle_map::{Handle, HandleLike};
 use jni::{
     objects::{JClass, JObject},
     sys::{jint, jlong},
     JNIEnv,
 };
-
-use crate::class::{v1_data_element::Generic, DeserializedV1Section};
-use handle_map::{Handle, HandleLike};
-use np_ffi_core::deserialize::v1::{
-    GetV1DEResult, GetV1SectionResult, LegibleV1Sections as LegibleSectionsHandle, V1DataElement,
+use np_ffi_core::{
+    deserialize::{
+        v1::{
+            DeserializedV1IdentityDetails, GetV1DEResult, GetV1IdentityDetailsResult,
+            GetV1SectionResult, LegibleV1Sections as LegibleSectionsHandle, V1DataElement,
+        },
+        DecryptMetadataResult,
+    },
+    v1::V1VerificationMode,
 };
 use pourover::{desc::ClassDesc, jni_method};
 
-static LEGIBLE_V1_SECTIONS: ClassDesc =
+use crate::class::{
+    v1_data_element::Generic, CredentialBook, DeserializedV1Section, VerificationMode,
+};
+
+static LEGIBLE_V1_SECTIONS_CLASS: ClassDesc =
     ClassDesc::new("com/google/android/nearby/presence/rust/LegibleV1Sections");
 
 /// Rust representation for `class LegibleV1Sections`.
@@ -41,7 +50,7 @@
     ) -> jni::errors::Result<Self> {
         let handle_id = handle.get_as_handle().get_id() as jlong;
 
-        pourover::call_constructor!(env, &LEGIBLE_V1_SECTIONS, "(J)V", handle_id,).map(Self)
+        pourover::call_constructor!(env, &LEGIBLE_V1_SECTIONS_CLASS, "(J)V", handle_id,).map(Self)
     }
 }
 
@@ -65,17 +74,50 @@
         &self,
         env: &mut JNIEnv<'env_local>,
     ) -> jni::errors::Result<jlong> {
-        use jni::signature::{Primitive, ReturnType};
-        use pourover::desc::FieldDesc;
+        use LEGIBLE_V1_SECTIONS_CLASS as CLS;
+        pourover::call_method!(env, &CLS, "getId", "()J", self.as_obj())
+    }
+}
 
-        static HANDLE_ID_FIELD: FieldDesc = LEGIBLE_V1_SECTIONS.field("handleId", "J");
+static IDENTITY_DETAILS_CLASS: ClassDesc =
+    ClassDesc::new("com/google/android/nearby/presence/rust/LegibleV1Sections$IdentityDetails");
 
-        env.get_field_unchecked(
-            self.0.as_ref(),
-            &HANDLE_ID_FIELD,
-            ReturnType::Primitive(Primitive::Long),
+/// Rust representation for `class LegibleV1Sections.IdentityDetails`.
+#[repr(transparent)]
+pub struct IdentityDetails<Obj>(pub Obj);
+
+impl<'local> IdentityDetails<JObject<'local>> {
+    pub fn construct(
+        env: &mut JNIEnv<'local>,
+        details: DeserializedV1IdentityDetails,
+    ) -> jni::errors::Result<Self> {
+        Self::construct_from_parts(
+            env,
+            details.verification_mode(),
+            details.cred_id(),
+            details.identity_token(),
         )
-        .and_then(|val| val.j())
+    }
+
+    pub fn construct_from_parts(
+        env: &mut JNIEnv<'local>,
+        verification_mode: V1VerificationMode,
+        credential_id: u32,
+        identity_token: [u8; 16],
+    ) -> jni::errors::Result<Self> {
+        let verification_mode = VerificationMode::value_for(env, verification_mode)?;
+        let credential_id = credential_id as jint;
+        let identity_token = env.byte_array_from_slice(&identity_token)?;
+
+        pourover::call_constructor!(
+            env,
+            &IDENTITY_DETAILS_CLASS,
+            "(II[B)V",
+            verification_mode,
+            credential_id,
+            identity_token
+        )
+        .map(Self)
     }
 }
 
@@ -86,6 +128,7 @@
     mut env: JNIEnv<'local>,
     legible_sections_obj: LegibleV1Sections<JObject<'local>>,
     index: jint,
+    credential_book: CredentialBook<JObject<'local>>,
 ) -> JObject<'local> {
     let Ok(legible_sections) = legible_sections_obj.as_rust_handle(&mut env) else {
         return JObject::null();
@@ -104,6 +147,7 @@
         index,
         section.num_des(),
         section.identity_kind(),
+        credential_book,
     ) {
         Ok(section) => section.0,
         Err(_) => JObject::null(),
@@ -149,6 +193,57 @@
 }
 
 #[jni_method(package = "com.google.android.nearby.presence.rust", class = "LegibleV1Sections")]
+extern "system" fn nativeGetSectionIdentityDetails<'local>(
+    mut env: JNIEnv<'local>,
+    legible_sections_obj: LegibleV1Sections<JObject<'local>>,
+    section_index: jint,
+) -> JObject<'local> {
+    let Ok(legible_sections) = legible_sections_obj.as_rust_handle(&mut env) else {
+        return JObject::null();
+    };
+    let Ok(section_index) = u8::try_from(section_index) else {
+        return JObject::null();
+    };
+
+    let GetV1IdentityDetailsResult::Success(details) =
+        legible_sections.get_section_identity_details(section_index)
+    else {
+        return JObject::null();
+    };
+
+    IdentityDetails::construct(&mut env, details).map_or_else(|_err| JObject::null(), |obj| obj.0)
+}
+
+#[jni_method(package = "com.google.android.nearby.presence.rust", class = "LegibleV1Sections")]
+extern "system" fn nativeGetSectionDecryptedMetadata<'local>(
+    mut env: JNIEnv<'local>,
+    legible_sections_obj: LegibleV1Sections<JObject<'local>>,
+    section_index: jint,
+) -> JObject<'local> {
+    let Ok(legible_sections) = legible_sections_obj.as_rust_handle(&mut env) else {
+        return JObject::null();
+    };
+    let Ok(section_index) = u8::try_from(section_index) else {
+        return JObject::null();
+    };
+
+    let DecryptMetadataResult::Success(metadata) =
+        legible_sections.decrypt_section_metadata(section_index)
+    else {
+        return JObject::null();
+    };
+
+    let Ok(buffer) = metadata.take_buffer() else {
+        return JObject::null();
+    };
+    let Ok(decrypted_metadata_array) = env.byte_array_from_slice(&buffer[..]) else {
+        return JObject::null();
+    };
+
+    decrypted_metadata_array.into()
+}
+
+#[jni_method(package = "com.google.android.nearby.presence.rust", class = "LegibleV1Sections")]
 extern "system" fn deallocate<'local>(
     _env: JNIEnv<'local>,
     _cls: JClass<'local>,
diff --git a/nearby/presence/np_java_ffi/src/class/np_adv.rs b/nearby/presence/np_java_ffi/src/class/np_adv.rs
index 88513cc..24de9c0 100644
--- a/nearby/presence/np_java_ffi/src/class/np_adv.rs
+++ b/nearby/presence/np_java_ffi/src/class/np_adv.rs
@@ -14,16 +14,15 @@
 
 use jni::{
     objects::{JByteArray, JClass, JObject},
-    sys::{jint, jlong},
+    sys::jint,
     JNIEnv,
 };
 
 use crate::class::{
-    DeserializeResult, DeserializeResultError, DeserializedV0Advertisement,
+    CredentialBook, DeserializeResult, DeserializeResultError, DeserializedV0Advertisement,
     DeserializedV1Advertisement, LegibleV1Sections, V0AdvertisementError,
 };
-use handle_map::Handle;
-use np_ffi_core::{credentials::CredentialBook, deserialize::deserialize_advertisement_from_slice};
+use np_ffi_core::deserialize::deserialize_advertisement_from_slice;
 use pourover::{jni_method, ToUnsigned};
 
 #[jni_method(package = "com.google.android.nearby.presence.rust", class = "NpAdv")]
@@ -31,9 +30,11 @@
     mut env: JNIEnv<'local>,
     _cls: JClass<'local>,
     service_data: JByteArray<'local>,
-    credential_book: jlong,
+    credential_book_obj: CredentialBook<JObject<'local>>,
 ) -> JObject<'local> {
-    let credential_book = CredentialBook::from_handle(Handle::from_id(credential_book as u64));
+    let Ok(credential_book) = credential_book_obj.get_handle(&mut env) else {
+        return JObject::null();
+    };
 
     // Unpack the service data
     let mut service_data_buf = [0i8; 256];
@@ -74,6 +75,7 @@
             adv.num_des(),
             adv.payload(),
             adv.identity_kind(),
+            credential_book_obj,
         )
         .and_then(|adv| DeserializeResult::from_v0_advertisement(&mut env, adv))
         .map(|obj| obj.0),
@@ -85,6 +87,7 @@
                     jint::from(adv.num_legible_sections),
                     jint::from(adv.num_undecryptable_sections),
                     sections,
+                    credential_book_obj,
                 )
             })
             .and_then(|adv| DeserializeResult::from_v1_advertisement(&mut env, adv))
diff --git a/nearby/presence/np_java_ffi/src/class/serialization_exception.rs b/nearby/presence/np_java_ffi/src/class/serialization_exception.rs
new file mode 100644
index 0000000..3e3235e
--- /dev/null
+++ b/nearby/presence/np_java_ffi/src/class/serialization_exception.rs
@@ -0,0 +1,139 @@
+// Copyright 2024 Google LLC
+//
+// 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
+//
+//     http://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.
+
+use jni::{
+    objects::{JObject, JString, JThrowable},
+    JNIEnv,
+};
+use pourover::desc::ClassDesc;
+
+static INVALID_DATA_ELEMENT_EXCEPTION: ClassDesc = ClassDesc::new(
+    "com/google/android/nearby/presence/rust/SerializationException$InvalidDataElementException",
+);
+
+/// Rust representation of `class SerializationException.InvalidDataElementException`.
+#[repr(transparent)]
+pub struct InvalidDataElementException<Obj>(pub Obj);
+
+impl<'local> InvalidDataElementException<JObject<'local>> {
+    /// Create a new instance.
+    pub fn construct<'s>(
+        env: &mut JNIEnv<'local>,
+        reason: &JString<'s>,
+    ) -> jni::errors::Result<Self> {
+        pourover::call_constructor!(
+            env,
+            &INVALID_DATA_ELEMENT_EXCEPTION,
+            "(Ljava/lang/String;)V",
+            reason
+        )
+        .map(Self)
+    }
+
+    /// Create a new instance and throw it.
+    pub fn throw_new<'s>(
+        env: &mut JNIEnv<'local>,
+        reason: &JString<'s>,
+    ) -> jni::errors::Result<()> {
+        Self::construct(env, reason)?.throw(env)
+    }
+}
+
+impl<'local, Obj: AsRef<JObject<'local>>> InvalidDataElementException<Obj> {
+    /// Throw this exception.
+    pub fn throw<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<()> {
+        env.throw(<&JThrowable>::from(self.0.as_ref()))
+    }
+}
+
+static INSUFFICIENT_SPACE_EXCEPTION: ClassDesc = ClassDesc::new(
+    "com/google/android/nearby/presence/rust/SerializationException$InsufficientSpaceException",
+);
+
+/// Rust representation of `class SerializationException.InsufficientSpaceException`.
+#[repr(transparent)]
+pub struct InsufficientSpaceException<Obj>(pub Obj);
+
+impl<'local> InsufficientSpaceException<JObject<'local>> {
+    /// Create a new instance.
+    pub fn construct(env: &mut JNIEnv<'local>) -> jni::errors::Result<Self> {
+        pourover::call_constructor!(env, &INSUFFICIENT_SPACE_EXCEPTION, "()V",).map(Self)
+    }
+
+    /// Create a new instance and throw it.
+    pub fn throw_new(env: &mut JNIEnv<'local>) -> jni::errors::Result<()> {
+        Self::construct(env)?.throw(env)
+    }
+}
+
+impl<'local, Obj: AsRef<JObject<'local>>> InsufficientSpaceException<Obj> {
+    /// Throw this exception.
+    pub fn throw<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<()> {
+        env.throw(<&JThrowable>::from(self.0.as_ref()))
+    }
+}
+
+static LDT_ENCRYPTION_EXCEPTION_CLASS: ClassDesc = ClassDesc::new(
+    "com/google/android/nearby/presence/rust/SerializationException$LdtEncryptionException",
+);
+
+/// Rust representation of `class SerializationException.LdtEncryptionException`.
+#[repr(transparent)]
+pub struct LdtEncryptionException<Obj>(pub Obj);
+
+impl<'local> LdtEncryptionException<JObject<'local>> {
+    /// Create a new instance.
+    pub fn construct(env: &mut JNIEnv<'local>) -> jni::errors::Result<Self> {
+        pourover::call_constructor!(env, &LDT_ENCRYPTION_EXCEPTION_CLASS, "()V").map(Self)
+    }
+
+    /// Create a new instance and throw it.
+    pub fn throw_new(env: &mut JNIEnv<'local>) -> jni::errors::Result<()> {
+        Self::construct(env)?.throw(env)
+    }
+}
+
+impl<'local, Obj: AsRef<JObject<'local>>> LdtEncryptionException<Obj> {
+    /// Throw this exception.
+    pub fn throw<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<()> {
+        env.throw(<&JThrowable>::from(self.0.as_ref()))
+    }
+}
+
+static UNENCRYPTED_SIZE_EXCEPTION_CLASS: ClassDesc = ClassDesc::new(
+    "com/google/android/nearby/presence/rust/SerializationException$UnencryptedSizeException",
+);
+
+/// Rust representation of `class SerializationException.UnencryptedSizeException`.
+#[repr(transparent)]
+pub struct UnencryptedSizeException<Obj>(pub Obj);
+
+impl<'local> UnencryptedSizeException<JObject<'local>> {
+    /// Create a new instance.
+    pub fn construct(env: &mut JNIEnv<'local>) -> jni::errors::Result<Self> {
+        pourover::call_constructor!(env, &UNENCRYPTED_SIZE_EXCEPTION_CLASS, "()V").map(Self)
+    }
+
+    /// Create a new instance and throw it.
+    pub fn throw_new(env: &mut JNIEnv<'local>) -> jni::errors::Result<()> {
+        Self::construct(env)?.throw(env)
+    }
+}
+
+impl<'local, Obj: AsRef<JObject<'local>>> UnencryptedSizeException<Obj> {
+    /// Throw this exception.
+    pub fn throw<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<()> {
+        env.throw(<&JThrowable>::from(self.0.as_ref()))
+    }
+}
diff --git a/nearby/presence/np_java_ffi/src/class/v0_advertisement_builder.rs b/nearby/presence/np_java_ffi/src/class/v0_advertisement_builder.rs
new file mode 100644
index 0000000..1ba39b6
--- /dev/null
+++ b/nearby/presence/np_java_ffi/src/class/v0_advertisement_builder.rs
@@ -0,0 +1,248 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+//     http://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.
+
+use handle_map::{Handle, HandleLike};
+use jni::{
+    objects::{JByteArray, JClass, JObject},
+    sys::jlong,
+    JNIEnv,
+};
+use np_ffi_core::{
+    serialize::v0::{
+        create_v0_encrypted_advertisement_builder, create_v0_public_advertisement_builder,
+        AddV0DEResult, CreateV0AdvertisementBuilderResult, SerializeV0AdvertisementResult,
+        V0AdvertisementBuilder,
+    },
+    v0::{BuildTxPowerResult, V0DataElement},
+};
+use pourover::{desc::ClassDesc, jni_method};
+
+use crate::class::{
+    v0_data_element::{TxPower, V0Actions},
+    InsufficientSpaceException, InvalidDataElementException, InvalidHandleException,
+    LdtEncryptionException, NoSpaceLeftException, UnencryptedSizeException, V0BroadcastCredential,
+};
+
+static V0_BUILDER_HANDLE_CLASS: ClassDesc = ClassDesc::new(
+    "com/google/android/nearby/presence/rust/V0AdvertisementBuilder$V0BuilderHandle",
+);
+
+#[repr(transparent)]
+pub struct V0BuilderHandle<Obj>(pub Obj);
+
+impl<'local, Obj: AsRef<JObject<'local>>> V0BuilderHandle<Obj> {
+    /// Get a reference to the inner `jni` crate [`JObject`].
+    pub fn as_obj(&self) -> &JObject<'local> {
+        self.0.as_ref()
+    }
+
+    /// Get the Rust [`HandleLike`] representation from this Java object.
+    pub fn as_rust_handle<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+    ) -> jni::errors::Result<V0AdvertisementBuilder> {
+        let handle_id = self.get_handle_id(env)?;
+        Ok(V0AdvertisementBuilder::from_handle(Handle::from_id(handle_id as u64)))
+    }
+
+    /// Get `long handleId` from the Java object
+    fn get_handle_id<'env_local>(
+        &self,
+        env: &mut JNIEnv<'env_local>,
+    ) -> jni::errors::Result<jlong> {
+        use V0_BUILDER_HANDLE_CLASS as CLS;
+        pourover::call_method!(env, &CLS, "getId", "()J", self.as_obj())
+    }
+
+    fn add_de<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+        data_element: V0DataElement,
+    ) -> jni::errors::Result<()> {
+        let builder = self.as_rust_handle(env)?;
+        #[allow(clippy::expect_used)]
+        let res = builder.add_de(data_element).expect("valid data structure (created in Rust)");
+
+        match res {
+            AddV0DEResult::Success => {}
+            AddV0DEResult::InvalidAdvertisementBuilderHandle => {
+                InvalidHandleException::throw_new(env)?;
+            }
+            AddV0DEResult::InsufficientAdvertisementSpace => {
+                InsufficientSpaceException::throw_new(env)?;
+            }
+            AddV0DEResult::InvalidIdentityTypeForDataElement => {
+                let _ = env
+                    .new_string("Mismatched identity kind for V0Actions data element")
+                    .map(|string| env.auto_local(string))
+                    .and_then(|string| InvalidDataElementException::throw_new(env, &string));
+            }
+        }
+
+        Ok(())
+    }
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V0AdvertisementBuilder.V0BuilderHandle"
+)]
+extern "system" fn allocatePublic<'local>(mut env: JNIEnv<'local>, _cls: JClass<'local>) -> jlong {
+    match create_v0_public_advertisement_builder() {
+        CreateV0AdvertisementBuilderResult::Success(builder) => {
+            builder.get_as_handle().get_id() as jlong
+        }
+        CreateV0AdvertisementBuilderResult::NoSpaceLeft => {
+            let _ = NoSpaceLeftException::throw_new(&mut env);
+            0
+        }
+    }
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V0AdvertisementBuilder.V0BuilderHandle"
+)]
+extern "system" fn allocatePrivate<'local>(
+    mut env: JNIEnv<'local>,
+    _cls: JClass<'local>,
+    credential: V0BroadcastCredential<JObject<'local>>,
+    salt_arr: JByteArray<'local>,
+) -> jlong {
+    let Ok(credential) = credential.get_as_core(&mut env) else {
+        return 0;
+    };
+
+    let mut salt = [0; 2];
+    if let Err(_jni_err) = env.get_byte_array_region(salt_arr, 0, &mut salt[..]) {
+        return 0;
+    }
+    let salt = salt.map(|byte| byte as u8).into();
+
+    match create_v0_encrypted_advertisement_builder(credential, salt) {
+        CreateV0AdvertisementBuilderResult::Success(builder) => {
+            builder.get_as_handle().get_id() as jlong
+        }
+        CreateV0AdvertisementBuilderResult::NoSpaceLeft => {
+            let _ = NoSpaceLeftException::throw_new(&mut env);
+            0
+        }
+    }
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V0AdvertisementBuilder.V0BuilderHandle",
+    method_name = "nativeAddTxPowerDataElement"
+)]
+extern "system" fn add_tx_power_de<'local>(
+    mut env: JNIEnv<'local>,
+    this: V0BuilderHandle<JObject<'local>>,
+    tx_power: TxPower<JObject<'local>>,
+) {
+    let tx_power = match tx_power.get_as_core(&mut env) {
+        Ok(BuildTxPowerResult::Success(tx_power)) => tx_power,
+        Ok(BuildTxPowerResult::OutOfRange) => {
+            let _ = env
+                .new_string("TX Power value out of range")
+                .map(|string| env.auto_local(string))
+                .and_then(|string| InvalidDataElementException::throw_new(&mut env, &string));
+            return;
+        }
+        Err(_jni_err) => {
+            // `crate jni` should have already thrown
+            return;
+        }
+    };
+    let Ok(()) = this.add_de(&mut env, V0DataElement::TxPower(tx_power)) else {
+        // `crate jni` should have already thrown
+        return;
+    };
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V0AdvertisementBuilder.V0BuilderHandle",
+    method_name = "nativeAddV0ActionsDataElement"
+)]
+extern "system" fn add_v0_actions_de<'local>(
+    mut env: JNIEnv<'local>,
+    this: V0BuilderHandle<JObject<'local>>,
+    v0_actions: V0Actions<JObject<'local>>,
+) {
+    let v0_actions = match v0_actions.get_as_core(&mut env) {
+        Ok(Some(v0_actions)) => v0_actions,
+        Ok(None) => {
+            let _ = env
+                .new_string("V0Actions is not valid")
+                .map(|string| env.auto_local(string))
+                .and_then(|string| InvalidDataElementException::throw_new(&mut env, &string));
+            return;
+        }
+        Err(_jni_err) => {
+            // `crate jni` should have already thrown
+            return;
+        }
+    };
+    let Ok(()) = this.add_de(&mut env, V0DataElement::Actions(v0_actions)) else {
+        // `crate jni` should have already thrown
+        return;
+    };
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V0AdvertisementBuilder.V0BuilderHandle"
+)]
+extern "system" fn nativeBuild<'local>(
+    mut env: JNIEnv<'local>,
+    this: V0BuilderHandle<JObject<'local>>,
+) -> JObject<'local> {
+    let Ok(builder) = this.as_rust_handle(&mut env) else {
+        return JObject::null();
+    };
+
+    match builder.into_advertisement() {
+        SerializeV0AdvertisementResult::Success(bytes) => {
+            #[allow(clippy::expect_used)]
+            let adv_bytes = bytes.as_slice().expect("should never be malformed from core");
+            env.byte_array_from_slice(adv_bytes).map_or(JObject::null(), JObject::from)
+        }
+        SerializeV0AdvertisementResult::InvalidAdvertisementBuilderHandle => {
+            let _ = InvalidHandleException::throw_new(&mut env);
+            JObject::null()
+        }
+        SerializeV0AdvertisementResult::LdtError => {
+            let _ = LdtEncryptionException::throw_new(&mut env);
+            JObject::null()
+        }
+        SerializeV0AdvertisementResult::UnencryptedError => {
+            let _ = UnencryptedSizeException::throw_new(&mut env);
+            JObject::null()
+        }
+    }
+}
+
+#[jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V0AdvertisementBuilder.V0BuilderHandle"
+)]
+extern "system" fn deallocate<'local>(
+    _env: JNIEnv<'local>,
+    _cls: JClass<'local>,
+    handle_id: jlong,
+) {
+    let handle = V0AdvertisementBuilder::from_handle(Handle::from_id(handle_id as u64));
+    let _ = handle.deallocate();
+}
diff --git a/nearby/presence/np_java_ffi/src/class/v0_broadcast_credential.rs b/nearby/presence/np_java_ffi/src/class/v0_broadcast_credential.rs
new file mode 100644
index 0000000..2da6553
--- /dev/null
+++ b/nearby/presence/np_java_ffi/src/class/v0_broadcast_credential.rs
@@ -0,0 +1,71 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+//     http://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.
+
+use jni::{
+    objects::{JByteArray, JObject},
+    signature::ReturnType,
+    JNIEnv,
+};
+
+use np_ffi_core::credentials::V0BroadcastCredential as CoreV0BroadcastCredential;
+use pourover::desc::{ClassDesc, FieldDesc};
+
+static V0_DISCOVERY_CREDENTIAL_CLS: ClassDesc =
+    ClassDesc::new("com/google/android/nearby/presence/rust/credential/V0BroadcastCredential");
+
+/// Rust representation of `class V0BroadcastCredential`.
+#[repr(transparent)]
+pub struct V0BroadcastCredential<Obj>(pub Obj);
+
+impl<'local, Obj: AsRef<JObject<'local>>> V0BroadcastCredential<Obj> {
+    /// Get an array field as a Rust array.
+    fn get_array<'env, const N: usize>(
+        &self,
+        env: &mut JNIEnv<'env>,
+        field: &FieldDesc,
+    ) -> jni::errors::Result<[u8; N]> {
+        let arr: JByteArray<'env> =
+            env.get_field_unchecked(self.0.as_ref(), field, ReturnType::Array)?.l()?.into();
+
+        let mut buf = [0; N];
+        env.get_byte_array_region(arr, 0, &mut buf[..])?;
+        Ok(buf.map(|byte| byte as u8))
+    }
+
+    /// Get the key seed.
+    pub fn get_key_seed<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<[u8; 32]> {
+        static KEY_SEED: FieldDesc = V0_DISCOVERY_CREDENTIAL_CLS.field("keySeed", "[B");
+        self.get_array(env, &KEY_SEED)
+    }
+
+    /// Get the identity token.
+    pub fn get_identity_token<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+    ) -> jni::errors::Result<[u8; 14]> {
+        static IDENTITY_TOKEN: FieldDesc = V0_DISCOVERY_CREDENTIAL_CLS.field("identityToken", "[B");
+        self.get_array(env, &IDENTITY_TOKEN)
+    }
+
+    /// Convert this to the `np_ffi_core` representation.
+    pub fn get_as_core<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+    ) -> jni::errors::Result<CoreV0BroadcastCredential> {
+        Ok(CoreV0BroadcastCredential::new(
+            self.get_key_seed(env)?,
+            self.get_identity_token(env)?.into(),
+        ))
+    }
+}
diff --git a/nearby/presence/np_java_ffi/src/class/v0_data_element.rs b/nearby/presence/np_java_ffi/src/class/v0_data_element.rs
index 759a486..44f0636 100644
--- a/nearby/presence/np_java_ffi/src/class/v0_data_element.rs
+++ b/nearby/presence/np_java_ffi/src/class/v0_data_element.rs
@@ -16,12 +16,15 @@
 
 use crate::class::IdentityKind;
 use jni::{
-    objects::{JClass, JObject},
+    objects::{JClass, JIntArray, JObject},
     signature::{Primitive, ReturnType},
     sys::{jboolean, jint, JNI_FALSE, JNI_TRUE},
     JNIEnv,
 };
-use np_ffi_core::{deserialize::v0::DeserializedV0IdentityKind, v0};
+use np_ffi_core::{
+    common::InvalidStackDataStructure, deserialize::v0::DeserializedV0IdentityKind,
+    serialize::AdvertisementBuilderKind, v0,
+};
 use pourover::desc::{ClassDesc, FieldDesc};
 
 static TX_POWER_CLASS: ClassDesc =
@@ -61,6 +64,18 @@
         )
         .and_then(|ret| ret.i())
     }
+
+    /// Get the `np_ffi_core` representation of this data element.
+    pub fn get_as_core<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+    ) -> jni::errors::Result<v0::BuildTxPowerResult> {
+        let power = self.get_tx_power(env)?;
+        let Ok(power) = i8::try_from(power) else {
+            return Ok(v0::BuildTxPowerResult::OutOfRange);
+        };
+        Ok(v0::TxPower::build_from_signed_byte(power))
+    }
 }
 
 static V0_ACTIONS_CLASS: ClassDesc =
@@ -77,7 +92,7 @@
         identity_kind: DeserializedV0IdentityKind,
         action_bits: jint,
     ) -> jni::errors::Result<Self> {
-        let identity_kind = IdentityKind::value_for_v0(env, identity_kind)?;
+        let identity_kind = IdentityKind::from(identity_kind).to_java(env)?;
 
         pourover::call_constructor!(env, &V0_ACTIONS_CLASS, "(II)V", identity_kind, action_bits)
             .map(Self)
@@ -123,25 +138,36 @@
         )
         .and_then(|ret| ret.i())
     }
+
+    /// Get the `np_ffi_core` representation of this data element. This returns `None` if the Rust
+    /// representation would not be well formed.
+    pub fn get_as_core<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+    ) -> jni::errors::Result<Option<v0::V0Actions>> {
+        let identity_kind = self.get_identity_kind(env)?;
+        let action_bits = self.get_action_bits(env)?;
+        construct_actions_from_parts(env, identity_kind, action_bits)
+    }
 }
 
-/// Helper to build a [`V0Actions`][v0::V0Actions] instance from raw Java fields.
-fn construct_actions_from_ints(
+/// Helper to build a [`V0Actions`][v0::V0Actions] instance from raw Java fields. This will return
+/// `None` if the given `identity_kind` is invalid.
+fn construct_actions_from_parts(
     env: &mut JNIEnv<'_>,
     identity_kind: jint,
     action_bits: jint,
-) -> Option<v0::V0Actions> {
-    let wrapper = if identity_kind == IdentityKind::plaintext(env).ok()? {
-        v0::V0Actions::Plaintext
-    } else if identity_kind == IdentityKind::decrypted(env).ok()? {
-        v0::V0Actions::Encrypted
-    } else {
-        return None;
+) -> jni::errors::Result<Option<v0::V0Actions>> {
+    let opt_kind = IdentityKind::from_java(env, identity_kind)?;
+
+    let wrapper = match opt_kind {
+        Some(IdentityKind::Plaintext) => v0::V0Actions::Plaintext,
+        Some(IdentityKind::Decrypted) => v0::V0Actions::Encrypted,
+        _ => return Ok(None),
     };
 
     let bits = v0::V0ActionBits::from(action_bits as u32);
-
-    Some(wrapper(bits))
+    Ok(Some(wrapper(bits)))
 }
 
 #[pourover::jni_method(
@@ -155,8 +181,18 @@
     action_bits: jint,
     action: jint,
 ) -> jboolean {
-    let Some(actions) = construct_actions_from_ints(&mut env, identity_kind, action_bits) else {
-        return JNI_FALSE;
+    let actions = match construct_actions_from_parts(&mut env, identity_kind, action_bits) {
+        Ok(Some(actions)) => actions,
+        Ok(None) => {
+            let _ = env.throw_new(
+                "java/lang/IllegalStateException",
+                "V0Actions is not validly constructed",
+            );
+            return JNI_FALSE;
+        }
+        Err(_jni_err) => {
+            return JNI_FALSE;
+        }
     };
 
     let Ok(action) = u8::try_from(action).map_err(From::from).and_then(v0::ActionType::try_from)
@@ -170,3 +206,82 @@
         JNI_FALSE
     }
 }
+
+#[pourover::jni_method(
+    package = "com.google.android.nearby.presence.rust",
+    class = "V0DataElement.V0Actions"
+)]
+extern "system" fn nativeMergeActions<'local>(
+    mut env: JNIEnv<'local>,
+    _cls: JClass<'local>,
+    identity_kind: jint,
+    actions: JIntArray<'local>,
+) -> jint {
+    let actions = {
+        let Ok(len) = env.get_array_length(&actions) else {
+            // `crate jni` should have already thrown
+            return 0;
+        };
+        let Ok(len) = usize::try_from(len) else {
+            // Should never occur
+            return 0;
+        };
+        let mut vec = vec![0; len];
+        if env.get_int_array_region(&actions, 0, &mut vec[..]).is_err() {
+            // `crate jni` should have already thrown
+            return 0;
+        };
+        vec
+    };
+
+    let mut parse_kind =
+        |identity_kind: jint| -> jni::errors::Result<Option<AdvertisementBuilderKind>> {
+            IdentityKind::from_java(&mut env, identity_kind).map(|opt_kind| {
+                opt_kind.and_then(|kind| match kind {
+                    IdentityKind::Plaintext => Some(AdvertisementBuilderKind::Public),
+                    IdentityKind::Decrypted => Some(AdvertisementBuilderKind::Encrypted),
+                    _ => None,
+                })
+            })
+        };
+
+    let adv_kind = match parse_kind(identity_kind) {
+        Ok(Some(adv_kind)) => adv_kind,
+        Ok(None) => {
+            let _ = env.throw_new("java/lang/IllegalArgumentException", "Invalid identity kind");
+            return 0;
+        }
+        Err(_jni_err) => return 0,
+    };
+
+    let mut v0_actions = v0::V0Actions::new_zeroed(adv_kind);
+    for action in actions {
+        let Ok(action_type) =
+            u8::try_from(action).map_err(Into::into).and_then(v0::ActionType::try_from)
+        else {
+            let _ = env.throw_new(
+                "java/lang/IllegalArgumentException",
+                format!("V0ActionType is invalid: {action}"),
+            );
+            return 0;
+        };
+
+        v0_actions = match v0_actions.set_action(action_type, true) {
+            Ok(v0::SetV0ActionResult::Success(v0_actions)) => v0_actions,
+            Ok(v0::SetV0ActionResult::Error(_v0_actions)) => {
+                let _ = env.throw_new(
+                    "java/lang/IllegalStateException",
+                    "V0ActionType is not valid for this IdentityKind",
+                );
+                return 0;
+            }
+            Err(InvalidStackDataStructure) => {
+                let _ = env
+                    .throw_new("java/lang/IllegalStateException", "Memory has somehow corrupted");
+                return 0;
+            }
+        };
+    }
+
+    0
+}
diff --git a/nearby/presence/np_java_ffi/src/class/v0_payload.rs b/nearby/presence/np_java_ffi/src/class/v0_payload.rs
index 9b5e05b..e962f5e 100644
--- a/nearby/presence/np_java_ffi/src/class/v0_payload.rs
+++ b/nearby/presence/np_java_ffi/src/class/v0_payload.rs
@@ -19,9 +19,49 @@
     sys::{jint, jlong},
     JNIEnv,
 };
-use np_ffi_core::deserialize::v0::{DeserializedV0IdentityKind, V0Payload};
+use np_ffi_core::deserialize::{
+    v0::{DeserializedV0IdentityDetails, DeserializedV0IdentityKind, V0Payload},
+    DecryptMetadataResult,
+};
 use np_ffi_core::v0::V0Actions as CoreV0Actions;
-use pourover::jni_method;
+use pourover::{desc::ClassDesc, jni_method};
+
+static IDENTITY_DETAILS_CLASS: ClassDesc =
+    ClassDesc::new("com/google/android/nearby/presence/rust/V0Payload$IdentityDetails");
+
+#[repr(transparent)]
+pub struct IdentityDetails<Obj>(pub Obj);
+
+impl<'local> IdentityDetails<JObject<'local>> {
+    pub fn construct(
+        env: &mut JNIEnv<'local>,
+        details: DeserializedV0IdentityDetails,
+    ) -> jni::errors::Result<Self> {
+        Self::construct_from_parts(env, details.cred_id(), details.identity_token(), details.salt())
+    }
+
+    /// Create an IdentityDetails instance
+    pub fn construct_from_parts(
+        env: &mut JNIEnv<'local>,
+        credential_id: u32,
+        identity_token: [u8; 14],
+        salt: [u8; 2],
+    ) -> jni::errors::Result<Self> {
+        let credential_id = credential_id as jint;
+        let identity_token = env.byte_array_from_slice(&identity_token)?;
+        let salt = env.byte_array_from_slice(&salt)?;
+
+        pourover::call_constructor!(
+            env,
+            &IDENTITY_DETAILS_CLASS,
+            "(I[B[B)V",
+            credential_id,
+            identity_token,
+            salt
+        )
+        .map(Self)
+    }
+}
 
 #[jni_method(package = "com.google.android.nearby.presence.rust", class = "V0Payload")]
 extern "system" fn nativeGetDataElement<'local>(
@@ -63,6 +103,44 @@
 }
 
 #[jni_method(package = "com.google.android.nearby.presence.rust", class = "V0Payload")]
+extern "system" fn nativeGetIdentityDetails<'local>(
+    mut env: JNIEnv<'local>,
+    _cls: JClass<'local>,
+    handle_id: jlong,
+) -> JObject<'local> {
+    let v0_payload = V0Payload::from_handle(Handle::from_id(handle_id as u64));
+
+    use np_ffi_core::deserialize::v0::GetV0IdentityDetailsResult::*;
+    match v0_payload.get_identity_details() {
+        Success(details) => IdentityDetails::construct(&mut env, details)
+            .map_or_else(|_err| JObject::null(), |obj| obj.0),
+        Error => JObject::null(),
+    }
+}
+
+#[jni_method(package = "com.google.android.nearby.presence.rust", class = "V0Payload")]
+extern "system" fn nativeGetDecryptedMetadata<'local>(
+    env: JNIEnv<'local>,
+    _cls: JClass<'local>,
+    handle_id: jlong,
+) -> JObject<'local> {
+    let v0_payload = V0Payload::from_handle(Handle::from_id(handle_id as u64));
+
+    let DecryptMetadataResult::Success(metadata) = v0_payload.decrypt_metadata() else {
+        return JObject::null();
+    };
+
+    let Ok(buffer) = metadata.take_buffer() else {
+        return JObject::null();
+    };
+    let Ok(decrypted_metadata_array) = env.byte_array_from_slice(&buffer[..]) else {
+        return JObject::null();
+    };
+
+    decrypted_metadata_array.into()
+}
+
+#[jni_method(package = "com.google.android.nearby.presence.rust", class = "V0Payload")]
 extern "system" fn deallocate<'local>(
     _env: JNIEnv<'local>,
     _cls: JClass<'local>,
diff --git a/nearby/presence/np_java_ffi/src/class/v1_broadcast_credential.rs b/nearby/presence/np_java_ffi/src/class/v1_broadcast_credential.rs
new file mode 100644
index 0000000..01fd30f
--- /dev/null
+++ b/nearby/presence/np_java_ffi/src/class/v1_broadcast_credential.rs
@@ -0,0 +1,78 @@
+// Copyright 2023 Google LLC
+//
+// 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
+//
+//     http://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.
+
+use jni::{
+    objects::{JByteArray, JObject},
+    signature::ReturnType,
+    JNIEnv,
+};
+
+use np_ffi_core::credentials::V1BroadcastCredential as CoreV1BroadcastCredential;
+use pourover::desc::{ClassDesc, FieldDesc};
+
+static V1_DISCOVERY_CREDENTIAL_CLS: ClassDesc =
+    ClassDesc::new("com/google/android/nearby/presence/rust/credential/V1BroadcastCredential");
+
+/// Rust representation of `class V1BroadcastCredential`.
+#[repr(transparent)]
+pub struct V1BroadcastCredential<Obj>(pub Obj);
+
+impl<'local, Obj: AsRef<JObject<'local>>> V1BroadcastCredential<Obj> {
+    /// Get an array field as a Rust array.
+    fn get_array<'env, const N: usize>(
+        &self,
+        env: &mut JNIEnv<'env>,
+        field: &FieldDesc,
+    ) -> jni::errors::Result<[u8; N]> {
+        let arr: JByteArray<'env> =
+            env.get_field_unchecked(self.0.as_ref(), field, ReturnType::Array)?.l()?.into();
+
+        let mut buf = [0; N];
+        env.get_byte_array_region(arr, 0, &mut buf[..])?;
+        Ok(buf.map(|byte| byte as u8))
+    }
+
+    /// Get the key seed.
+    pub fn get_key_seed<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<[u8; 32]> {
+        static KEY_SEED: FieldDesc = V1_DISCOVERY_CREDENTIAL_CLS.field("keySeed", "[B");
+        self.get_array(env, &KEY_SEED)
+    }
+
+    /// Get the identity token.
+    pub fn get_identity_token<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+    ) -> jni::errors::Result<[u8; 16]> {
+        static IDENTITY_TOKEN: FieldDesc = V1_DISCOVERY_CREDENTIAL_CLS.field("identityToken", "[B");
+        self.get_array(env, &IDENTITY_TOKEN)
+    }
+
+    /// Get the private key.
+    pub fn get_private_key<'env>(&self, env: &mut JNIEnv<'env>) -> jni::errors::Result<[u8; 32]> {
+        static PRIVATE_KEY: FieldDesc = V1_DISCOVERY_CREDENTIAL_CLS.field("privateKey", "[B");
+        self.get_array(env, &PRIVATE_KEY)
+    }
+
+    /// Convert this to the `np_ffi_core` representation.
+    pub fn get_as_core<'env>(
+        &self,
+        env: &mut JNIEnv<'env>,
+    ) -> jni::errors::Result<CoreV1BroadcastCredential> {
+        Ok(CoreV1BroadcastCredential::new(
+            self.get_key_seed(env)?,
+            self.get_identity_token(env)?.into(),
+            self.get_private_key(env)?,
+        ))
+    }
+}
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DecryptTests.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DecryptTests.java
new file mode 100644
index 0000000..f9a7ede
--- /dev/null
+++ b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DecryptTests.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright 2023 Google LLC
+ *
+ * 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
+ *
+ *     http://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.
+ */
+
+package com.google.android.nearby.presence.rust;
+
+import static com.google.android.nearby.presence.rust.TestData.*;
+import static com.google.common.truth.Truth.assertThat;
+
+import com.google.android.nearby.presence.rust.credential.CredentialBook;
+import org.junit.jupiter.api.Test;
+
+public class DecryptTests {
+
+  public static final class TestMetadata implements CredentialBook.MatchedMetadata {
+    public byte[] plaintextMetadata;
+    public byte[] encryptedMetadata;
+
+    public TestMetadata(byte[] plaintextMetadata, byte[] encryptedMetadata) {
+      this.plaintextMetadata = plaintextMetadata;
+      this.encryptedMetadata = encryptedMetadata;
+    }
+
+    @Override
+    public byte[] getEncryptedMetadataBytes() {
+      return encryptedMetadata;
+    }
+  }
+
+  public static final TestMetadata V0_METADATA =
+      new TestMetadata(ALICE_METADATA, V0_ENCRYPTED_ALICE_METADATA);
+  public static final TestMetadata V1_METADATA =
+      new TestMetadata(ALICE_METADATA, V1_ENCRYPTED_ALICE_METADATA);
+
+  DeserializeResult<TestMetadata> parsePrivateAdv(byte[] bytes) {
+    try (CredentialBook<TestMetadata> book =
+        CredentialBook.<TestMetadata>builder()
+            .addDiscoveryCredential(V0_CRED, V0_METADATA)
+            .addDiscoveryCredential(V1_CRED, V1_METADATA)
+            .build()) {
+      return NpAdv.deserializeAdvertisement(bytes, book);
+    }
+  }
+
+  @Test
+  void deserializeAdvertisement_v0_canParsePrivate() {
+    try (DeserializeResult result = parsePrivateAdv(V0_PRIVATE)) {
+      assertThat(result.getKind()).isEqualTo(DeserializeResult.Kind.V0_ADVERTISEMENT);
+
+      DeserializedV0Advertisement adv = result.getAsV0();
+
+      assertThat(adv).isNotNull();
+      assertThat(adv.isLegible()).isTrue();
+      assertThat(adv.getIdentity()).isEqualTo(IdentityKind.DECRYPTED);
+      assertThat(adv.getMatchedMetadata()).isSameInstanceAs(V0_METADATA);
+      assertThat(adv.getDecryptedMetadata()).isEqualTo(ALICE_METADATA);
+    }
+  }
+
+  @Test
+  void deserializeAdvertisement_v1_canParsePrivate() {
+    try (DeserializeResult result = parsePrivateAdv(V1_PRIVATE)) {
+      assertThat(result.getKind()).isEqualTo(DeserializeResult.Kind.V1_ADVERTISEMENT);
+
+      DeserializedV1Advertisement adv = result.getAsV1();
+
+      assertThat(adv).isNotNull();
+      assertThat(adv.getNumLegibleSections()).isEqualTo(1);
+      assertThat(adv.getNumUndecryptableSections()).isEqualTo(0);
+
+      DeserializedV1Section section = adv.getSection(0);
+      assertThat(section.getIdentityKind()).isEqualTo(IdentityKind.DECRYPTED);
+      assertThat(section.getMatchedMetadata()).isSameInstanceAs(V1_METADATA);
+      assertThat(section.getDecryptedMetadata()).isEqualTo(ALICE_METADATA);
+    }
+  }
+}
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DeserializeTests.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DeserializeTests.java
index d4f146e..7457edb 100644
--- a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DeserializeTests.java
+++ b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/DeserializeTests.java
@@ -16,6 +16,7 @@
 
 package com.google.android.nearby.presence.rust;
 
+import static com.google.android.nearby.presence.rust.DeserializedV1Section.VerificationMode;
 import static com.google.android.nearby.presence.rust.TestData.*;
 import static com.google.android.nearby.presence.rust.credential.CredentialBook.NoMetadata;
 import static com.google.common.truth.Truth.assertThat;
@@ -53,6 +54,8 @@
       assertThat(adv.isLegible()).isTrue();
       assertThat(adv.getIdentity()).isEqualTo(IdentityKind.PLAINTEXT);
       assertThat(adv.getDataElementCount()).isEqualTo(2);
+      assertThat(adv.getMatchedMetadata()).isNull();
+      assertThat(adv.getDecryptedMetadata()).isNull();
     }
   }
 
@@ -67,6 +70,8 @@
       assertThat(adv.isLegible()).isTrue();
       assertThat(adv.getIdentity()).isEqualTo(IdentityKind.PLAINTEXT);
       assertThat(adv.getDataElementCount()).isEqualTo(2);
+      assertThat(adv.getMatchedMetadata()).isNull();
+      assertThat(adv.getDecryptedMetadata()).isNull();
     }
   }
 
@@ -82,6 +87,10 @@
       assertThat(adv.getIdentity()).isEqualTo(IdentityKind.DECRYPTED);
       assertThat(adv.getDataElementCount()).isEqualTo(1);
       assertThat(adv.getDataElement(0)).isInstanceOf(V0DataElement.TxPower.class);
+      assertThat(adv.getIdentityToken()).isEqualTo(V0_IDENTITY_TOKEN);
+      assertThat(adv.getSalt()).asList().containsExactly((byte) 0x22, (byte) 0x22);
+      assertThat(adv.getMatchedMetadata()).isSameInstanceAs(NoMetadata.INSTANCE);
+      assertThat(adv.getDecryptedMetadata()).isNull();
     }
   }
 
@@ -152,6 +161,10 @@
       DeserializedV1Section section = adv.getSection(0);
       assertThat(section.getIdentityKind()).isEqualTo(IdentityKind.DECRYPTED);
       assertThat(section.getDataElementCount()).isEqualTo(1);
+      assertThat(section.getIdentityToken()).isEqualTo(V1_IDENTITY_TOKEN);
+      assertThat(section.getVerificationMode()).isEqualTo(VerificationMode.SIGNATURE);
+      assertThat(section.getMatchedMetadata()).isSameInstanceAs(NoMetadata.INSTANCE);
+      assertThat(section.getDecryptedMetadata()).isNull();
     }
   }
 
@@ -178,6 +191,8 @@
       assertThat(section).isNotNull();
       assertThat(section.getIdentityKind()).isEqualTo(IdentityKind.PLAINTEXT);
       assertThat(section.getDataElementCount()).isEqualTo(1);
+      assertThat(section.getMatchedMetadata()).isNull();
+      assertThat(section.getDecryptedMetadata()).isNull();
     }
   }
 
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/SerializeTests.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/SerializeTests.java
new file mode 100644
index 0000000..b8e18fd
--- /dev/null
+++ b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/SerializeTests.java
@@ -0,0 +1,183 @@
+/*
+ * Copyright 2024 Google LLC
+ *
+ * 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
+ *
+ *     http://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.
+ */
+
+package com.google.android.nearby.presence.rust;
+
+import static com.google.android.nearby.presence.rust.TestData.*;
+import static com.google.android.nearby.presence.rust.V0DataElement.TxPower;
+import static com.google.android.nearby.presence.rust.V0DataElement.V0ActionType;
+import static com.google.android.nearby.presence.rust.V0DataElement.V0Actions;
+import static com.google.android.nearby.presence.rust.credential.CredentialBook.NoMetadata;
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import com.google.android.nearby.presence.rust.credential.CredentialBook;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+
+public class SerializeTests {
+
+  public static final TxPower TX_POWER = new TxPower(7);
+  public static final TxPower INVALID_TX_POWER = new TxPower(-777);
+
+  public static final V0Actions PUBLIC_ACTIONS =
+      new V0Actions(IdentityKind.PLAINTEXT, V0ActionType.NEARBY_SHARE, V0ActionType.CROSS_DEV_SDK);
+  public static final V0Actions PRIVATE_ACTIONS =
+      new V0Actions(IdentityKind.DECRYPTED, V0ActionType.CALL_TRANSFER, V0ActionType.NEARBY_SHARE);
+
+  public static final byte[] SALT = new byte[] {0x12, 0x34};
+
+  @Test
+  void serializeAdvertisement_v0_canSerialize() throws Exception {
+    try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
+      builder.addDataElement(TX_POWER);
+      builder.addDataElement(PUBLIC_ACTIONS);
+      byte[] adv = builder.build();
+      assertThat(adv).isNotNull();
+    }
+  }
+
+  @Test
+  void serializeAdvertisement_v0_canSerializePrivate() throws Exception {
+    try (V0AdvertisementBuilder builder =
+        V0AdvertisementBuilder.newEncrypted(V0_BROADCAST_CRED, SALT)) {
+      builder.addDataElement(TX_POWER);
+      builder.addDataElement(PRIVATE_ACTIONS);
+      byte[] adv = builder.build();
+      assertThat(adv).isNotNull();
+    }
+  }
+
+  @Test
+  void serializeAdvertisement_v0_canRoundtrip() throws Exception {
+    try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic();
+        CredentialBook book = CredentialBook.empty()) {
+      builder.addDataElement(TX_POWER);
+      builder.addDataElement(PUBLIC_ACTIONS);
+      byte[] advBytes = builder.build();
+
+      DeserializeResult result = NpAdv.deserializeAdvertisement(advBytes, book);
+      DeserializedV0Advertisement adv = result.getAsV0();
+
+      assertThat(adv).isNotNull();
+      assertThat(adv.getDataElementCount()).isEqualTo(2);
+      V0DataElement de0 = adv.getDataElement(0);
+      assertThat(de0).isInstanceOf(TxPower.class);
+      TxPower txPower = (TxPower) de0;
+      assertThat(txPower.getTxPower()).isEqualTo(TX_POWER.getTxPower());
+      V0DataElement de1 = adv.getDataElement(1);
+      assertThat(de1).isInstanceOf(V0Actions.class);
+      V0Actions v0Actions = (V0Actions) de1;
+      assertThat(v0Actions.getIdentityKind()).isEqualTo(IdentityKind.PLAINTEXT);
+      assertThat(v0Actions.getActionBits()).isEqualTo(PUBLIC_ACTIONS.getActionBits());
+    }
+  }
+
+  @Test
+  void serializeAdvertisement_v0_canRoundtripPrivate() throws Exception {
+    try (V0AdvertisementBuilder builder =
+            V0AdvertisementBuilder.newEncrypted(V0_BROADCAST_CRED, SALT);
+        CredentialBook book =
+            CredentialBook.builder().addDiscoveryCredential(V0_CRED, NoMetadata.INSTANCE).build()) {
+      builder.addDataElement(TX_POWER);
+      builder.addDataElement(PRIVATE_ACTIONS);
+      byte[] advBytes = builder.build();
+
+      DeserializeResult result = NpAdv.deserializeAdvertisement(advBytes, book);
+      DeserializedV0Advertisement adv = result.getAsV0();
+
+      assertThat(adv).isNotNull();
+      assertThat(adv.getDataElementCount()).isEqualTo(2);
+      V0DataElement de0 = adv.getDataElement(0);
+      assertThat(de0).isInstanceOf(TxPower.class);
+      TxPower txPower = (TxPower) de0;
+      assertThat(txPower.getTxPower()).isEqualTo(TX_POWER.getTxPower());
+      V0DataElement de1 = adv.getDataElement(1);
+      assertThat(de1).isInstanceOf(V0Actions.class);
+      V0Actions v0Actions = (V0Actions) de1;
+      assertThat(v0Actions.getIdentityKind()).isEqualTo(IdentityKind.DECRYPTED);
+      assertThat(v0Actions.getActionBits()).isEqualTo(PRIVATE_ACTIONS.getActionBits());
+    }
+  }
+
+  @Test
+  void serializeAdvertisement_v0_emptyIsError() throws Exception {
+    try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
+      assertThrows(SerializationException.UnencryptedSizeException.class, () -> builder.build());
+    }
+  }
+
+  @Test
+  void serializeAdvertisement_v0_fullIsError() throws Exception {
+    try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
+      assertThrows(
+          SerializationException.InsufficientSpaceException.class,
+          () -> {
+            // Overfill the advertisement. Fixing b/311225033 will break this method.
+            for (int i = 0; i < 50; i++) {
+              builder.addDataElement(TX_POWER);
+            }
+          });
+    }
+  }
+
+  @Test
+  void serializeAdvertisement_v0_publicAdvPrivateActionsIsError() throws Exception {
+    try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
+      assertThrows(
+          SerializationException.InvalidDataElementException.class,
+          () -> builder.addDataElement(PRIVATE_ACTIONS));
+    }
+  }
+
+  @Test
+  void serializeAdvertisement_v0_privateAdvPublicActionsIsError() throws Exception {
+    try (V0AdvertisementBuilder builder =
+        V0AdvertisementBuilder.newEncrypted(V0_BROADCAST_CRED, SALT)) {
+      assertThrows(
+          SerializationException.InvalidDataElementException.class,
+          () -> builder.addDataElement(PUBLIC_ACTIONS));
+    }
+  }
+
+  @Test
+  void serializeAdvertisement_v0_invalidTxPowerIsError() throws Exception {
+    try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
+      assertThrows(
+          SerializationException.InvalidDataElementException.class,
+          () -> builder.addDataElement(INVALID_TX_POWER));
+    }
+  }
+
+  @Test
+  void serializeAdvertisement_v0_handleIsConsumedByBuild() throws Exception {
+    try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
+      builder.addDataElement(TX_POWER);
+      byte[] adv = builder.build();
+      assertThrows(Handle.InvalidHandleException.class, () -> builder.addDataElement(TX_POWER));
+      assertThrows(Handle.InvalidHandleException.class, () -> builder.build());
+    }
+  }
+
+  @Test
+  @Disabled("b/311225033: Duplicate data element spec change not implemented")
+  void serializeAdvertisement_v0_deNotAddedTwice() throws Exception {
+    try (V0AdvertisementBuilder builder = V0AdvertisementBuilder.newPublic()) {
+      builder.addDataElement(TX_POWER);
+      assertThrows(Exception.class, () -> builder.addDataElement(TX_POWER));
+    }
+  }
+}
diff --git a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/TestData.java b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/TestData.java
index 8ee26a1..122b2a9 100644
--- a/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/TestData.java
+++ b/nearby/presence/np_java_ffi/test/com/google/android/nearby/presence/rust/TestData.java
@@ -16,6 +16,7 @@
 
 package com.google.android.nearby.presence.rust;
 
+import com.google.android.nearby.presence.rust.credential.V0BroadcastCredential;
 import com.google.android.nearby.presence.rust.credential.V0DiscoveryCredential;
 import com.google.android.nearby.presence.rust.credential.V1DiscoveryCredential;
 
@@ -34,6 +35,173 @@
     0x15, 0x06, // tx power value 6
   };
 
+  public static final byte[] ALICE_METADATA = {
+    (byte) 0x7b,
+    (byte) 0x22,
+    (byte) 0x6e,
+    (byte) 0x61,
+    (byte) 0x6d,
+    (byte) 0x65,
+    (byte) 0x22,
+    (byte) 0x3a,
+    (byte) 0x22,
+    (byte) 0x41,
+    (byte) 0x6c,
+    (byte) 0x69,
+    (byte) 0x63,
+    (byte) 0x65,
+    (byte) 0x22,
+    (byte) 0x2c,
+    (byte) 0x22,
+    (byte) 0x65,
+    (byte) 0x6d,
+    (byte) 0x61,
+    (byte) 0x69,
+    (byte) 0x6c,
+    (byte) 0x22,
+    (byte) 0x3a,
+    (byte) 0x22,
+    (byte) 0x61,
+    (byte) 0x6c,
+    (byte) 0x69,
+    (byte) 0x63,
+    (byte) 0x65,
+    (byte) 0x40,
+    (byte) 0x67,
+    (byte) 0x6d,
+    (byte) 0x61,
+    (byte) 0x69,
+    (byte) 0x6c,
+    (byte) 0x2e,
+    (byte) 0x63,
+    (byte) 0x6f,
+    (byte) 0x6d,
+    (byte) 0x22,
+    (byte) 0x7d
+  };
+
+  public static final byte[] V0_ENCRYPTED_ALICE_METADATA = {
+    (byte) 0x8d,
+    (byte) 0xd4,
+    (byte) 0x7a,
+    (byte) 0x58,
+    (byte) 0x47,
+    (byte) 0x30,
+    (byte) 0x65,
+    (byte) 0x89,
+    (byte) 0x06,
+    (byte) 0x16,
+    (byte) 0x04,
+    (byte) 0xca,
+    (byte) 0x68,
+    (byte) 0xce,
+    (byte) 0xb4,
+    (byte) 0x18,
+    (byte) 0xa7,
+    (byte) 0x40,
+    (byte) 0xb8,
+    (byte) 0x36,
+    (byte) 0xb0,
+    (byte) 0x94,
+    (byte) 0xb4,
+    (byte) 0xc0,
+    (byte) 0x6c,
+    (byte) 0xaf,
+    (byte) 0x7c,
+    (byte) 0x4a,
+    (byte) 0x3b,
+    (byte) 0x68,
+    (byte) 0x28,
+    (byte) 0x54,
+    (byte) 0x8f,
+    (byte) 0x36,
+    (byte) 0xd5,
+    (byte) 0x68,
+    (byte) 0x12,
+    (byte) 0x6c,
+    (byte) 0xc2,
+    (byte) 0x63,
+    (byte) 0x31,
+    (byte) 0x4a,
+    (byte) 0x48,
+    (byte) 0xe4,
+    (byte) 0x3b,
+    (byte) 0xb3,
+    (byte) 0xcf,
+    (byte) 0xd4,
+    (byte) 0x10,
+    (byte) 0x49,
+    (byte) 0x59,
+    (byte) 0xe7,
+    (byte) 0x2b,
+    (byte) 0xde,
+    (byte) 0xeb,
+    (byte) 0x78,
+    (byte) 0x44,
+    (byte) 0x68
+  };
+
+  public static final byte[] V1_ENCRYPTED_ALICE_METADATA = {
+    (byte) 0x0c,
+    (byte) 0x27,
+    (byte) 0x43,
+    (byte) 0x58,
+    (byte) 0xb8,
+    (byte) 0xb9,
+    (byte) 0x90,
+    (byte) 0x72,
+    (byte) 0xb1,
+    (byte) 0xe8,
+    (byte) 0xba,
+    (byte) 0x7d,
+    (byte) 0x8e,
+    (byte) 0xdb,
+    (byte) 0xac,
+    (byte) 0x7e,
+    (byte) 0x15,
+    (byte) 0xd5,
+    (byte) 0x05,
+    (byte) 0x30,
+    (byte) 0x4d,
+    (byte) 0xb7,
+    (byte) 0xe0,
+    (byte) 0xbd,
+    (byte) 0x8f,
+    (byte) 0xe9,
+    (byte) 0xab,
+    (byte) 0x48,
+    (byte) 0x66,
+    (byte) 0xac,
+    (byte) 0x2c,
+    (byte) 0x8a,
+    (byte) 0x24,
+    (byte) 0x0c,
+    (byte) 0x02,
+    (byte) 0xb4,
+    (byte) 0xd0,
+    (byte) 0xb7,
+    (byte) 0x10,
+    (byte) 0xd6,
+    (byte) 0x30,
+    (byte) 0x12,
+    (byte) 0xd6,
+    (byte) 0x98,
+    (byte) 0xdc,
+    (byte) 0x55,
+    (byte) 0xae,
+    (byte) 0xb0,
+    (byte) 0x9c,
+    (byte) 0x9a,
+    (byte) 0x7d,
+    (byte) 0x2e,
+    (byte) 0xcb,
+    (byte) 0xba,
+    (byte) 0xdd,
+    (byte) 0x7d,
+    (byte) 0x4e,
+    (byte) 0xd1
+  };
+
   public static final byte[] V0_KEY_SEED = {
     0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
     0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
@@ -42,8 +210,6 @@
 
   public static final byte[] V0_IDENTITY_TOKEN = {
     0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
-    0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x33,
-    0x33, 0x33, 0x33, 0x33
   };
 
   public static final byte[] V0_IDENTITY_TOKEN_HMAC = {
@@ -84,6 +250,9 @@
   public static final V0DiscoveryCredential V0_CRED =
       new V0DiscoveryCredential(V0_KEY_SEED, V0_IDENTITY_TOKEN_HMAC);
 
+  public static final V0BroadcastCredential V0_BROADCAST_CRED =
+      new V0BroadcastCredential(V0_KEY_SEED, V0_IDENTITY_TOKEN);
+
   public static final byte[] V0_PRIVATE = {
     0x04, // adv header
     0x22,
diff --git a/nearby/src/ffi.rs b/nearby/src/ffi.rs
index ac0c585..3427747 100644
--- a/nearby/src/ffi.rs
+++ b/nearby/src/ffi.rs
@@ -12,99 +12,110 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-use crate::{crypto_ffi, CargoOptions};
+use crate::CargoOptions;
 use cmd_runner::{run_cmd_shell, run_cmd_shell_with_color, YellowStderr};
 use std::{fs, path};
 
 // wrapper for checking all ffi related things
-pub fn check_ffi(root: &path::Path, cargo_options: &CargoOptions) -> anyhow::Result<()> {
-    crypto_ffi::check_boringssl(root, cargo_options)?;
-    check_np_ffi_cmake(root, cargo_options)?;
-    check_ldt_cmake(root, cargo_options)?;
-
+pub(crate) fn check_all_ffi(
+    root: &path::Path,
+    cargo_options: &CargoOptions,
+    boringssl_enabled: bool,
+) -> anyhow::Result<()> {
+    check_np_ffi_cmake(root, cargo_options, boringssl_enabled)?;
+    check_ldt_cmake(root, cargo_options, boringssl_enabled)?;
+    check_ukey2_cmake(root, cargo_options, boringssl_enabled)?;
     Ok(())
 }
 
-pub fn check_np_ffi_cmake(root: &path::Path, cargo_options: &CargoOptions) -> anyhow::Result<()> {
+pub(crate) fn check_np_ffi_cmake(
+    root: &path::Path,
+    cargo_options: &CargoOptions,
+    boringssl_enabled: bool,
+) -> anyhow::Result<()> {
     log::info!("Checking CMake build and tests for np ffi c/c++ code");
-    let build_dir = root.join("presence/cmake-build");
-    fs::create_dir_all(&build_dir)?;
-
     let locked_arg = if cargo_options.locked { "--locked" } else { "" };
+    let features =
+        if boringssl_enabled { "--no-default-features --features boringssl" } else { "" };
+    run_cmd_shell(root, format!("cargo build  -p np_c_ffi {locked_arg} {features} --release"))?;
 
+    let build_dir = root.join("cmake-build");
+    fs::create_dir_all(&build_dir)?;
     run_cmd_shell_with_color::<YellowStderr>(
         &build_dir,
         "cmake -G Ninja -DENABLE_TESTS=true -DCMAKE_BUILD_TYPE=Release -DENABLE_FUZZ=false ..",
     )?;
-
-    // verify sample and benchmarks build
-    let np_ffi_crate_dir = root.to_path_buf().join("presence/np_c_ffi");
-    run_cmd_shell(&np_ffi_crate_dir, format!("cargo build {locked_arg} --release"))?;
     run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake --build . --target np_cpp_sample")?;
     run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake --build . --target np_ffi_bench")?;
 
-    // Run tests with different crypto backends
-    let tests_dir = build_dir.join("np_cpp_ffi/tests");
-    for build_config in [
-        // test with default build settings (rustcrypto)
-        format!("build {locked_arg} --quiet --release"),
-        // test with boringssl
-        format!("build {locked_arg} --quiet --no-default-features --features=boringssl"),
-    ] {
-        let _ = run_cmd_shell_with_color::<YellowStderr>(
-            &build_dir,
-            "rm np_cpp_ffi/tests/np_ffi_tests",
-        );
-        run_cmd_shell(&np_ffi_crate_dir, format!("cargo {}", build_config))?;
-        run_cmd_shell_with_color::<YellowStderr>(
-            &build_dir,
-            "cmake --build . --target np_ffi_tests",
-        )?;
-        run_cmd_shell_with_color::<YellowStderr>(&tests_dir, "ctest")?;
-    }
-
+    // Force detection of updated static lib if previous version was already built
+    let test_dir = build_dir.join("presence/np_cpp_ffi/tests");
+    let _ = run_cmd_shell_with_color::<YellowStderr>(&test_dir, "rm np_ffi_tests");
+    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake --build . --target np_ffi_tests")?;
+    run_cmd_shell_with_color::<YellowStderr>(&test_dir, "ctest")?;
     Ok(())
 }
 
-pub fn check_ldt_cmake(root: &path::Path, cargo_options: &CargoOptions) -> anyhow::Result<()> {
+pub(crate) fn check_ldt_cmake(
+    root: &path::Path,
+    cargo_options: &CargoOptions,
+    boringssl_enabled: bool,
+) -> anyhow::Result<()> {
     log::info!("Checking CMake build and tests for ldt c/c++ code");
-    let build_dir = root.join("presence/cmake-build");
-    fs::create_dir_all(&build_dir)?;
-
     let locked_arg = if cargo_options.locked { "--locked" } else { "" };
+    let features =
+        if boringssl_enabled { "--no-default-features --features boringssl" } else { "" };
+    run_cmd_shell(
+        root,
+        format!("cargo build {locked_arg} {features} -p ldt_np_adv_ffi --quiet --release"),
+    )?;
 
+    let build_dir = root.join("cmake-build");
+    fs::create_dir_all(&build_dir)?;
     run_cmd_shell_with_color::<YellowStderr>(
         &build_dir,
         "cmake -G Ninja -DENABLE_TESTS=true -DCMAKE_BUILD_TYPE=Release -DENABLE_FUZZ=false ..",
     )?;
-
-    run_cmd_shell(root, format!("cargo build {locked_arg} -p ldt_np_adv_ffi --quiet --release"))?;
     run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake --build . --target ldt_c_sample")?;
     run_cmd_shell_with_color::<YellowStderr>(
         &build_dir,
         "cmake --build . --target ldt_benchmarks",
     )?;
+    // Force detection of updated static lib if previous version was already built
+    let test_dir = build_dir.join("presence/ldt_np_adv_ffi/c/tests");
+    let _ = run_cmd_shell_with_color::<YellowStderr>(&test_dir, "rm ldt_ffi_tests");
+    run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake --build . --target ldt_ffi_tests")?;
+    run_cmd_shell_with_color::<YellowStderr>(&test_dir, "ctest")?;
+    Ok(())
+}
 
-    // Run the LDT ffi unit tests. These are rebuilt and tested against all of the different
-    // Cargo build configurations based on the feature flags.
-    let ldt_tests_dir = build_dir.join("ldt_np_adv_ffi/c/tests");
-    for build_config in [
-        // test with default build settings (rustcrypto, no_std)
-        format!("build {locked_arg} --quiet --release"),
-        // test with boringssl crypto feature flag
-        format!("build {locked_arg} --quiet --no-default-features --features boringssl --release"),
-        // test without defaults and std feature flag
-        format!("build {locked_arg} --quiet --no-default-features --features std --release"),
-    ] {
-        run_cmd_shell(root, format!("cargo {} -p ldt_np_adv_ffi", build_config))?;
-        // Force detection of updated `ldt_np_adv_ffi` static lib
-        let _ = run_cmd_shell_with_color::<YellowStderr>(&ldt_tests_dir, "rm ldt_ffi_tests");
-        run_cmd_shell_with_color::<YellowStderr>(
-            &build_dir,
-            "cmake --build . --target ldt_ffi_tests",
-        )?;
-        run_cmd_shell_with_color::<YellowStderr>(&ldt_tests_dir, "ctest")?;
-    }
+pub(crate) fn check_ukey2_cmake(
+    root: &path::Path,
+    cargo_options: &CargoOptions,
+    boringssl_enabled: bool,
+) -> anyhow::Result<()> {
+    log::info!("Checking Ukey2 ffi");
 
+    let locked_arg = if cargo_options.locked { "--locked" } else { "" };
+    let features =
+        if boringssl_enabled { "--no-default-features --features boringssl" } else { "" };
+    run_cmd_shell(
+        root,
+        format!("cargo build -p ukey2_c_ffi {locked_arg} {features} --quiet --release --lib"),
+    )?;
+
+    let build_dir = root.join("cmake-build");
+    fs::create_dir_all(&build_dir)?;
+    run_cmd_shell_with_color::<YellowStderr>(
+        &build_dir,
+        "cmake -G Ninja -DENABLE_TESTS=true -DENABLE_FUZZ=false ..",
+    )?;
+    let test_dir = build_dir.join("connections/ukey2/ukey2_c_ffi/cpp");
+    let _ = run_cmd_shell_with_color::<YellowStderr>(&test_dir, "rm ukey2_ffi_test");
+    run_cmd_shell_with_color::<YellowStderr>(
+        &build_dir,
+        "cmake --build . --target ukey2_ffi_test",
+    )?;
+    run_cmd_shell_with_color::<YellowStderr>(&test_dir, "ctest")?;
     Ok(())
 }
diff --git a/nearby/src/fuzzers.rs b/nearby/src/fuzzers.rs
index fa4f21e..e809518 100644
--- a/nearby/src/fuzzers.rs
+++ b/nearby/src/fuzzers.rs
@@ -63,11 +63,9 @@
 // Runs the fuzztest fuzzers as short lived unit tests, compatible with gtest
 pub(crate) fn build_fuzztest_unit_tests(root: &path::Path) -> anyhow::Result<()> {
     log::info!("Checking fuzztest targets in unit test mode");
-
-    // first build the rust static libs to link against
-    let np_ffi_crate_dir = root.join("presence/np_c_ffi");
-    run_cmd_shell(&np_ffi_crate_dir, "cargo build --release")?;
-    let build_dir = root.join("presence/cmake-build");
+    run_cmd_shell(root, "cargo build -p np_c_ffi --release")?;
+    run_cmd_shell(root, "cargo build -p ldt_np_adv_ffi --release")?;
+    let build_dir = root.join("cmake-build");
     fs::create_dir_all(&build_dir)?;
     run_cmd_shell_with_color::<YellowStderr>(&build_dir, "cmake -G Ninja -DENABLE_FUZZ=true ..")?;
 
@@ -78,7 +76,13 @@
         )?;
     }
 
-    run_cmd_shell_with_color::<YellowStderr>(&build_dir.join("np_cpp_ffi/fuzz/"), "ctest")?;
-    run_cmd_shell_with_color::<YellowStderr>(&build_dir.join("ldt_np_adv_ffi/c/fuzz/"), "ctest")?;
+    run_cmd_shell_with_color::<YellowStderr>(
+        &build_dir.join("presence/np_cpp_ffi/fuzz/"),
+        "ctest",
+    )?;
+    run_cmd_shell_with_color::<YellowStderr>(
+        &build_dir.join("presence/ldt_np_adv_ffi/c/fuzz/"),
+        "ctest",
+    )?;
     Ok(())
 }
diff --git a/nearby/src/jni.rs b/nearby/src/jni.rs
index 54aafdc..4ef3590 100644
--- a/nearby/src/jni.rs
+++ b/nearby/src/jni.rs
@@ -15,13 +15,8 @@
 use cmd_runner::run_cmd_shell;
 use std::path;
 
-// This has to happen after both boringssl has been built and prepare rust openssl patches has been run.
-pub fn check_ldt_jni(root: &path::Path) -> anyhow::Result<()> {
-    run_cmd_shell(root, "cargo build -p ldt_np_jni --no-default-features --features=boringssl")?;
-    Ok(())
-}
-
-pub fn run_kotlin_tests(root: &path::Path) -> anyhow::Result<()> {
+pub fn run_ldt_kotlin_tests(root: &path::Path) -> anyhow::Result<()> {
+    run_cmd_shell(root, "cargo build -p ldt_np_jni")?;
     let kotlin_lib_path = root.to_path_buf().join("presence/ldt_np_jni/java/LdtNpJni");
     run_cmd_shell(&kotlin_lib_path, "./gradlew :test")?;
     Ok(())
@@ -36,7 +31,7 @@
 
 pub fn run_np_java_ffi_tests(root: &path::Path) -> anyhow::Result<()> {
     run_cmd_shell(root, "cargo build -p np_java_ffi -F crypto_provider_default/rustcrypto")?;
-    let ukey2_jni_path = root.to_path_buf().join("presence/np_java_ffi");
-    run_cmd_shell(&ukey2_jni_path, "./gradlew :test --info --rerun")?;
+    let np_java_path = root.to_path_buf().join("presence/np_java_ffi");
+    run_cmd_shell(&np_java_path, "./gradlew :test --info --rerun")?;
     Ok(())
 }
diff --git a/nearby/src/main.rs b/nearby/src/main.rs
index 25fe348..6fe4263 100644
--- a/nearby/src/main.rs
+++ b/nearby/src/main.rs
@@ -14,6 +14,7 @@
 
 extern crate core;
 
+use anyhow::anyhow;
 use clap::Parser as _;
 use cmd_runner::{license_checker::LicenseSubcommand, run_cmd, run_cmd_shell, YellowStderr};
 use env_logger::Env;
@@ -25,7 +26,6 @@
 mod fuzzers;
 mod jni;
 mod license;
-mod ukey2;
 
 fn main() -> anyhow::Result<()> {
     env_logger::Builder::from_env(Env::default().default_filter_or("info")).init();
@@ -46,10 +46,12 @@
             ));
         }
         Subcommand::VerifyCi { ref check_options } => verify_ci(&root_dir, check_options)?,
+        Subcommand::RunAllChecks { ref check_options } => run_all_checks(&root_dir, check_options)?,
         Subcommand::CleanEverything => clean_everything(&root_dir)?,
         Subcommand::CheckFormat(ref options) => check_format(&root_dir, options)?,
         Subcommand::CheckWorkspace(ref options) => check_workspace(&root_dir, options)?,
-        Subcommand::CheckAllFfi(ref options) => ffi::check_ffi(&root_dir, options)?,
+        Subcommand::CheckAllFfi(ref options) => ffi::check_all_ffi(&root_dir, options, false)?,
+        Subcommand::BazelBuild => bazel_build(&root_dir)?,
         Subcommand::BuildBoringssl => crypto_ffi::build_boringssl(&root_dir)?,
         Subcommand::CheckBoringssl(ref options) => crypto_ffi::check_boringssl(&root_dir, options)?,
         Subcommand::CheckBoringsslAtLatest(ref options) => {
@@ -60,18 +62,29 @@
         Subcommand::License(license_subcommand) => {
             license_subcommand.run(&LICENSE_CHECKER, &root_dir)?
         }
-        Subcommand::CheckUkey2Ffi(ref options) => ukey2::check_ukey2_ffi(&root_dir, options)?,
+        Subcommand::CheckUkey2Ffi(ref options) => {
+            ffi::check_ukey2_cmake(&root_dir, options, false)?
+        }
         Subcommand::RunUkey2JniTests => jni::run_ukey2_jni_tests(&root_dir)?,
         Subcommand::RunNpJavaFfiTests => jni::run_np_java_ffi_tests(&root_dir)?,
-        Subcommand::CheckLdtJni => jni::check_ldt_jni(&root_dir)?,
-        Subcommand::CheckLdtCmake(ref options) => ffi::check_ldt_cmake(&root_dir, options)?,
-        Subcommand::CheckNpFfiCmake(ref options) => ffi::check_np_ffi_cmake(&root_dir, options)?,
-        Subcommand::RunKotlinTests => jni::run_kotlin_tests(&root_dir)?,
+        Subcommand::CheckLdtCmake(ref options) => ffi::check_ldt_cmake(&root_dir, options, false)?,
+        Subcommand::CheckNpFfiCmake(ref options) => {
+            ffi::check_np_ffi_cmake(&root_dir, options, false)?
+        }
+        Subcommand::RunLdtKotlinTests => jni::run_ldt_kotlin_tests(&root_dir)?,
     }
 
     Ok(())
 }
 
+fn bazel_build(root: &path::Path) -> anyhow::Result<()> {
+    let beto_rust_root =
+        root.parent().ok_or_else(|| anyhow!("project root dir has no parent dir"))?;
+    run_cmd_shell(beto_rust_root, "bazel build ldt_np_adv_ffi")?;
+    run_cmd_shell(beto_rust_root, "bazel build np_cpp_sample")?;
+    Ok(())
+}
+
 fn check_format(root: &path::Path, options: &FormatterOptions) -> anyhow::Result<()> {
     // Rust format
     {
@@ -150,31 +163,55 @@
     Ok(())
 }
 
-/// Runs default checks that are suiable for verifying a local change.
-pub fn run_default_checks(root: &path::Path, check_options: &CheckOptions) -> anyhow::Result<()> {
-    license::LICENSE_CHECKER.check(root)?;
-    check_workspace(root, check_options)?;
-    crypto_ffi::check_boringssl(root, &check_options.cargo_options)?;
-    ffi::check_ffi(root, &check_options.cargo_options)?;
-    if !cfg!(target_os = "windows") {
-        fuzzers::build_fuzztest_unit_tests(root)?;
-    }
-    crypto_ffi::check_boringssl_at_head(root, &check_options.cargo_options)?;
-    ukey2::check_ukey2_ffi(root, &check_options.cargo_options)?;
-    if !cfg!(target_os = "windows") {
-        // Test below requires Java SE 9, but on Windows we only have Java SE 8 installed
+pub fn verify_ci(root: &path::Path, check_options: &crate::CheckOptions) -> anyhow::Result<()> {
+    if cfg!(target_os = "linux") {
+        run_all_checks(root, check_options)?;
+    } else if cfg!(target_os = "macos") {
+        license::LICENSE_CHECKER.check(root)?;
+        check_workspace(root, check_options)?;
+        ffi::check_all_ffi(root, &check_options.cargo_options, false)?;
         jni::run_np_java_ffi_tests(root)?;
+        jni::run_ldt_kotlin_tests(root)?;
+        jni::run_ukey2_jni_tests(root)?;
+    } else if cfg!(windows) {
+        license::LICENSE_CHECKER.check(root)?;
+        check_workspace(root, check_options)?;
+        ffi::check_all_ffi(root, &check_options.cargo_options, false)?;
+    } else {
+        panic!("Unsupported OS for running CI!")
     }
-    jni::run_ukey2_jni_tests(root)?;
     Ok(())
 }
 
 /// Runs checks to ensure lints are passing and all targets are building
-pub fn verify_ci(root: &path::Path, check_options: &CheckOptions) -> anyhow::Result<()> {
+pub fn run_all_checks(root: &path::Path, check_options: &CheckOptions) -> anyhow::Result<()> {
     run_default_checks(root, check_options)?;
     Ok(())
 }
 
+/// Runs default checks that are suiable for verifying a local change.
+pub fn run_default_checks(root: &path::Path, check_options: &CheckOptions) -> anyhow::Result<()> {
+    license::LICENSE_CHECKER.check(root)?;
+    check_workspace(root, check_options)?;
+    // ffi build and run C++ tests against rustcrypto
+    ffi::check_all_ffi(root, &check_options.cargo_options, false)?;
+
+    // ffi build and run tests against boringssl
+    crypto_ffi::check_boringssl(root, &check_options.cargo_options)?;
+    ffi::check_all_ffi(root, &check_options.cargo_options, true)?;
+
+    if !cfg!(target_os = "windows") {
+        fuzzers::build_fuzztest_unit_tests(root)?;
+    }
+    if !cfg!(target_os = "windows") {
+        // Test below requires Java SE 9, but on Windows we only have Java SE 8 installed
+        jni::run_np_java_ffi_tests(root)?;
+    }
+    jni::run_ldt_kotlin_tests(root)?;
+    jni::run_ukey2_jni_tests(root)?;
+    Ok(())
+}
+
 pub fn clean_everything(root: &path::Path) -> anyhow::Result<()> {
     run_cmd_shell(root, "cargo clean")?;
     run_cmd_shell(&root.join("presence/np_c_ffi"), "cargo clean")?;
@@ -192,11 +229,18 @@
 
 #[derive(clap::Subcommand, Debug, Clone)]
 enum Subcommand {
-    /// Runs all of the checks that CI runs
+    /// Runs all the checks that CI runs
     VerifyCi {
         #[command(flatten)]
         check_options: CheckOptions,
     },
+    /// Runs all available checks, including ones that are not normally run on CI
+    RunAllChecks {
+        #[command(flatten)]
+        check_options: CheckOptions,
+    },
+    /// Builds all the bazel targets
+    BazelBuild,
     /// Runs the default set of checks suitable for verifying local changes.
     RunDefaultChecks(CheckOptions),
     /// Cleans the main workspace and all sub projects - useful if upgrading rust compiler version
@@ -227,10 +271,8 @@
     License(LicenseSubcommand),
     /// Builds and runs tests for the UKEY2 FFI
     CheckUkey2Ffi(CargoOptions),
-    /// Checks the build of ldt_jni wrapper with non default features, ie boringssl
-    CheckLdtJni,
     /// Runs the kotlin tests of the LDT Jni API
-    RunKotlinTests,
+    RunLdtKotlinTests,
     /// Checks the build of the ukey2_jni wrapper and runs tests
     RunUkey2JniTests,
     /// Checks the build of the np_java_ffi wrapper and runs tests
diff --git a/nearby/src/ukey2.rs b/nearby/src/ukey2.rs
deleted file mode 100644
index 5e711a1..0000000
--- a/nearby/src/ukey2.rs
+++ /dev/null
@@ -1,45 +0,0 @@
-// Copyright 2023 Google LLC
-//
-// 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
-//
-//     http://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.
-
-use cmd_runner::{run_cmd_shell, run_cmd_shell_with_color, YellowStderr};
-use std::{fs, path};
-
-use crate::CargoOptions;
-
-pub(crate) fn check_ukey2_ffi(
-    root: &path::Path,
-    cargo_options: &CargoOptions,
-) -> anyhow::Result<()> {
-    log::info!("Checking Ukey2 ffi");
-    let ffi_dir = root.join("connections/ukey2/ukey2_c_ffi");
-
-    let locked_arg = if cargo_options.locked { "--locked" } else { "" };
-
-    // Default build, RustCrypto
-    run_cmd_shell(&ffi_dir, format!("cargo build {locked_arg} --quiet --release --lib"))?;
-    // BoringSSL
-    run_cmd_shell(
-        &ffi_dir,
-        format!("cargo build {locked_arg} --quiet --no-default-features --features=boringssl"),
-    )?;
-    run_cmd_shell(&ffi_dir, "cargo clippy --no-default-features --features=boringssl")?;
-
-    // now run cmake build
-    let ffi_build_dir = ffi_dir.join("cpp/build");
-    fs::create_dir_all(&ffi_build_dir)?;
-    run_cmd_shell_with_color::<YellowStderr>(&ffi_build_dir, "cmake ..")?;
-    run_cmd_shell_with_color::<YellowStderr>(&ffi_build_dir, "cmake --build .")?;
-    run_cmd_shell_with_color::<YellowStderr>(&ffi_build_dir, "ctest")?;
-    Ok(())
-}
diff --git a/remoteauth/Cargo.lock b/remoteauth/Cargo.lock
index 165b375..7e17954 100644
--- a/remoteauth/Cargo.lock
+++ b/remoteauth/Cargo.lock
@@ -28,47 +28,48 @@
 
 [[package]]
 name = "anstream"
-version = "0.6.13"
+version = "0.6.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb"
+checksum = "418c75fa768af9c03be99d17643f93f79bbba589895012a80e3452a19ddda15b"
 dependencies = [
  "anstyle",
  "anstyle-parse",
  "anstyle-query",
  "anstyle-wincon",
  "colorchoice",
+ "is_terminal_polyfill",
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle"
-version = "1.0.6"
+version = "1.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc"
+checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.3"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
+checksum = "c03a11a9034d92058ceb6ee011ce58af4a9bf61491aa7e1e59ecd24bd40d22d4"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.0.2"
+version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
+checksum = "a64c907d4e79225ac72e2a354c9ce84d50ebb4586dee56c82b3ee73004f537f5"
 dependencies = [
  "windows-sys",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.2"
+version = "3.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
+checksum = "61a38449feb7068f52bb06c12759005cf459ee52bb4adc1d5a7c4322d716fb19"
 dependencies = [
  "anstyle",
  "windows-sys",
@@ -76,15 +77,15 @@
 
 [[package]]
 name = "anyhow"
-version = "1.0.82"
+version = "1.0.83"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
+checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3"
 
 [[package]]
 name = "autocfg"
-version = "1.2.0"
+version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
 
 [[package]]
 name = "bstr"
@@ -115,9 +116,9 @@
 
 [[package]]
 name = "cc"
-version = "1.0.94"
+version = "1.0.97"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17f6e324229dc011159fcc089755d1e2e216a90d43a7dea6853ca740b84f35e7"
+checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
 
 [[package]]
 name = "cfg-if"
@@ -196,9 +197,9 @@
 
 [[package]]
 name = "colorchoice"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
+checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422"
 
 [[package]]
 name = "core-foundation-sys"
@@ -361,6 +362,12 @@
 ]
 
 [[package]]
+name = "is_terminal_polyfill"
+version = "1.70.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8478577c03552c21db0e2724ffb8986a5ce7af88107e6be5d2ee6e158c12800"
+
+[[package]]
 name = "itoa"
 version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -383,9 +390,9 @@
 
 [[package]]
 name = "libc"
-version = "0.2.153"
+version = "0.2.154"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
+checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
 
 [[package]]
 name = "license"
@@ -412,9 +419,9 @@
 
 [[package]]
 name = "num-traits"
-version = "0.2.18"
+version = "0.2.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
 dependencies = [
  "autocfg",
 ]
@@ -440,9 +447,9 @@
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.81"
+version = "1.0.82"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d1597b0c024618f09a9c3b8655b7e430397a36d23fdafec26d6965e9eec3eba"
+checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
 dependencies = [
  "unicode-ident",
 ]
@@ -503,9 +510,9 @@
 
 [[package]]
 name = "ryu"
-version = "1.0.17"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
 
 [[package]]
 name = "same-file"
@@ -518,18 +525,18 @@
 
 [[package]]
 name = "serde"
-version = "1.0.198"
+version = "1.0.200"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
+checksum = "ddc6f9cc94d67c0e21aaf7eda3a010fd3af78ebf6e096aa6e2e13c79749cce4f"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.198"
+version = "1.0.200"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
+checksum = "856f046b9400cee3c8c94ed572ecdb752444c24528c035cd35882aad6f492bcb"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -561,9 +568,9 @@
 
 [[package]]
 name = "syn"
-version = "2.0.60"
+version = "2.0.61"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "909518bc7b1c9b779f1bbf07f2929d35af9f0f37e47c6e9ef7f9dddc1e1821f3"
+checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -581,18 +588,18 @@
 
 [[package]]
 name = "thiserror"
-version = "1.0.58"
+version = "1.0.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
+checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "1.0.58"
+version = "1.0.60"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
+checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -682,37 +689,15 @@
 checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
 
 [[package]]
-name = "winapi"
-version = "0.3.9"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
-dependencies = [
- "winapi-i686-pc-windows-gnu",
- "winapi-x86_64-pc-windows-gnu",
-]
-
-[[package]]
-name = "winapi-i686-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
-
-[[package]]
 name = "winapi-util"
-version = "0.1.6"
+version = "0.1.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596"
+checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
 dependencies = [
- "winapi",
+ "windows-sys",
 ]
 
 [[package]]
-name = "winapi-x86_64-pc-windows-gnu"
-version = "0.4.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
-
-[[package]]
 name = "windows-core"
 version = "0.52.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"