tests.rs 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. //! `test(totp_rewrap_)` suite: DEK re-wrap onto the active TOTP key.
  2. use secrecy::SecretString;
  3. use sqlx::PgPool;
  4. use uuid::Uuid;
  5. use super::*;
  6. use crate::cli::test_support::TestError;
  7. use crate::crypto::totp as crypto_totp;
  8. use crate::keys::{KeyId, KeyRing};
  9. const OLD_ID: &str = "old";
  10. const NEW_ID: &str = "new";
  11. const OLD_MATERIAL: &str = "old-totp-material-0123456789abcdef";
  12. const NEW_MATERIAL: &str = "new-totp-material-0123456789abcdef";
  13. /// A parsed v3 body, borrowing the blob.
  14. struct Parsed<'a> {
  15. kid: &'a str,
  16. wrap_nonce: &'a [u8],
  17. wrapped_dek: &'a [u8],
  18. data_nonce: &'a [u8],
  19. data_ct: &'a [u8],
  20. }
  21. fn key(id: &str, material: &str) -> (KeyId, SecretString) {
  22. (KeyId::new(id), SecretString::from(material.to_owned()))
  23. }
  24. /// A ring whose TOTP chain is `totp` (newest first), with throwaway pepper/PII
  25. /// chains (untouched by re-wrap).
  26. fn ring_with_totp(totp: &[(&str, &str)]) -> Result<KeyRing, TestError> {
  27. let totp_keys = totp
  28. .iter()
  29. .map(|(id, material)| key(id, material))
  30. .collect();
  31. KeyRing::from_parts(
  32. vec![key("p1", "pepper-material-0123456789abcdef")],
  33. totp_keys,
  34. vec![key("k1", "pii-material-0123456789abcdef")],
  35. )
  36. .map_err(|error| TestError::Harness(error.to_string()))
  37. }
  38. async fn insert_user(pool: &PgPool, id: Uuid, blob: &[u8]) -> Result<(), TestError> {
  39. sqlx::query(
  40. "INSERT INTO users (id, email, password_hash, totp_secret_enc, totp_enabled, created_at, updated_at)
  41. VALUES ($1, NULL, 'x', $2, true, now(), now())",
  42. )
  43. .bind(id)
  44. .bind(blob)
  45. .execute(pool)
  46. .await?;
  47. Ok(())
  48. }
  49. async fn stored_blob(pool: &PgPool, id: Uuid) -> Result<Vec<u8>, TestError> {
  50. let blob: Option<Vec<u8>> = sqlx::query_scalar("SELECT totp_secret_enc FROM users WHERE id = $1")
  51. .bind(id)
  52. .fetch_one(pool)
  53. .await?;
  54. blob.ok_or(TestError::Missing("totp_secret_enc"))
  55. }
  56. fn parse(blob: &[u8]) -> Result<Parsed<'_>, TestError> {
  57. let kid_len = usize::from(*blob.get(1).ok_or(TestError::Missing("kid_len"))?);
  58. let header = 2 + kid_len;
  59. let kid = std::str::from_utf8(blob.get(2..header).ok_or(TestError::Missing("kid"))?)
  60. .map_err(|_| TestError::Missing("kid utf8"))?;
  61. let body = blob.get(header..).ok_or(TestError::Missing("body"))?;
  62. let wrap_nonce = body
  63. .get(..NONCE_LEN)
  64. .ok_or(TestError::Missing("wrap_nonce"))?;
  65. let wrapped_dek = body
  66. .get(NONCE_LEN..NONCE_LEN + WRAPPED_DEK_LEN)
  67. .ok_or(TestError::Missing("wrapped_dek"))?;
  68. let data_start = NONCE_LEN + WRAPPED_DEK_LEN;
  69. let data_nonce = body
  70. .get(data_start..data_start + NONCE_LEN)
  71. .ok_or(TestError::Missing("data_nonce"))?;
  72. let data_ct = body
  73. .get(data_start + NONCE_LEN..)
  74. .ok_or(TestError::Missing("data_ct"))?;
  75. Ok(Parsed {
  76. kid,
  77. wrap_nonce,
  78. wrapped_dek,
  79. data_nonce,
  80. data_ct,
  81. })
  82. }
  83. #[tokio::test]
  84. async fn totp_rewrap_moves_v3_rows_to_active_key() -> Result<(), TestError> {
  85. let (_db, pool) = crate::db::test_support::migrated_pool()
  86. .await
  87. .map_err(|error| TestError::Harness(error.to_string()))?;
  88. let old = ring_with_totp(&[(OLD_ID, OLD_MATERIAL)])?;
  89. let full = ring_with_totp(&[(NEW_ID, NEW_MATERIAL), (OLD_ID, OLD_MATERIAL)])?;
  90. let new_only = ring_with_totp(&[(NEW_ID, NEW_MATERIAL)])?;
  91. let plaintext = crate::cli::secret::generate_totp_secret()?;
  92. let v3 = crypto_totp::encrypt_secret(&old, &plaintext, true)?;
  93. let v2 = crypto_totp::encrypt_secret(&old, &plaintext, false)?;
  94. // A v3 header that names a key but carries no body: malformed, must be skipped.
  95. let malformed = vec![3u8, 3, b'o', b'l', b'd'];
  96. let v3_id = Uuid::new_v4();
  97. let v2_id = Uuid::new_v4();
  98. let bad_id = Uuid::new_v4();
  99. insert_user(&pool, v3_id, &v3).await?;
  100. insert_user(&pool, v2_id, &v2).await?;
  101. insert_user(&pool, bad_id, &malformed).await?;
  102. let report = rewrap_with(&pool, &full).await?;
  103. assert_eq!(report.scanned, 3);
  104. assert_eq!(report.rewrapped, 1);
  105. assert_eq!(report.legacy, 1);
  106. assert_eq!(report.malformed, 1);
  107. assert_eq!(report.already_active, 0);
  108. let after = stored_blob(&pool, v3_id).await?;
  109. let before = parse(&v3)?;
  110. let now = parse(&after)?;
  111. assert_eq!(now.kid, NEW_ID, "row must now name the active key");
  112. assert_eq!(
  113. now.data_nonce, before.data_nonce,
  114. "data nonce must be preserved byte-for-byte"
  115. );
  116. assert_eq!(
  117. now.data_ct, before.data_ct,
  118. "secret ciphertext+tag must be preserved byte-for-byte"
  119. );
  120. assert_ne!(
  121. now.wrap_nonce, before.wrap_nonce,
  122. "wrap nonce must be fresh"
  123. );
  124. assert_ne!(
  125. now.wrapped_dek, before.wrapped_dek,
  126. "wrapped DEK must be re-sealed under the active key"
  127. );
  128. assert_eq!(crypto_totp::decrypt_secret(&new_only, &after)?, plaintext);
  129. // v1/v2 and malformed blobs are skipped and never overwritten.
  130. assert_eq!(stored_blob(&pool, v2_id).await?, v2);
  131. assert_eq!(stored_blob(&pool, bad_id).await?, malformed);
  132. Ok(())
  133. }
  134. #[tokio::test]
  135. async fn totp_rewrap_is_idempotent() -> Result<(), TestError> {
  136. let (_db, pool) = crate::db::test_support::migrated_pool()
  137. .await
  138. .map_err(|error| TestError::Harness(error.to_string()))?;
  139. let old = ring_with_totp(&[(OLD_ID, OLD_MATERIAL)])?;
  140. let full = ring_with_totp(&[(NEW_ID, NEW_MATERIAL), (OLD_ID, OLD_MATERIAL)])?;
  141. let plaintext = crate::cli::secret::generate_totp_secret()?;
  142. let v3 = crypto_totp::encrypt_secret(&old, &plaintext, true)?;
  143. let id = Uuid::new_v4();
  144. insert_user(&pool, id, &v3).await?;
  145. let first = rewrap_with(&pool, &full).await?;
  146. assert_eq!(first.rewrapped, 1);
  147. let after_first = stored_blob(&pool, id).await?;
  148. let second = rewrap_with(&pool, &full).await?;
  149. assert_eq!(second.rewrapped, 0, "a re-run must rewrite nothing");
  150. assert_eq!(second.already_active, 1);
  151. assert_eq!(
  152. stored_blob(&pool, id).await?,
  153. after_first,
  154. "a re-run must not touch an already-active row"
  155. );
  156. Ok(())
  157. }