Merge branch 'main' into wt/t_8b24e067
Some checks failed
ci / test (pull_request) Failing after 24s
Some checks failed
ci / test (pull_request) Failing after 24s
This commit is contained in:
commit
deee357772
4 changed files with 248 additions and 22 deletions
|
|
@ -208,7 +208,7 @@ If you set `ONIONWIRE_HOME`, delete that directory instead.
|
||||||
|
|
||||||
## Seized laptop
|
## Seized laptop
|
||||||
|
|
||||||
Chat bodies in sqlite are ChaCha20-Poly1305 (`nonce || ciphertext` in `messages.plaintext`), wrapped by a passphrase-derived Argon2id key. A disk grep of `onionwire.db` must not yield the message log.
|
Chat bodies in sqlite are ChaCha20-Poly1305 (`nonce || ciphertext` in `messages.plaintext`) with AAD bound to `friend_id`, `dir`, and row id, wrapped by a passphrase-derived Argon2id key. A disk grep of `onionwire.db` must not yield the message log. Empty-AAD v0.2 blobs are rewrapped once on unlock.
|
||||||
|
|
||||||
Still plaintext on disk (unless you add OS/FDE):
|
Still plaintext on disk (unless you add OS/FDE):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ A signed `loc` frame (`onion`, `ts`, `sig`) rewrites a friend’s locator **only
|
||||||
|
|
||||||
## Message bodies are encrypted at rest; keys are not
|
## Message bodies are encrypted at rest; keys are not
|
||||||
|
|
||||||
Chat bodies in sqlite are ChaCha20-Poly1305 (`nonce || ciphertext` in the `messages.plaintext` column). A random 32-byte data key is wrapped with Argon2id (same params as identity backup) from a non-empty passphrase. Salt + wrapped key live in `store_meta`. Unlock is fail-closed: wrong or empty passphrase does not open chat.
|
Chat bodies in sqlite are ChaCha20-Poly1305 (`nonce || ciphertext` in the `messages.plaintext` column) with AAD `owmsg1 || friend_id_le64 || dir || 0x00 || row_id_le64`. Swapping ciphertext between rows fails closed. A random 32-byte data key is wrapped with Argon2id (same params as identity backup) from a non-empty passphrase. Salt + wrapped key live in `store_meta`. Unlock is fail-closed: wrong or empty passphrase does not open chat. Empty-AAD v0.2 blobs are rewrapped once on unlock; `list_messages` never falls back to empty AAD.
|
||||||
|
|
||||||
Identity secret key, friend public keys, and locators remain plaintext in the same db. The message key is not wrapped with `identity_sk` (that key is already on disk). A seized laptop still yields who you talk to and your identity unless you add OS/FDE. sqlcipher is out of v1. `/wipe` overwrites message bodies and vacuums; `/wipe-all` deletes the data dir.
|
Identity secret key, friend public keys, and locators remain plaintext in the same db. The message key is not wrapped with `identity_sk` (that key is already on disk). A seized laptop still yields who you talk to and your identity unless you add OS/FDE. sqlcipher is out of v1. `/wipe` overwrites message bodies and vacuums; `/wipe-all` deletes the data dir.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
//! Encrypted identity backup. Onion (locator) is not included.
|
//! Encrypted identity backup. Onion (locator) is not included.
|
||||||
|
|
||||||
use argon2::{Algorithm, Argon2, Params, Version};
|
use argon2::{Algorithm, Argon2, Params, Version};
|
||||||
use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng};
|
use chacha20poly1305::aead::{Aead, AeadCore, KeyInit, OsRng, Payload};
|
||||||
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce};
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
|
|
||||||
|
|
@ -95,11 +95,21 @@ pub fn open(passphrase: &str, blob: &[u8]) -> Result<BackupKeys, String> {
|
||||||
BackupKeys::from_bytes(&pt)
|
BackupKeys::from_bytes(&pt)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn aead_encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>, String> {
|
pub(crate) fn aead_encrypt(
|
||||||
|
key: &[u8; 32],
|
||||||
|
plaintext: &[u8],
|
||||||
|
aad: &[u8],
|
||||||
|
) -> Result<Vec<u8>, String> {
|
||||||
let cipher = ChaCha20Poly1305::new(Key::from_slice(key));
|
let cipher = ChaCha20Poly1305::new(Key::from_slice(key));
|
||||||
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
|
let nonce = ChaCha20Poly1305::generate_nonce(&mut OsRng);
|
||||||
let ct = cipher
|
let ct = cipher
|
||||||
.encrypt(&nonce, plaintext)
|
.encrypt(
|
||||||
|
&nonce,
|
||||||
|
Payload {
|
||||||
|
msg: plaintext,
|
||||||
|
aad,
|
||||||
|
},
|
||||||
|
)
|
||||||
.map_err(|_| "encrypt failed".to_string())?;
|
.map_err(|_| "encrypt failed".to_string())?;
|
||||||
let mut out = Vec::with_capacity(NONCE_LEN + ct.len());
|
let mut out = Vec::with_capacity(NONCE_LEN + ct.len());
|
||||||
out.extend_from_slice(&nonce);
|
out.extend_from_slice(&nonce);
|
||||||
|
|
@ -107,13 +117,19 @@ pub(crate) fn aead_encrypt(key: &[u8; 32], plaintext: &[u8]) -> Result<Vec<u8>,
|
||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn aead_decrypt(key: &[u8; 32], blob: &[u8]) -> Result<Vec<u8>, String> {
|
pub(crate) fn aead_decrypt(key: &[u8; 32], blob: &[u8], aad: &[u8]) -> Result<Vec<u8>, String> {
|
||||||
if blob.len() < NONCE_LEN + 16 {
|
if blob.len() < NONCE_LEN + 16 {
|
||||||
return Err("ciphertext length".into());
|
return Err("ciphertext length".into());
|
||||||
}
|
}
|
||||||
let nonce = Nonce::from_slice(&blob[..NONCE_LEN]);
|
let nonce = Nonce::from_slice(&blob[..NONCE_LEN]);
|
||||||
let cipher = ChaCha20Poly1305::new(Key::from_slice(key));
|
let cipher = ChaCha20Poly1305::new(Key::from_slice(key));
|
||||||
cipher
|
cipher
|
||||||
.decrypt(nonce, &blob[NONCE_LEN..])
|
.decrypt(
|
||||||
|
nonce,
|
||||||
|
Payload {
|
||||||
|
msg: &blob[NONCE_LEN..],
|
||||||
|
aad,
|
||||||
|
},
|
||||||
|
)
|
||||||
.map_err(|_| "wrong passphrase or corrupt".to_string())
|
.map_err(|_| "wrong passphrase or corrupt".to_string())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
240
src/store.rs
240
src/store.rs
|
|
@ -270,12 +270,13 @@ impl Store {
|
||||||
match row {
|
match row {
|
||||||
Some((salt, wrapped)) => {
|
Some((salt, wrapped)) => {
|
||||||
let wrap_key = crate::backup::kdf(passphrase, &salt)?;
|
let wrap_key = crate::backup::kdf(passphrase, &salt)?;
|
||||||
let raw = crate::backup::aead_decrypt(&wrap_key, &wrapped)
|
let raw = crate::backup::aead_decrypt(&wrap_key, &wrapped, b"")
|
||||||
.map_err(|_| Error("wrong passphrase".into()))?;
|
.map_err(|_| Error("wrong passphrase".into()))?;
|
||||||
if raw.len() != 32 {
|
if raw.len() != 32 {
|
||||||
return Err(Error("wrapped message key length".into()));
|
return Err(Error("wrapped message key length".into()));
|
||||||
}
|
}
|
||||||
self.msg_key.copy_from_slice(&raw);
|
self.msg_key.copy_from_slice(&raw);
|
||||||
|
self.rewrap_empty_aad_messages()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|
@ -284,7 +285,7 @@ impl Store {
|
||||||
let mut salt = [0u8; 16];
|
let mut salt = [0u8; 16];
|
||||||
OsRng.fill_bytes(&mut salt);
|
OsRng.fill_bytes(&mut salt);
|
||||||
let wrap_key = crate::backup::kdf(passphrase, &salt)?;
|
let wrap_key = crate::backup::kdf(passphrase, &salt)?;
|
||||||
let wrapped = crate::backup::aead_encrypt(&wrap_key, &data_key)?;
|
let wrapped = crate::backup::aead_encrypt(&wrap_key, &data_key, b"")?;
|
||||||
self.conn.execute(
|
self.conn.execute(
|
||||||
"INSERT INTO store_meta (id, kdf_salt, wrapped_key) VALUES (1, ?1, ?2)",
|
"INSERT INTO store_meta (id, kdf_salt, wrapped_key) VALUES (1, ?1, ?2)",
|
||||||
params![salt.as_slice(), wrapped],
|
params![salt.as_slice(), wrapped],
|
||||||
|
|
@ -297,17 +298,59 @@ impl Store {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reencrypt_legacy_messages(&self) -> Result<()> {
|
fn reencrypt_legacy_messages(&self) -> Result<()> {
|
||||||
let mut stmt = self.conn.prepare("SELECT id, plaintext FROM messages")?;
|
let mut stmt = self
|
||||||
let rows: Vec<(i64, Vec<u8>)> = stmt
|
.conn
|
||||||
.query_map([], |row| Ok((row.get(0)?, row.get(1)?)))?
|
.prepare("SELECT id, friend_id, dir, plaintext FROM messages")?;
|
||||||
|
let rows: Vec<(i64, i64, String, Vec<u8>)> = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
|
||||||
|
})?
|
||||||
.collect::<std::result::Result<_, _>>()?;
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
drop(stmt);
|
drop(stmt);
|
||||||
if rows.is_empty() {
|
if rows.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let tx = self.conn.unchecked_transaction()?;
|
let tx = self.conn.unchecked_transaction()?;
|
||||||
for (id, plain) in rows {
|
for (id, friend_id, dir, plain) in rows {
|
||||||
let blob = crate::backup::aead_encrypt(&self.msg_key, &plain)?;
|
let aad = message_aad(friend_id, &dir, id);
|
||||||
|
let blob = crate::backup::aead_encrypt(&self.msg_key, &plain, &aad)?;
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE messages SET plaintext = ?1 WHERE id = ?2",
|
||||||
|
params![blob, id],
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
tx.commit()?;
|
||||||
|
let _ = self.conn.execute_batch("PRAGMA wal_checkpoint(TRUNCATE)");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One-shot: empty-AAD v0.2 blobs → row-bound AAD. list_messages never falls back.
|
||||||
|
fn rewrap_empty_aad_messages(&self) -> Result<()> {
|
||||||
|
let mut stmt = self
|
||||||
|
.conn
|
||||||
|
.prepare("SELECT id, friend_id, dir, plaintext FROM messages")?;
|
||||||
|
let rows: Vec<(i64, i64, String, Vec<u8>)> = stmt
|
||||||
|
.query_map([], |row| {
|
||||||
|
Ok((row.get(0)?, row.get(1)?, row.get(2)?, row.get(3)?))
|
||||||
|
})?
|
||||||
|
.collect::<std::result::Result<_, _>>()?;
|
||||||
|
drop(stmt);
|
||||||
|
let mut updates = Vec::new();
|
||||||
|
for (id, friend_id, dir, blob) in rows {
|
||||||
|
let aad = message_aad(friend_id, &dir, id);
|
||||||
|
if crate::backup::aead_decrypt(&self.msg_key, &blob, &aad).is_ok() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if let Ok(pt) = crate::backup::aead_decrypt(&self.msg_key, &blob, b"") {
|
||||||
|
let new_blob = crate::backup::aead_encrypt(&self.msg_key, &pt, &aad)?;
|
||||||
|
updates.push((id, new_blob));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if updates.is_empty() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let tx = self.conn.unchecked_transaction()?;
|
||||||
|
for (id, blob) in updates {
|
||||||
tx.execute(
|
tx.execute(
|
||||||
"UPDATE messages SET plaintext = ?1 WHERE id = ?2",
|
"UPDATE messages SET plaintext = ?1 WHERE id = ?2",
|
||||||
params![blob, id],
|
params![blob, id],
|
||||||
|
|
@ -663,28 +706,42 @@ impl Store {
|
||||||
|row| row.get(0),
|
|row| row.get(0),
|
||||||
)
|
)
|
||||||
.map_err(|_| Error("friend not found".into()))?;
|
.map_err(|_| Error("friend not found".into()))?;
|
||||||
let blob = crate::backup::aead_encrypt(&self.msg_key, plaintext)?;
|
let tx = self.conn.unchecked_transaction()?;
|
||||||
self.conn.execute(
|
tx.execute(
|
||||||
"INSERT INTO messages (friend_id, dir, plaintext, created_at) VALUES (?1, ?2, ?3, ?4)",
|
"INSERT INTO messages (friend_id, dir, plaintext, created_at) VALUES (?1, ?2, x'', ?3)",
|
||||||
params![friend_id, dir, blob, unix_now()],
|
params![friend_id, dir, unix_now()],
|
||||||
)?;
|
)?;
|
||||||
|
let row_id = tx.last_insert_rowid();
|
||||||
|
let aad = message_aad(friend_id, dir, row_id);
|
||||||
|
let blob = crate::backup::aead_encrypt(&self.msg_key, plaintext, &aad)?;
|
||||||
|
tx.execute(
|
||||||
|
"UPDATE messages SET plaintext = ?1 WHERE id = ?2",
|
||||||
|
params![blob, row_id],
|
||||||
|
)?;
|
||||||
|
tx.commit()?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_messages(&self, friend_pk: &[u8]) -> Result<Vec<Message>> {
|
pub fn list_messages(&self, friend_pk: &[u8]) -> Result<Vec<Message>> {
|
||||||
let mut stmt = self.conn.prepare(
|
let mut stmt = self.conn.prepare(
|
||||||
"SELECT m.dir, m.plaintext FROM messages m
|
"SELECT m.id, m.friend_id, m.dir, m.plaintext FROM messages m
|
||||||
JOIN friends f ON f.id = m.friend_id
|
JOIN friends f ON f.id = m.friend_id
|
||||||
WHERE f.pubkey = ?1
|
WHERE f.pubkey = ?1
|
||||||
ORDER BY m.id",
|
ORDER BY m.id",
|
||||||
)?;
|
)?;
|
||||||
let rows = stmt.query_map(params![friend_pk], |row| {
|
let rows = stmt.query_map(params![friend_pk], |row| {
|
||||||
Ok((row.get::<_, String>(0)?, row.get::<_, Vec<u8>>(1)?))
|
Ok((
|
||||||
|
row.get::<_, i64>(0)?,
|
||||||
|
row.get::<_, i64>(1)?,
|
||||||
|
row.get::<_, String>(2)?,
|
||||||
|
row.get::<_, Vec<u8>>(3)?,
|
||||||
|
))
|
||||||
})?;
|
})?;
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
for row in rows {
|
for row in rows {
|
||||||
let (dir, blob) = row?;
|
let (id, friend_id, dir, blob) = row?;
|
||||||
let plaintext = crate::backup::aead_decrypt(&self.msg_key, &blob)
|
let aad = message_aad(friend_id, &dir, id);
|
||||||
|
let plaintext = crate::backup::aead_decrypt(&self.msg_key, &blob, &aad)
|
||||||
.map_err(|_| Error("message decrypt failed".into()))?;
|
.map_err(|_| Error("message decrypt failed".into()))?;
|
||||||
out.push(Message { dir, plaintext });
|
out.push(Message { dir, plaintext });
|
||||||
}
|
}
|
||||||
|
|
@ -775,6 +832,17 @@ fn mkdir_700(path: &Path) -> Result<()> {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// AAD: `owmsg1` || friend_id_le64 || dir || 0x00 || row_id_le64
|
||||||
|
fn message_aad(friend_id: i64, dir: &str, row_id: i64) -> Vec<u8> {
|
||||||
|
let mut aad = Vec::with_capacity(6 + 8 + dir.len() + 1 + 8);
|
||||||
|
aad.extend_from_slice(b"owmsg1");
|
||||||
|
aad.extend_from_slice(&friend_id.to_le_bytes());
|
||||||
|
aad.extend_from_slice(dir.as_bytes());
|
||||||
|
aad.push(0);
|
||||||
|
aad.extend_from_slice(&row_id.to_le_bytes());
|
||||||
|
aad
|
||||||
|
}
|
||||||
|
|
||||||
fn unix_now() -> i64 {
|
fn unix_now() -> i64 {
|
||||||
SystemTime::now()
|
SystemTime::now()
|
||||||
.duration_since(UNIX_EPOCH)
|
.duration_since(UNIX_EPOCH)
|
||||||
|
|
@ -790,6 +858,148 @@ fn fingerprint(pubkey: &[u8]) -> String {
|
||||||
mod at_rest {
|
mod at_rest {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn swapped_ciphertext_does_not_show_other_friends_body() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let store = Store::open_at_with_passphrase(dir.path(), "aad-pass").unwrap();
|
||||||
|
let alice = [1u8; 32];
|
||||||
|
let bob = [2u8; 32];
|
||||||
|
store.upsert_friend(&alice, "a.onion", None).unwrap();
|
||||||
|
store.upsert_friend(&bob, "b.onion", None).unwrap();
|
||||||
|
store
|
||||||
|
.append_message(&alice, "out", b"secret-for-alice")
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.append_message(&bob, "out", b"secret-for-bob")
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let blobs: Vec<(i64, Vec<u8>)> = {
|
||||||
|
let mut stmt = store
|
||||||
|
.conn
|
||||||
|
.prepare("SELECT id, plaintext FROM messages ORDER BY id")
|
||||||
|
.unwrap();
|
||||||
|
stmt.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))
|
||||||
|
.unwrap()
|
||||||
|
.collect::<rusqlite::Result<Vec<_>>>()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
assert_eq!(blobs.len(), 2);
|
||||||
|
store
|
||||||
|
.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE messages SET plaintext = ?1 WHERE id = ?2",
|
||||||
|
params![&blobs[1].1, blobs[0].0],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE messages SET plaintext = ?1 WHERE id = ?2",
|
||||||
|
params![&blobs[0].1, blobs[1].0],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
if let Ok(msgs) = store.list_messages(&alice) {
|
||||||
|
assert!(
|
||||||
|
msgs.iter()
|
||||||
|
.all(|m| m.plaintext.as_slice() != b"secret-for-bob"),
|
||||||
|
"alice saw bob's body after ciphertext swap"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if let Ok(msgs) = store.list_messages(&bob) {
|
||||||
|
assert!(
|
||||||
|
msgs.iter()
|
||||||
|
.all(|m| m.plaintext.as_slice() != b"secret-for-alice"),
|
||||||
|
"bob saw alice's body after ciphertext swap"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn empty_aad_rows_rewrap_on_unlock_then_swap_fails() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let store = Store::open_at_with_passphrase(dir.path(), "rewrap-pass").unwrap();
|
||||||
|
let alice = [1u8; 32];
|
||||||
|
let bob = [2u8; 32];
|
||||||
|
store.upsert_friend(&alice, "a.onion", None).unwrap();
|
||||||
|
store.upsert_friend(&bob, "b.onion", None).unwrap();
|
||||||
|
store
|
||||||
|
.append_message(&alice, "out", b"secret-for-alice")
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.append_message(&bob, "out", b"secret-for-bob")
|
||||||
|
.unwrap();
|
||||||
|
let ids: Vec<i64> = {
|
||||||
|
let mut stmt = store
|
||||||
|
.conn
|
||||||
|
.prepare("SELECT id FROM messages ORDER BY id")
|
||||||
|
.unwrap();
|
||||||
|
stmt.query_map([], |r| r.get(0))
|
||||||
|
.unwrap()
|
||||||
|
.collect::<rusqlite::Result<Vec<_>>>()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
let a_blob = crate::backup::aead_encrypt(&store.msg_key, b"secret-for-alice", b"").unwrap();
|
||||||
|
let b_blob = crate::backup::aead_encrypt(&store.msg_key, b"secret-for-bob", b"").unwrap();
|
||||||
|
store
|
||||||
|
.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE messages SET plaintext = ?1 WHERE id = ?2",
|
||||||
|
params![a_blob, ids[0]],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE messages SET plaintext = ?1 WHERE id = ?2",
|
||||||
|
params![b_blob, ids[1]],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
drop(store);
|
||||||
|
|
||||||
|
let store = Store::open_at_with_passphrase(dir.path(), "rewrap-pass").unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
store.list_messages(&alice).unwrap()[0].plaintext,
|
||||||
|
b"secret-for-alice"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
store.list_messages(&bob).unwrap()[0].plaintext,
|
||||||
|
b"secret-for-bob"
|
||||||
|
);
|
||||||
|
|
||||||
|
let blobs: Vec<(i64, Vec<u8>)> = {
|
||||||
|
let mut stmt = store
|
||||||
|
.conn
|
||||||
|
.prepare("SELECT id, plaintext FROM messages ORDER BY id")
|
||||||
|
.unwrap();
|
||||||
|
stmt.query_map([], |r| Ok((r.get(0)?, r.get(1)?)))
|
||||||
|
.unwrap()
|
||||||
|
.collect::<rusqlite::Result<Vec<_>>>()
|
||||||
|
.unwrap()
|
||||||
|
};
|
||||||
|
store
|
||||||
|
.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE messages SET plaintext = ?1 WHERE id = ?2",
|
||||||
|
params![&blobs[1].1, blobs[0].0],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
store
|
||||||
|
.conn
|
||||||
|
.execute(
|
||||||
|
"UPDATE messages SET plaintext = ?1 WHERE id = ?2",
|
||||||
|
params![&blobs[0].1, blobs[1].0],
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
if let Ok(msgs) = store.list_messages(&alice) {
|
||||||
|
assert!(
|
||||||
|
msgs.iter()
|
||||||
|
.all(|m| m.plaintext.as_slice() != b"secret-for-bob"),
|
||||||
|
"alice saw bob's body after post-rewrap swap"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn first_unlock_reencrypts_legacy_plaintext_rows() {
|
fn first_unlock_reencrypts_legacy_plaintext_rows() {
|
||||||
let dir = tempfile::tempdir().unwrap();
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue