diff --git a/.sqlx/query-09b62085a558ad9db5035f1b06657c2ef4e11f81963bdf6c1fcec1442c765ea4.json b/.sqlx/query-09b62085a558ad9db5035f1b06657c2ef4e11f81963bdf6c1fcec1442c765ea4.json new file mode 100644 index 000000000..76c05c2d7 --- /dev/null +++ b/.sqlx/query-09b62085a558ad9db5035f1b06657c2ef4e11f81963bdf6c1fcec1442c765ea4.json @@ -0,0 +1,40 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n id,\n payload,\n certificates,\n signature\n FROM objects\n WHERE id = $1\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "payload", + "type_info": "Bytea" + }, + { + "ordinal": 2, + "name": "certificates", + "type_info": "Bytea" + }, + { + "ordinal": 3, + "name": "signature", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [ + "Uuid" + ] + }, + "nullable": [ + false, + false, + false, + false + ] + }, + "hash": "09b62085a558ad9db5035f1b06657c2ef4e11f81963bdf6c1fcec1442c765ea4" +} diff --git a/.sqlx/query-fe371a75d4ee9d884b1520869195267941e85d301dffd1434aa8a4d997df9845.json b/.sqlx/query-190a13ab2ceec0f44a0afcd997d0057777de6f3a81779f08c9daec9b69f07010.json similarity index 68% rename from .sqlx/query-fe371a75d4ee9d884b1520869195267941e85d301dffd1434aa8a4d997df9845.json rename to .sqlx/query-190a13ab2ceec0f44a0afcd997d0057777de6f3a81779f08c9daec9b69f07010.json index c74f31803..3c589b8f9 100644 --- a/.sqlx/query-fe371a75d4ee9d884b1520869195267941e85d301dffd1434aa8a4d997df9845.json +++ b/.sqlx/query-190a13ab2ceec0f44a0afcd997d0057777de6f3a81779f08c9daec9b69f07010.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n id,\n object_id,\n jurisdiction as \"jurisdiction_code: JurisdictionCode\",\n object_type,\n action,\n created_at\n FROM journal_entries\n ORDER BY created_at DESC\n LIMIT 1\n ", + "query": "\n SELECT\n id,\n object_id,\n jurisdiction as \"jurisdiction_code: cacvote::JurisdictionCode\",\n object_type,\n action,\n created_at\n FROM journal_entries\n ORDER BY created_at DESC\n LIMIT 1\n ", "describe": { "columns": [ { @@ -15,7 +15,7 @@ }, { "ordinal": 2, - "name": "jurisdiction_code: JurisdictionCode", + "name": "jurisdiction_code: cacvote::JurisdictionCode", "type_info": "Varchar" }, { @@ -46,5 +46,5 @@ false ] }, - "hash": "fe371a75d4ee9d884b1520869195267941e85d301dffd1434aa8a4d997df9845" + "hash": "190a13ab2ceec0f44a0afcd997d0057777de6f3a81779f08c9daec9b69f07010" } diff --git a/.sqlx/query-7b4d1012f0cd3b246ab242e4786aa86812cfdc6d80fc57e5fc5fe6938f90952e.json b/.sqlx/query-7b4d1012f0cd3b246ab242e4786aa86812cfdc6d80fc57e5fc5fe6938f90952e.json new file mode 100644 index 000000000..4e998b832 --- /dev/null +++ b/.sqlx/query-7b4d1012f0cd3b246ab242e4786aa86812cfdc6d80fc57e5fc5fe6938f90952e.json @@ -0,0 +1,98 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n cb.id AS cast_ballot_id,\n cb.payload AS cast_ballot_payload,\n cb.certificates AS cast_ballot_certificates,\n cb.signature AS cast_ballot_signature,\n rr.id AS registration_request_id,\n rr.payload AS registration_request_payload,\n rr.certificates AS registration_request_certificates,\n rr.signature AS registration_request_signature,\n r.id AS registration_id,\n r.payload AS registration_payload,\n r.certificates AS registration_certificates,\n r.signature AS registration_signature,\n cb.created_at AS created_at\n FROM objects AS cb\n -- join on registration request\n INNER JOIN objects AS rr\n ON (convert_from(cb.payload, 'UTF8')::jsonb ->> $1)::uuid = rr.id\n -- join on registration\n INNER JOIN objects AS r\n ON (convert_from(cb.payload, 'UTF8')::jsonb ->> $2)::uuid = r.id\n WHERE rr.object_type = $3\n AND cb.object_type = $4\n AND r.object_type = $5\n ORDER BY cb.created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "cast_ballot_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "cast_ballot_payload", + "type_info": "Bytea" + }, + { + "ordinal": 2, + "name": "cast_ballot_certificates", + "type_info": "Bytea" + }, + { + "ordinal": 3, + "name": "cast_ballot_signature", + "type_info": "Bytea" + }, + { + "ordinal": 4, + "name": "registration_request_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "registration_request_payload", + "type_info": "Bytea" + }, + { + "ordinal": 6, + "name": "registration_request_certificates", + "type_info": "Bytea" + }, + { + "ordinal": 7, + "name": "registration_request_signature", + "type_info": "Bytea" + }, + { + "ordinal": 8, + "name": "registration_id", + "type_info": "Uuid" + }, + { + "ordinal": 9, + "name": "registration_payload", + "type_info": "Bytea" + }, + { + "ordinal": 10, + "name": "registration_certificates", + "type_info": "Bytea" + }, + { + "ordinal": 11, + "name": "registration_signature", + "type_info": "Bytea" + }, + { + "ordinal": 12, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "7b4d1012f0cd3b246ab242e4786aa86812cfdc6d80fc57e5fc5fe6938f90952e" +} diff --git a/.sqlx/query-b0e10aa8d206ddd303159182c6bb5caf719855d2d4e9cb42ef3b023764a3a4b6.json b/.sqlx/query-b0e10aa8d206ddd303159182c6bb5caf719855d2d4e9cb42ef3b023764a3a4b6.json new file mode 100644 index 000000000..f8b280fec --- /dev/null +++ b/.sqlx/query-b0e10aa8d206ddd303159182c6bb5caf719855d2d4e9cb42ef3b023764a3a4b6.json @@ -0,0 +1,23 @@ +{ + "db_name": "PostgreSQL", + "query": "\n INSERT INTO eg_private_keys (election_object_id, private_key)\n VALUES ($1, $2)\n RETURNING id\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + } + ], + "parameters": { + "Left": [ + "Uuid", + "Bytea" + ] + }, + "nullable": [ + false + ] + }, + "hash": "b0e10aa8d206ddd303159182c6bb5caf719855d2d4e9cb42ef3b023764a3a4b6" +} diff --git a/.sqlx/query-be586027918f26b699a064fb904303a7c3354332703da33c7ba593ad74bf1693.json b/.sqlx/query-be586027918f26b699a064fb904303a7c3354332703da33c7ba593ad74bf1693.json new file mode 100644 index 000000000..3717aa3a5 --- /dev/null +++ b/.sqlx/query-be586027918f26b699a064fb904303a7c3354332703da33c7ba593ad74bf1693.json @@ -0,0 +1,48 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n rr.id,\n rr.payload,\n rr.certificates,\n rr.signature,\n rr.created_at\n FROM\n objects AS rr\n WHERE\n rr.object_type = $1\n AND\n NOT EXISTS (\n SELECT 1\n FROM objects AS r\n WHERE r.object_type = $2\n AND rr.id = (convert_from(r.payload, 'UTF8')::jsonb ->> $3)::uuid\n )\n ORDER BY rr.created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "payload", + "type_info": "Bytea" + }, + { + "ordinal": 2, + "name": "certificates", + "type_info": "Bytea" + }, + { + "ordinal": 3, + "name": "signature", + "type_info": "Bytea" + }, + { + "ordinal": 4, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false + ] + }, + "hash": "be586027918f26b699a064fb904303a7c3354332703da33c7ba593ad74bf1693" +} diff --git a/.sqlx/query-f3bea3cd380dffea913274cd686e6d1be327c232364856c4ce32296aec960db1.json b/.sqlx/query-ca0d436ef724ae3fd3c6adc19432ad3ea329c84e8dd8cb1fd519aad642e2e49d.json similarity index 60% rename from .sqlx/query-f3bea3cd380dffea913274cd686e6d1be327c232364856c4ce32296aec960db1.json rename to .sqlx/query-ca0d436ef724ae3fd3c6adc19432ad3ea329c84e8dd8cb1fd519aad642e2e49d.json index 946cfc313..58b6cb09b 100644 --- a/.sqlx/query-f3bea3cd380dffea913274cd686e6d1be327c232364856c4ce32296aec960db1.json +++ b/.sqlx/query-ca0d436ef724ae3fd3c6adc19432ad3ea329c84e8dd8cb1fd519aad642e2e49d.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n id,\n object_id,\n jurisdiction as \"jurisdiction_code: JurisdictionCode\",\n object_type,\n action,\n created_at\n FROM journal_entries\n WHERE object_id IS NOT NULL\n AND object_type IN ('RegistrationRequest')\n AND object_id NOT IN (SELECT id FROM objects)\n ", + "query": "\n SELECT\n id,\n object_id,\n jurisdiction as \"jurisdiction_code: cacvote::JurisdictionCode\",\n object_type,\n action,\n created_at\n FROM journal_entries\n WHERE object_id IS NOT NULL\n AND object_type IN ($1, $2)\n AND object_id NOT IN (SELECT id FROM objects)\n ", "describe": { "columns": [ { @@ -15,7 +15,7 @@ }, { "ordinal": 2, - "name": "jurisdiction_code: JurisdictionCode", + "name": "jurisdiction_code: cacvote::JurisdictionCode", "type_info": "Varchar" }, { @@ -35,7 +35,10 @@ } ], "parameters": { - "Left": [] + "Left": [ + "Varchar", + "Varchar" + ] }, "nullable": [ false, @@ -46,5 +49,5 @@ false ] }, - "hash": "f3bea3cd380dffea913274cd686e6d1be327c232364856c4ce32296aec960db1" + "hash": "ca0d436ef724ae3fd3c6adc19432ad3ea329c84e8dd8cb1fd519aad642e2e49d" } diff --git a/.sqlx/query-f6623ae184e6512b5827d4d42683005e9493b04d4cef80adc8abe29be833cc85.json b/.sqlx/query-f6623ae184e6512b5827d4d42683005e9493b04d4cef80adc8abe29be833cc85.json new file mode 100644 index 000000000..f588d7672 --- /dev/null +++ b/.sqlx/query-f6623ae184e6512b5827d4d42683005e9493b04d4cef80adc8abe29be833cc85.json @@ -0,0 +1,103 @@ +{ + "db_name": "PostgreSQL", + "query": "\n SELECT\n r.id AS registration_id,\n r.payload AS registration_payload,\n r.certificates AS registration_certificates,\n r.signature AS registration_signature,\n e.id AS election_id,\n e.payload AS election_payload,\n e.certificates AS election_certificates,\n e.signature AS election_signature,\n rr.id AS registration_request_id,\n rr.payload AS registration_request_payload,\n rr.certificates AS registration_request_certificates,\n rr.signature AS registration_request_signature,\n r.created_at AS created_at,\n r.server_synced_at IS NOT NULL AS \"is_synced!: bool\"\n FROM objects AS r\n INNER JOIN objects AS e\n ON (convert_from(r.payload, 'UTF8')::jsonb ->> $1)::uuid = e.id\n INNER JOIN objects AS rr\n ON (convert_from(r.payload, 'UTF8')::jsonb ->> $2)::uuid = rr.id\n WHERE e.object_type = $3\n AND r.object_type = $4\n ORDER BY r.created_at DESC\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "registration_id", + "type_info": "Uuid" + }, + { + "ordinal": 1, + "name": "registration_payload", + "type_info": "Bytea" + }, + { + "ordinal": 2, + "name": "registration_certificates", + "type_info": "Bytea" + }, + { + "ordinal": 3, + "name": "registration_signature", + "type_info": "Bytea" + }, + { + "ordinal": 4, + "name": "election_id", + "type_info": "Uuid" + }, + { + "ordinal": 5, + "name": "election_payload", + "type_info": "Bytea" + }, + { + "ordinal": 6, + "name": "election_certificates", + "type_info": "Bytea" + }, + { + "ordinal": 7, + "name": "election_signature", + "type_info": "Bytea" + }, + { + "ordinal": 8, + "name": "registration_request_id", + "type_info": "Uuid" + }, + { + "ordinal": 9, + "name": "registration_request_payload", + "type_info": "Bytea" + }, + { + "ordinal": 10, + "name": "registration_request_certificates", + "type_info": "Bytea" + }, + { + "ordinal": 11, + "name": "registration_request_signature", + "type_info": "Bytea" + }, + { + "ordinal": 12, + "name": "created_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 13, + "name": "is_synced!: bool", + "type_info": "Bool" + } + ], + "parameters": { + "Left": [ + "Text", + "Text", + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + false, + null + ] + }, + "hash": "f6623ae184e6512b5827d4d42683005e9493b04d4cef80adc8abe29be833cc85" +} diff --git a/Cargo.lock b/Cargo.lock index 04b4282d3..a5be26182 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + [[package]] name = "ahash" version = "0.7.7" @@ -420,6 +431,27 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cacvote-jx-terminal-backend" version = "0.1.0" @@ -445,6 +477,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "tempfile", "thiserror", "time", "tokio", @@ -454,6 +487,7 @@ dependencies = [ "tracing-subscriber", "types-rs", "uuid", + "zip", ] [[package]] @@ -517,6 +551,7 @@ version = "1.0.83" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f1174fb0b6ec23863f8b971027804a42614e347eafb0a95bf0b12cdae21fc4d0" dependencies = [ + "jobserver", "libc", ] @@ -550,6 +585,16 @@ dependencies = [ "chrono", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "clap" version = "4.4.12" @@ -648,6 +693,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "constcat" version = "0.3.1" @@ -694,6 +745,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc32fast" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" +dependencies = [ + "cfg-if", +] + [[package]] name = "crossbeam-queue" version = "0.3.10" @@ -1126,6 +1186,16 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6" +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + [[package]] name = "flume" version = "0.11.0" @@ -1711,6 +1781,15 @@ dependencies = [ "hashbrown 0.14.3", ] +[[package]] +name = "inout" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" +dependencies = [ + "generic-array", +] + [[package]] name = "interprocess-docfix" version = "1.2.2" @@ -1758,6 +1837,15 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +[[package]] +name = "jobserver" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "685a7d121ee3f65ae4fddd72b25a04bb36b6af81bc0828f7d5434c0fe60fa3a2" +dependencies = [ + "libc", +] + [[package]] name = "js-sys" version = "0.3.66" @@ -2172,12 +2260,35 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + [[package]] name = "pcsc" version = "2.8.2" @@ -3973,3 +4084,52 @@ name = "zeroize" version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + +[[package]] +name = "zip" +version = "0.6.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" +dependencies = [ + "aes", + "byteorder", + "bzip2", + "constant_time_eq", + "crc32fast", + "crossbeam-utils", + "flate2", + "hmac", + "pbkdf2", + "sha1", + "time", + "zstd", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.10+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/Cargo.toml b/Cargo.toml index 440ddeba9..f792447ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -74,6 +74,7 @@ ui-rs = { path = "libs/ui-rs" } uinput = "0.1.3" url = "2.5.0" uuid = { version = "1.4.0", features = ["serde", "v4", "js"] } +zip = "0.6.6" [workspace.dependencies.sqlx] version = "0.7.1" diff --git a/apps/cacvote-jx-terminal/backend/Cargo.toml b/apps/cacvote-jx-terminal/backend/Cargo.toml index 0f63b423a..bfbcf63a5 100644 --- a/apps/cacvote-jx-terminal/backend/Cargo.toml +++ b/apps/cacvote-jx-terminal/backend/Cargo.toml @@ -24,6 +24,7 @@ reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sqlx = { workspace = true } +tempfile = { workspace = true } thiserror = { workspace = true } time = { workspace = true } tokio = { workspace = true, features = ["rt-multi-thread"] } @@ -33,6 +34,7 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true, features = ["env-filter"] } types-rs = { workspace = true, features = ["backend"] } uuid = { workspace = true } +zip = { workspace = true } [dev-dependencies] mockall = { workspace = true } diff --git a/apps/cacvote-jx-terminal/backend/db/migrations/20240418215655_add_eg_private_keys.sql b/apps/cacvote-jx-terminal/backend/db/migrations/20240418215655_add_eg_private_keys.sql new file mode 100644 index 000000000..b62b89105 --- /dev/null +++ b/apps/cacvote-jx-terminal/backend/db/migrations/20240418215655_add_eg_private_keys.sql @@ -0,0 +1,8 @@ +CREATE TABLE eg_private_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + election_object_id UUID NOT NULL, + private_key BYTEA NOT NULL, + created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, + + FOREIGN KEY (election_object_id) REFERENCES objects(id) +); diff --git a/apps/cacvote-jx-terminal/backend/src/app.rs b/apps/cacvote-jx-terminal/backend/src/app.rs index e7b009711..9dc45075b 100644 --- a/apps/cacvote-jx-terminal/backend/src/app.rs +++ b/apps/cacvote-jx-terminal/backend/src/app.rs @@ -22,16 +22,18 @@ use tower_http::services::{ServeDir, ServeFile}; use tower_http::trace::TraceLayer; use tracing::Level; use types_rs::cacvote::{ - CreateRegistrationData, Election, Payload, Registration, SessionData, SignedObject, + CreateElectionRequest, CreateRegistrationRequest, Election, Payload, Registration, SessionData, + SignedObject, }; use uuid::Uuid; use crate::config::{Config, MAX_REQUEST_SIZE}; -use crate::{db, smartcard}; +use crate::{db, electionguard, smartcard}; use tokio::sync::broadcast; #[derive(Clone)] struct AppState { + config: Config, pool: PgPool, smartcard: smartcard::DynSmartcard, broadcast_tx: broadcast::Sender, @@ -107,6 +109,7 @@ pub(crate) fn setup(pool: PgPool, config: Config, smartcard: smartcard::DynSmart .layer(DefaultBodyLimit::max(MAX_REQUEST_SIZE)) .layer(TraceLayer::new_for_http()) .with_state(AppState { + config, pool, smartcard, broadcast_tx, @@ -181,9 +184,12 @@ async fn get_elections(State(AppState { pool, .. }): State) -> impl In async fn create_election( State(AppState { - pool, smartcard, .. + config, + pool, + smartcard, + .. }): State, - Json(election): Json, + Json(election): Json, ) -> impl IntoResponse { let jurisdiction_code = match smartcard.get_card_details() { Some(card_details) => card_details.card_details.jurisdiction_code(), @@ -203,7 +209,7 @@ async fn create_election( ); } - let mut connection = match pool.acquire().await { + let mut transaction = match pool.begin().await { Ok(connection) => connection, Err(e) => { tracing::error!("error getting database connection: {e}"); @@ -214,7 +220,27 @@ async fn create_election( } }; - let payload = Payload::Election(election); + let election_config = match electionguard::generate_election_config( + &config.eg_classpath, + election.election_definition.election.clone(), + ) { + Ok(election_config) => election_config, + Err(e) => { + tracing::error!("error generating election config: {e}"); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ "error": "error generating election config" })), + ); + } + }; + + let payload = Payload::Election(Election { + jurisdiction_code: election.jurisdiction_code, + mailing_address: election.mailing_address, + election_definition: election.election_definition, + electionguard_election_metadata_blob: election_config.public_metadata_blob, + }); + let serialized_payload = match serde_json::to_vec(&payload) { Ok(serialized_payload) => serialized_payload, Err(e) => { @@ -258,7 +284,7 @@ async fn create_election( signature: signed.data, }; - if let Err(e) = db::add_object(&mut connection, &signed_object).await { + if let Err(e) = db::add_object(&mut transaction, &signed_object).await { tracing::error!("error adding object to database: {e}"); return ( StatusCode::INTERNAL_SERVER_ERROR, @@ -266,6 +292,28 @@ async fn create_election( ); } + if let Err(e) = db::add_eg_private_key( + &mut transaction, + &signed_object.id, + &election_config.private_metadata_blob, + ) + .await + { + tracing::error!("error adding EG private key to database: {e}"); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ "error": "error adding EG private key to database" })), + ); + } + + if let Err(e) = transaction.commit().await { + tracing::error!("error committing transaction: {e}"); + return ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ "error": "error committing transaction" })), + ); + } + (StatusCode::CREATED, Json(json!({ "id": signed_object.id }))) } @@ -273,12 +321,12 @@ async fn create_registration( State(AppState { pool, smartcard, .. }): State, - Json(CreateRegistrationData { + Json(CreateRegistrationRequest { registration_request_id, election_id, ballot_style_id, precinct_id, - }): Json, + }): Json, ) -> impl IntoResponse { let jurisdiction_code = match smartcard.get_card_details() { Some(card_details) => card_details.card_details.jurisdiction_code(), diff --git a/apps/cacvote-jx-terminal/backend/src/config.rs b/apps/cacvote-jx-terminal/backend/src/config.rs index 4003ef72d..b0aab4277 100644 --- a/apps/cacvote-jx-terminal/backend/src/config.rs +++ b/apps/cacvote-jx-terminal/backend/src/config.rs @@ -1,6 +1,6 @@ //! Application configuration. -use std::time::Duration; +use std::{path::PathBuf, time::Duration}; use clap::Parser; use types_rs::cacvote::JurisdictionCode; @@ -35,9 +35,13 @@ pub(crate) struct Config { /// Directory to serve static files from. #[arg(long, env = "PUBLIC_DIR")] - pub(crate) public_dir: Option, + pub(crate) public_dir: Option, /// Log level. #[arg(long, env = "LOG_LEVEL", default_value = "info")] pub(crate) log_level: tracing::Level, + + /// ElectionGuard Java CLI CLASSPATH. + #[arg(long, env = "EG_CLASSPATH")] + pub(crate) eg_classpath: PathBuf, } diff --git a/apps/cacvote-jx-terminal/backend/src/db.rs b/apps/cacvote-jx-terminal/backend/src/db.rs index 08ed036e6..a580603fb 100644 --- a/apps/cacvote-jx-terminal/backend/src/db.rs +++ b/apps/cacvote-jx-terminal/backend/src/db.rs @@ -528,6 +528,26 @@ pub(crate) async fn get_cast_ballots( Ok(cast_ballots) } +pub(crate) async fn add_eg_private_key( + executor: &mut sqlx::PgConnection, + election_object_id: &Uuid, + private_metadata_blob: &[u8], +) -> color_eyre::Result { + let record = sqlx::query!( + r#" + INSERT INTO eg_private_keys (election_object_id, private_key) + VALUES ($1, $2) + RETURNING id + "#, + election_object_id, + private_metadata_blob + ) + .fetch_one(executor) + .await?; + + Ok(record.id) +} + #[cfg(test)] mod tests { use openssl::{ @@ -561,13 +581,14 @@ mod tests { async fn test_pending_registration_requests(pool: sqlx::PgPool) -> color_eyre::Result<()> { let (certificates, _, private_key) = load_keypair()?; let election_definition = load_election_definition()?; - let mut connection = &mut pool.acquire().await?; + let connection = &mut pool.acquire().await?; let jurisdiction_code = JurisdictionCode::try_from("st.test-jurisdiction").unwrap(); let election_payload = cacvote::Payload::Election(cacvote::Election { jurisdiction_code: jurisdiction_code.clone(), election_definition: election_definition.clone(), mailing_address: "123 Main St".to_owned(), + electionguard_election_metadata_blob: vec![], }); let election_object = cacvote::SignedObject::from_payload( &election_payload, @@ -575,10 +596,9 @@ mod tests { &private_key, )?; - add_object_from_server(&mut connection, &election_object).await?; + add_object_from_server(connection, &election_object).await?; - let pending_registration_requests = - get_pending_registration_requests(&mut connection).await?; + let pending_registration_requests = get_pending_registration_requests(connection).await?; assert!( pending_registration_requests.is_empty(), @@ -598,10 +618,9 @@ mod tests { &private_key, )?; - add_object_from_server(&mut connection, ®istration_request_object).await?; + add_object_from_server(connection, ®istration_request_object).await?; - let pending_registration_requests = - get_pending_registration_requests(&mut connection).await?; + let pending_registration_requests = get_pending_registration_requests(connection).await?; match pending_registration_requests.as_slice() { [registration_request] => { @@ -630,10 +649,9 @@ mod tests { &private_key, )?; - add_object_from_server(&mut connection, ®istration_object).await?; + add_object_from_server(connection, ®istration_object).await?; - let pending_registration_requests = - get_pending_registration_requests(&mut connection).await?; + let pending_registration_requests = get_pending_registration_requests(connection).await?; assert!( pending_registration_requests.is_empty(), diff --git a/apps/cacvote-jx-terminal/backend/src/electionguard.rs b/apps/cacvote-jx-terminal/backend/src/electionguard.rs new file mode 100644 index 000000000..9b727e4e0 --- /dev/null +++ b/apps/cacvote-jx-terminal/backend/src/electionguard.rs @@ -0,0 +1,502 @@ +use std::{ + fs::{read_dir, DirBuilder, File}, + io, + path::PathBuf, +}; + +use serde::Serialize; +use types_rs::election as vx_election; + +#[derive(Debug, Serialize)] +pub(crate) struct Manifest { + pub(crate) election_scope_id: String, + pub(crate) spec_version: String, + pub(crate) r#type: ElectionType, + pub(crate) start_date: String, + pub(crate) end_date: String, + pub(crate) geopolitical_units: Vec, + pub(crate) parties: Vec, + pub(crate) candidates: Vec, + pub(crate) contests: Vec, + pub(crate) ballot_styles: Vec, + pub(crate) name: Vec, + pub(crate) contact_information: ContactInformation, +} + +impl From for Manifest { + fn from(value: vx_election::Election) -> Self { + Self { + election_scope_id: "TestManifest".to_owned(), + spec_version: "v2.0.0".to_owned(), + r#type: if value.title.contains("Primary") { + ElectionType::Primary + } else { + ElectionType::General + }, + start_date: "start".to_owned(), + end_date: "end".to_owned(), + geopolitical_units: value.districts.into_iter().map(Into::into).collect(), + parties: value.parties.into_iter().map(Into::into).collect(), + candidates: value + .contests + .clone() + .into_iter() + .filter_map(|contest| match contest { + vx_election::Contest::Candidate(contest) => Some(contest.candidates), + vx_election::Contest::YesNo(_) => None, + }) + .flat_map(|candidates| candidates.into_iter().map(Into::into)) + .collect(), + contests: value + .contests + .into_iter() + .enumerate() + .map(|(i, contest)| Contest { + sequence_order: i as u32 + 1, + ..contest.into() + }) + .collect(), + ballot_styles: value.ballot_styles.into_iter().map(Into::into).collect(), + name: Vec::new(), + contact_information: ContactInformation::default(), + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) enum ElectionType { + #[serde(rename = "primary")] + Primary, + + #[serde(rename = "general")] + General, +} + +#[derive(Debug, Clone, Serialize)] +pub(crate) struct ObjectId(String); + +impl From for ObjectId { + fn from(district_id: vx_election::DistrictId) -> Self { + Self(format!("district-{district_id}")) + } +} + +impl From for ObjectId { + fn from(contest_id: vx_election::ContestId) -> Self { + Self(format!("contest-{contest_id}")) + } +} + +impl From for ObjectId { + fn from(candidate_id: vx_election::CandidateId) -> Self { + Self(format!("cand-{candidate_id}")) + } +} + +impl From for ObjectId { + fn from(party_id: vx_election::PartyId) -> Self { + Self(format!("party-{party_id}")) + } +} + +impl From for ObjectId { + fn from(ballot_style_id: vx_election::BallotStyleId) -> Self { + Self(format!("ballot-style-{ballot_style_id}")) + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct GeopoliticalUnit { + pub(crate) object_id: ObjectId, + pub(crate) name: String, + pub(crate) r#type: GeopoliticalUnitType, + pub(crate) contact_information: Option>, +} + +impl From for GeopoliticalUnit { + fn from(district: vx_election::District) -> Self { + GeopoliticalUnit { + object_id: district.id.into(), + name: district.name, + r#type: GeopoliticalUnitType::District, + contact_information: None, + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) enum GeopoliticalUnitType { + #[serde(rename = "district")] + District, +} + +#[derive(Debug, Serialize)] +pub(crate) struct Party { + pub(crate) object_id: ObjectId, + pub(crate) name: String, + pub(crate) abbreviation: String, + pub(crate) color: Option, + pub(crate) logo_uri: Option, +} + +impl From for Party { + fn from(party: types_rs::election::Party) -> Self { + Party { + object_id: party.id.into(), + name: party.name, + abbreviation: party.abbrev, + color: None, + logo_uri: None, + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct Candidate { + pub(crate) object_id: ObjectId, + pub(crate) name: String, + pub(crate) party_id: Option, + pub(crate) image_url: Option, + pub(crate) is_write_in: bool, +} + +impl From for Candidate { + fn from(candidate: vx_election::Candidate) -> Self { + Candidate { + object_id: candidate.id.into(), + name: candidate.name, + party_id: candidate + .party_ids + .and_then(|party_ids| party_ids.first().cloned()) + .map(Into::into), + image_url: None, + is_write_in: false, + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct Contest { + pub(crate) object_id: ObjectId, + pub(crate) sequence_order: u32, + pub(crate) electoral_district_id: ObjectId, + pub(crate) vote_variation: VoteVariation, + pub(crate) number_elected: u32, + pub(crate) votes_allowed: u32, + pub(crate) name: String, + pub(crate) ballot_selections: Vec, + pub(crate) ballot_title: Option, + pub(crate) ballot_subtitle: Option, +} + +impl From for Contest { + fn from(contest: vx_election::Contest) -> Self { + match contest { + vx_election::Contest::Candidate(contest) => Contest { + object_id: contest.id.clone().into(), + sequence_order: 0, + electoral_district_id: contest.district_id.into(), + vote_variation: if contest.seats == 1 { + VoteVariation::OneOfM + } else { + VoteVariation::NofM + }, + votes_allowed: contest.seats, + number_elected: 1, + name: ObjectId::from(contest.id).0, + ballot_selections: contest + .candidates + .into_iter() + .enumerate() + .map(|(i, candidate)| BallotSelection { + sequence_order: i as u32 + 1, + ..candidate.into() + }) + .collect(), + ballot_title: Some(contest.title), + ballot_subtitle: None, + }, + vx_election::Contest::YesNo(_) => unimplemented!(), + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct BallotSelection { + pub(crate) object_id: ObjectId, + pub(crate) sequence_order: u32, + pub(crate) candidate_id: ObjectId, +} + +impl From for BallotSelection { + fn from(value: vx_election::Candidate) -> Self { + let candidate_id: ObjectId = value.id.into(); + BallotSelection { + object_id: candidate_id.clone(), + sequence_order: 0, + candidate_id, + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) enum VoteVariation { + #[serde(rename = "one_of_m")] + OneOfM, + + #[serde(rename = "n_of_m")] + NofM, +} + +#[derive(Debug, Serialize)] +pub(crate) struct BallotStyle { + pub(crate) object_id: ObjectId, + pub(crate) geopolitical_unit_ids: Vec, + pub(crate) party_ids: Vec, + pub(crate) image_uri: Option, +} + +impl From for BallotStyle { + fn from(ballot_style: vx_election::BallotStyle) -> Self { + BallotStyle { + object_id: ballot_style.id.into(), + geopolitical_unit_ids: ballot_style.districts.into_iter().map(Into::into).collect(), + party_ids: ballot_style + .party_id + .map(|party_id| vec![party_id.into()]) + .unwrap_or_default(), + image_uri: None, + } + } +} + +#[derive(Debug, Serialize)] +pub(crate) struct ContactInformation { + pub(crate) name: String, + pub(crate) address_line: Vec, + pub(crate) email: Option, + pub(crate) phone: Option, +} + +impl Default for ContactInformation { + fn default() -> Self { + Self { + name: "contact".to_owned(), + address_line: Vec::new(), + email: None, + phone: None, + } + } +} + +pub(crate) struct ElectionConfig { + pub(crate) public_metadata_blob: Vec, + pub(crate) private_metadata_blob: Vec, +} + +/// Generate ElectionGuard metadata for an election. +pub(crate) fn generate_election_config( + classpath: &PathBuf, + election: impl Into, +) -> io::Result { + let manifest: Manifest = election.into(); + + // create a temporary working directory securely + let temp_dir = tempfile::tempdir()?; + let temp_dir_path = temp_dir.path(); + + // write the manifest to a file + let manifest_path = temp_dir_path.join("manifest.json"); + let manifest_file = File::create(&manifest_path)?; + serde_json::to_writer(manifest_file, &manifest)?; + + // create a temporary output directory securely + let output_directory = temp_dir_path.join("output"); + DirBuilder::new().create(&output_directory)?; + + // run the Java ElectionGuard CLI to create the election configuration + let trustees_directory = output_directory.join("trustees"); + run_create_election_config(classpath, &manifest_path, &output_directory)?; + run_trusted_key_ceremony( + classpath, + &output_directory, + &trustees_directory, + &output_directory, + )?; + + // at this point, the output directory should contain the public election + // configuration & key and the private keys in the `trustees` directory + let public_metadata_blob = zip_files_in_directory_to_buffer(&output_directory)?; + let private_metadata_blob = zip_files_in_directory_to_buffer(&trustees_directory)?; + + Ok(ElectionConfig { + public_metadata_blob, + private_metadata_blob, + }) +} + +/// Zip all files directly within a directory into a zip archive. This function +/// does not recursively zip files in subdirectories, and it does not include +/// the directory itself in the archive. +fn zip_files_in_directory_to_buffer(directory: &PathBuf) -> io::Result> { + let mut zip_buffer = Vec::new(); + let writer = std::io::Cursor::new(&mut zip_buffer); + let mut zip = zip::ZipWriter::new(writer); + + zip_files_in_directory(&mut zip, directory)?; + + zip.finish()?; + + // `zip` is holding on to `writer` which is holding on to `zip_buffer`, + // so we need to drop `zip` to release the borrow on `zip_buffer`. + drop(zip); + + Ok(zip_buffer) +} + +/// Zip all files directly within a directory into a zip archive. This function +/// does not recursively zip files in subdirectories, and it does not include +/// the directory itself in the archive. +fn zip_files_in_directory(zip: &mut zip::ZipWriter, directory: &PathBuf) -> io::Result<()> +where + W: io::Write + io::Seek, +{ + for entry in read_dir(directory)? { + let entry = entry?; + let path = entry.path(); + let name = path + .strip_prefix(&directory) + .expect("entry must be in output directory"); + + if path.is_file() { + zip.start_file( + name.to_str().expect("entry must have valid UTF-8 name"), + Default::default(), + )?; + let mut file = File::open(&path)?; + std::io::copy(&mut file, zip)?; + } + } + + Ok(()) +} + +/// Run the Java ElectionGuard CLI to create an election configuration. Expects +/// to read and write files because the Java ElectionGuard implementation +/// expects to work with files. +pub(crate) fn run_create_election_config( + classpath: &PathBuf, + manifest_path: &PathBuf, + output_directory: &PathBuf, +) -> io::Result<()> { + std::process::Command::new("java") + .arg("-classpath") + .arg(classpath) + .arg("electionguard.cli.RunCreateElectionConfig") + .arg("-manifest") + .arg(manifest_path) + .arg("-nguardians") + .arg("1") + .arg("-quorum") + .arg("1") + .arg("-out") + .arg(output_directory) + .arg("--baux0") + .arg("device42") + .output() + .map(|output| { + if let Ok(stdout) = std::str::from_utf8(&output.stdout) { + if !stdout.is_empty() { + tracing::debug!("electionguard.cli.RunCreateElectionConfig stdout: {stdout}"); + } + } + + if let Ok(stderr) = std::str::from_utf8(&output.stderr) { + if !stderr.is_empty() { + tracing::debug!("electionguard.cli.RunCreateElectionConfig stderr: {stderr}"); + } + } + }) +} + +/// Run the Java ElectionGuard CLI to create a trustee (private) election key. +/// Expects to read and write files because the Java ElectionGuard +/// implementation expects to work with files. +pub(crate) fn run_trusted_key_ceremony( + classpath: &PathBuf, + input_directory: &PathBuf, + trustees_directory: &PathBuf, + output_directory: &PathBuf, +) -> io::Result<()> { + std::process::Command::new("java") + .arg("-classpath") + .arg(classpath) + .arg("electionguard.cli.RunTrustedKeyCeremony") + .arg("-in") + .arg(input_directory) + .arg("-trustees") + .arg(trustees_directory) + .arg("-out") + .arg(output_directory) + .output() + .map(|output| { + if let Ok(stdout) = std::str::from_utf8(&output.stdout) { + if !stdout.is_empty() { + tracing::debug!("electionguard.cli.RunTrustedKeyCeremony stdout: {stdout}"); + } + } + + if let Ok(stderr) = std::str::from_utf8(&output.stderr) { + if !stderr.is_empty() { + tracing::debug!("electionguard.cli.RunTrustedKeyCeremony stderr: {stderr}"); + } + } + }) +} + +#[cfg(test)] +mod tests { + use types_rs::election::ElectionDefinition; + + use super::*; + + fn load_election_definition() -> color_eyre::Result { + Ok(ElectionDefinition::try_from( + &include_bytes!("../tests/fixtures/electionFamousNames2021.json")[..], + )?) + } + + #[test] + fn test_generate_election_config() { + if let Ok(classpath) = std::env::var("EG_CLASSPATH") { + let election_definition = load_election_definition().unwrap(); + let election_config = + generate_election_config(&PathBuf::from(classpath), election_definition.election) + .unwrap(); + + let public_metadata_zip = + zip::ZipArchive::new(std::io::Cursor::new(election_config.public_metadata_blob)) + .unwrap(); + let mut file_names = public_metadata_zip.file_names().collect::>(); + file_names.sort(); + assert_eq!( + file_names, + vec![ + "constants.json", + "election_config.json", + "election_initialized.json", + "manifest.json", + ] + ); + + let private_metadata_zip = + zip::ZipArchive::new(std::io::Cursor::new(election_config.private_metadata_blob)) + .unwrap(); + let mut file_names = private_metadata_zip.file_names().collect::>(); + file_names.sort(); + assert_eq!(file_names, vec!["decryptingTrustee-trustee1.json"]); + } else { + eprintln!("EG_CLASSPATH environment variable not set"); + } + } +} diff --git a/apps/cacvote-jx-terminal/backend/src/main.rs b/apps/cacvote-jx-terminal/backend/src/main.rs index 4a84a8c75..2b455b5b0 100644 --- a/apps/cacvote-jx-terminal/backend/src/main.rs +++ b/apps/cacvote-jx-terminal/backend/src/main.rs @@ -50,6 +50,7 @@ mod app; mod cac; mod config; mod db; +mod electionguard; mod log; mod smartcard; mod sync; diff --git a/apps/cacvote-jx-terminal/backend/src/sync.rs b/apps/cacvote-jx-terminal/backend/src/sync.rs index d1e868f34..b18bbd1b5 100644 --- a/apps/cacvote-jx-terminal/backend/src/sync.rs +++ b/apps/cacvote-jx-terminal/backend/src/sync.rs @@ -107,7 +107,7 @@ async fn pull_objects( #[cfg(test)] mod tests { - use std::{net::TcpListener, sync::Arc}; + use std::{net::TcpListener, path::PathBuf, sync::Arc}; use reqwest::Url; use tracing::Level; @@ -134,6 +134,7 @@ mod tests { public_dir: None, log_level: Level::DEBUG, jurisdiction_code: JurisdictionCode::try_from(JURISDICTION_CODE).unwrap(), + eg_classpath: PathBuf::from("/not/real/path"), }; tokio::spawn(async move { diff --git a/apps/cacvote-jx-terminal/frontend/public/styles.css b/apps/cacvote-jx-terminal/frontend/public/styles.css index 04674d9cb..89736c336 100644 --- a/apps/cacvote-jx-terminal/frontend/public/styles.css +++ b/apps/cacvote-jx-terminal/frontend/public/styles.css @@ -594,10 +594,6 @@ video { width: 100vw; } -.w-20 { - width: 5rem; -} - .table-auto { table-layout: auto; } @@ -770,11 +766,6 @@ video { font-style: italic; } -.text-gray-200 { - --tw-text-opacity: 1; - color: rgb(229 231 235 / var(--tw-text-opacity)); -} - .text-gray-400 { --tw-text-opacity: 1; color: rgb(156 163 175 / var(--tw-text-opacity)); @@ -814,11 +805,6 @@ video { background-color: rgb(209 213 219 / var(--tw-bg-opacity)); } -.hover\:bg-purple-300:hover { - --tw-bg-opacity: 1; - background-color: rgb(216 180 254 / var(--tw-bg-opacity)); -} - .focus\:border-blue-500:focus { --tw-border-opacity: 1; border-color: rgb(59 130 246 / var(--tw-border-opacity)); diff --git a/apps/cacvote-jx-terminal/frontend/src/pages/ballots_page.rs b/apps/cacvote-jx-terminal/frontend/src/pages/ballots_page.rs index 97c8a8aa1..88456dcdb 100644 --- a/apps/cacvote-jx-terminal/frontend/src/pages/ballots_page.rs +++ b/apps/cacvote-jx-terminal/frontend/src/pages/ballots_page.rs @@ -1,15 +1,12 @@ use dioxus::prelude::*; -use types_rs::{ - cacvote::{self, SessionData}, - cdf::cvr::Cvr, -}; +use types_rs::cacvote; use ui_rs::DateOrDateTimeCell; use crate::components::ElectionConfigurationCell; pub fn BallotsPage(cx: Scope) -> Element { let session_data = use_shared_state::(cx).unwrap(); - let SessionData::Authenticated { + let cacvote::SessionData::Authenticated { elections, cast_ballots, .. @@ -37,31 +34,6 @@ struct CastBallotsTableProps { cast_ballots: Vec, } -fn summarize_cast_vote_record(cvr: &Cvr) -> String { - let mut summary = String::new(); - - for snapshot in cvr.cvr_snapshot.iter() { - if let Some(ref contests) = snapshot.cvr_contest { - for contest in contests { - if let Some(ref contest_selections) = contest.cvr_contest_selection { - for contest_selection in contest_selections { - if let Some(ref contest_selection_id) = - contest_selection.contest_selection_id - { - summary.push_str(&format!( - "{}: {}\n", - contest.contest_id, contest_selection_id - )); - } - } - } - } - } - } - - summary -} - fn CastBallotsTable(cx: Scope) -> Element { let elections = &cx.props.elections; @@ -150,18 +122,6 @@ fn CastBallotsTable(cx: Scope) -> Element { }) } } - details { - rsx!(summary { - class: "text-gray-200", - "DEBUG" - }) - { - let summary = summarize_cast_vote_record(&cast_ballot.cvr); - rsx!(pre { - summary - }) - } - } ) } DateOrDateTimeCell { diff --git a/apps/cacvote-jx-terminal/frontend/src/pages/elections_page.rs b/apps/cacvote-jx-terminal/frontend/src/pages/elections_page.rs index 4b189a480..eebdeb8dc 100644 --- a/apps/cacvote-jx-terminal/frontend/src/pages/elections_page.rs +++ b/apps/cacvote-jx-terminal/frontend/src/pages/elections_page.rs @@ -47,6 +47,7 @@ pub fn ElectionsPage(cx: Scope) -> Element { election_definition, jurisdiction_code, mailing_address: mailing_address.get().clone(), + electionguard_election_metadata_blob: vec![], }; let res = client.post(url).json(&election).send().await; diff --git a/apps/cacvote-jx-terminal/frontend/src/pages/voters_page.rs b/apps/cacvote-jx-terminal/frontend/src/pages/voters_page.rs index cccac9269..128219e38 100644 --- a/apps/cacvote-jx-terminal/frontend/src/pages/voters_page.rs +++ b/apps/cacvote-jx-terminal/frontend/src/pages/voters_page.rs @@ -53,7 +53,7 @@ fn PendingRegistrationsTable(cx: Scope) -> Eleme let link_voter_registration_request_and_election = { // TODO: make this work // to_owned![is_linking_registration_request_with_election]; - |create_registration_data: cacvote::CreateRegistrationData| async move { + |create_registration_data: cacvote::CreateRegistrationRequest| async move { // is_linking_registration_request_with_election.set(true); let url = get_url("/api/registrations"); @@ -89,7 +89,7 @@ fn PendingRegistrationsTable(cx: Scope) -> Eleme select { class: "dark:bg-gray-800 dark:text-white dark:border-gray-600 border-2 rounded-md p-2 focus:outline-none focus:border-blue-500", oninput: move |event| { - let create_registration_data = serde_json::from_str::(event.inner().value.as_str()).expect("parse succeeded"); + let create_registration_data = serde_json::from_str::(event.inner().value.as_str()).expect("parse succeeded"); cx.spawn({ to_owned![link_voter_registration_request_and_election, create_registration_data]; async move { @@ -127,7 +127,7 @@ fn PendingRegistrationsTable(cx: Scope) -> Eleme for ballot_style in election_presenter.election.ballot_styles.iter() { for precinct_id in ballot_style.precincts.iter() { { - let create_registration_data = cacvote::CreateRegistrationData { + let create_registration_data = cacvote::CreateRegistrationRequest { election_id: election_presenter.id, registration_request_id: registration_request_presenter.id, ballot_style_id: ballot_style.id.clone(), diff --git a/apps/cacvote-mark/backend/src/cacvote-server/types.ts b/apps/cacvote-mark/backend/src/cacvote-server/types.ts index 882f27226..caa2bc556 100644 --- a/apps/cacvote-mark/backend/src/cacvote-server/types.ts +++ b/apps/cacvote-mark/backend/src/cacvote-server/types.ts @@ -130,7 +130,8 @@ export class Election { constructor( private readonly jurisdictionCode: JurisdictionCode, private readonly electionDefinition: ElectionDefinition, - private readonly mailingAddress: string + private readonly mailingAddress: string, + private readonly electionguardElectionMetadataBlob: Buffer ) {} getJurisdictionCode(): JurisdictionCode { @@ -145,11 +146,17 @@ export class Election { return this.mailingAddress; } + getElectionguardElectionMetadataBlob(): Buffer { + return this.electionguardElectionMetadataBlob; + } + toJSON(): unknown { return { jurisdictionCode: this.jurisdictionCode, electionDefinition: this.electionDefinition, mailingAddress: this.mailingAddress, + electionguardElectionMetadataBlob: + this.electionguardElectionMetadataBlob.toString('base64'), }; } } @@ -161,12 +168,20 @@ const ElectionStructSchema = z.object({ .transform((s) => Buffer.from(s, 'base64').toString('utf-8')) .transform((s) => safeParseElectionDefinition(s).unsafeUnwrap()), mailingAddress: z.string(), + electionguardElectionMetadataBlob: z + .string() + .transform((s) => Buffer.from(s, 'base64')), }); export const ElectionSchema: z.ZodSchema = ElectionStructSchema.transform( (o) => - new Election(o.jurisdictionCode, o.electionDefinition, o.mailingAddress) + new Election( + o.jurisdictionCode, + o.electionDefinition, + o.mailingAddress, + o.electionguardElectionMetadataBlob + ) ) as unknown as z.ZodSchema; export class RegistrationRequest { @@ -416,7 +431,8 @@ export const PayloadSchema: z.ZodSchema = z new Election( o.jurisdictionCode, o.electionDefinition, - o.mailingAddress + o.mailingAddress, + o.electionguardElectionMetadataBlob ) ); } diff --git a/libs/types-rs/src/cacvote/mod.rs b/libs/types-rs/src/cacvote/mod.rs index c04985b5a..218b32db9 100644 --- a/libs/types-rs/src/cacvote/mod.rs +++ b/libs/types-rs/src/cacvote/mod.rs @@ -7,7 +7,6 @@ use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use uuid::Uuid; -use crate::cdf::cvr::Cvr; use crate::election::BallotStyleId; use crate::election::ElectionDefinition; use crate::election::ElectionHash; @@ -199,6 +198,7 @@ pub enum Payload { Registration(Registration), Election(Election), CastBallot(CastBallot), + ShuffledEncryptedCastBallot(ShuffledEncryptedCastBallot), } impl Payload { @@ -208,6 +208,9 @@ impl Payload { Self::Registration(_) => Self::registration_object_type(), Self::Election(_) => Self::election_object_type(), Self::CastBallot(_) => Self::cast_ballot_object_type(), + Self::ShuffledEncryptedCastBallot(_) => { + Self::shuffled_encrypted_cast_ballot_object_type() + } } } @@ -234,6 +237,12 @@ impl Payload { // `Payload` enum. "CastBallot" } + + pub fn shuffled_encrypted_cast_ballot_object_type() -> &'static str { + // This must match the naming rules of the `serde` attribute in the + // `Payload` enum. + "ShuffledEncryptedCastBallot" + } } impl JurisdictionScoped for Payload { @@ -243,6 +252,9 @@ impl JurisdictionScoped for Payload { Self::Registration(registration) => registration.jurisdiction_code(), Self::Election(election) => election.jurisdiction_code(), Self::CastBallot(cast_ballot) => cast_ballot.jurisdiction_code(), + Self::ShuffledEncryptedCastBallot(_) => { + unimplemented!("TODO: pull from field") + } } } } @@ -473,12 +485,23 @@ impl Registration { } } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CreateElectionRequest { + pub jurisdiction_code: JurisdictionCode, + pub election_definition: ElectionDefinition, + pub mailing_address: String, +} + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct Election { pub jurisdiction_code: JurisdictionCode, pub election_definition: ElectionDefinition, pub mailing_address: String, + + #[serde(with = "Base64Standard")] + pub electionguard_election_metadata_blob: Vec, } impl JurisdictionScoped for Election { @@ -503,7 +526,7 @@ pub struct CastBallot { pub registration_request_object_id: Uuid, pub registration_object_id: Uuid, pub election_object_id: Uuid, - pub cvr: Cvr, + pub electionguard_encrypted_cvr_blob: Vec, } impl CastBallot { @@ -532,14 +555,6 @@ impl JurisdictionScoped for CastBallot { } } -impl Deref for CastBallot { - type Target = Cvr; - - fn deref(&self) -> &Self::Target { - &self.cvr - } -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct CastBallotPresenter { @@ -576,10 +591,6 @@ impl CastBallotPresenter { &self.registration_request } - pub fn cvr(&self) -> &Cvr { - &self.cvr - } - pub fn created_at(&self) -> OffsetDateTime { self.created_at } @@ -597,6 +608,11 @@ impl Deref for CastBallotPresenter { } } +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +// TODO: fill in these fields +pub struct ShuffledEncryptedCastBallot; + #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub enum SessionData { @@ -622,20 +638,13 @@ impl Default for SessionData { #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct CreateRegistrationData { +pub struct CreateRegistrationRequest { pub election_id: Uuid, pub registration_request_id: Uuid, pub ballot_style_id: BallotStyleId, pub precinct_id: PrecinctId, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -#[serde(rename_all = "camelCase")] -pub struct CreateElectionData { - pub election_data: String, - pub return_address: String, -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ElectionPresenter { pub id: Uuid,