Browse Source

Initial commit

Ian Goldberg 1 year ago
commit
62601eb0e3
5 changed files with 762 additions and 0 deletions
  1. 2 0
      .gitignore
  2. 25 0
      Cargo.toml
  3. 94 0
      README.md
  4. 343 0
      src/main.rs
  5. 298 0
      src/params.rs

+ 2 - 0
.gitignore

@@ -0,0 +1,2 @@
+/Cargo.lock
+/target

+ 25 - 0
Cargo.toml

@@ -0,0 +1,25 @@
+[package]
+name = "spiral-spir"
+version = "0.1.0"
+authors = ["Ian Goldberg <iang@uwaterloo.ca>"]
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+aes = "0.8"
+# curve25519-dalek needs this older version of rand
+rand07 = { package = "rand", version = "0.7" }
+# but Spiral needs this newer version
+rand = "0.8"
+curve25519-dalek = { package = "curve25519-dalek-ng", version = "3", default-features = false, features = ["serde", "std"] }
+lazy_static = "1"
+sha2 = "0.9"
+subtle = { package = "subtle-ng", version = "2.4" }
+spiral-rs = { git = "https://github.com/menonsamir/spiral-rs/", rev = "0f9bdc157" }
+
+[features]
+default = ["u64_backend"]
+u32_backend = ["curve25519-dalek/u32_backend"]
+u64_backend = ["curve25519-dalek/u64_backend"]
+simd_backend = ["curve25519-dalek/simd_backend"]

+ 94 - 0
README.md

@@ -0,0 +1,94 @@
+# Symmetric Private Information Retrieval (SPIR) built on Spiral
+
+*Ian Goldberg (iang@uwaterloo.ca), July 2022*
+
+This code implements Symmetric Private Information Retrieval, building
+on the Spiral PIR library (this code is not written by the Spiral
+authors).
+
+Spiral is a recent single-server (computational) PIR system from 2022:
+
+  * Samir Jordan Menon, David J. Wu,
+"Spiral: Fast, High-Rate Single-Server PIR via FHE Composition".
+IEEE Symposium on Security and Privacy, 2022.
+eprint:
+[https://eprint.iacr.org/2022/368](https://eprint.iacr.org/2022/368),
+code:
+[https://github.com/menonsamir/spiral-rs](https://github.com/menonsamir/spiral-rs)
+
+In ordinary PIR, the client learns the database record they were looking for, and the server does not learn which record that was.  The client is _not_ prevented, however, from learning _additional_ database records.  Downloading the whole database, for example, can be seen as a trivial form of PIR (though one that transfers way too much data, except for tiny databases).
+
+In Symmetric PIR (SPIR), the client must learn only one database record, in addition to the server learning no information about which record that was.  SPIR is similar to oblivious transfer (OT), except that SPIR aims to have sublinear communication (at no time is something the size of the whole database downloaded), while OT does not have that restriction.
+
+This code builds SPIR from ordinary PIR (using Spiral), using a strategy based on that of Naor and Pinkas:
+
+  * Moni Naor and Benny Pinkas, "Oblivious Transfer and Polynomial Evaluation".
+STOC 1999.  paper: [https://dl.acm.org/doi/10.1145/301250.301312](https://dl.acm.org/doi/10.1145/301250.301312)
+
+The general strategy is this (for a database with N=2^r records):
+
+  1. Server: Encrypt each record of the database with a unique key
+  2. Client: Use 1-of-N OT to retrieve exactly one key
+  3. Client: Privately download the encrypted record and decrypt it with the key retrieved above
+
+In the Naor and Pinkas paper, step 3 was just trivial download of the whole encrypted database, but we use Sprial to privately retrieve the encrypted record of interest.  Note, however, that the client will typically learn additional nearby records as well, which is why we need to encrypt each record separately.
+
+The 1-of-N OT is built from r 1-of-2 OTs in the same manner as in the Naor and Pinkas paper: the server picks r pairs of random keys, and the client chooses one key from each pair, according to the bits of the desired query index value.  Then each database entry is encrypted with the combination of keys associated with its index.
+
+The Naor and Pinkas paper uses this for its encryption, where F is a pseudorandom function (PRF) family:
+
+   Enc[j] = Db[j] XOR F\_{K_1,j1}(j) XOR F\_{K_2,j2}(j) XOR ... XOR F\_{K_r,jr}(j)
+
+where j1,j2,...,jr are the bits of j, as j ranges from 0 to N-1.
+
+We instead use:
+
+   Ktot = K_1,j1 XOR K_2,k2 XOR ... XOR K_r,kr
+
+   Enc[j] = Db[j] XOR F\_{Ktot}(j)
+
+That is, Naor and Pinkas XOR the _outputs_ of the PRF, whereas we XOR the _keys_.  This requires a slightly stronger assumption on the PRF family (for example, that the functions with different keys are independent, even if the keys have some known XOR relation), but we're going to use AES for F, so that should be fine.
+
+For the 1-of-2 OT protocol, we base it on the simple one from the 1989 paper of Bellare and Micali:
+
+  * Mihir Bellare and Silvio Micali, "Non-Interactive Oblivious Transfer and Applications".  CRYPTO 1989.  paper: [https://cseweb.ucsd.edu/~mihir/papers/niot.pdf](https://cseweb.ucsd.edu/~mihir/papers/niot.pdf)
+
+We slightly optimize the protocol in that instead of the client sending both β0 and β1 to the server, and the server checking that their sum (in elliptic curve terms) is C, the client just sends β0 and the server computes β1 = C-β0.  In addition, the server sends back the two keys ElGamal-encrypted to the keys β0 and β1, where the client can know the private key for at most one of them.  Bellare and Micali's paper does this as (r0\*G, H(r0\*β0) XOR K0, r1\*G, H(r1\*β1) XOR K1), whereas we use the slightly more efficient (r\*G, H(r\*β0) XOR K0, H(r\*β1) XOR K1).
+
+## Running the code
+
+To run the code:
+
+`cargo run --release 20`
+
+Where `20` is the value of r (that is, the database will have N=2^20 entries).  Each entry is 8 bytes.  There are three phases of execution: a one-time Spiral public key generation (this only has to be done once, regardless of how many SPIR queries you do), a preprocessing phase per SPIR query (this can be done _before_ knowing the contents of the database on the server side, or the desired index on the client side), and the runtime phase per SPIR query (once those two things are known).
+
+A sample output (for r=20):
+
+```
+===== ONE-TIME SETUP =====
+
+Using a 2048 x 4096 byte database (8388608 bytes total)
+OT one-time setup: 3064 µs
+Spiral client one-time setup: 224935 µs, 10878976 bytes
+
+===== PREPROCESSING =====
+
+rand_idx = 425489 rand_pir_idx = 831
+Spiral query: 691 µs, 32768 bytes
+key OT query in 365 µs, 640 bytes
+key OT serve in 1860 µs, 1280 bytes
+key OT receive in 1148 µs
+
+===== RUNTIME =====
+
+Send to server 8 bytes
+Server encrypt database 89036 µs
+Server load database 974249 µs
+expansion (took 166596 us).
+Server compute response 344613 µs, 14336 bytes (*including* the above expansion time)
+Client decode response 888 µs
+index = 948810, Response = 9488100948830
+```
+
+The various lines show the amount of compute time taken and the amount of data transferred between the client and the server.  The last line shows the random index that was looked up, and the database value the client retrieved.  The value for index i should be (10000001*i+20).

+ 343 - 0
src/main.rs

@@ -0,0 +1,343 @@
+// We really want points to be capital letters and scalars to be
+// lowercase letters
+#![allow(non_snake_case)]
+
+pub mod params;
+
+use aes::cipher::{BlockEncrypt, KeyInit};
+use aes::Aes128Enc;
+use aes::Block;
+use std::env;
+use std::io::Cursor;
+use std::mem;
+use std::time::Instant;
+use subtle::Choice;
+use subtle::ConditionallySelectable;
+
+use rand::RngCore;
+
+use sha2::Digest;
+use sha2::Sha256;
+use sha2::Sha512;
+
+use curve25519_dalek::constants as dalek_constants;
+use curve25519_dalek::ristretto::CompressedRistretto;
+use curve25519_dalek::ristretto::RistrettoBasepointTable;
+use curve25519_dalek::ristretto::RistrettoPoint;
+use curve25519_dalek::scalar::Scalar;
+
+use spiral_rs::client::*;
+use spiral_rs::params::*;
+use spiral_rs::server::*;
+
+use lazy_static::lazy_static;
+
+type DbEntry = u64;
+
+// Generators of the Ristretto group (the standard B and another one C,
+// for which the DL relationship is unknown), and their precomputed
+// multiplication tables.  Used for the Oblivious Transfer protocol
+lazy_static! {
+    pub static ref OT_B: RistrettoPoint = dalek_constants::RISTRETTO_BASEPOINT_POINT;
+    pub static ref OT_C: RistrettoPoint =
+        RistrettoPoint::hash_from_bytes::<Sha512>(b"OT Generator C");
+    pub static ref OT_B_TABLE: RistrettoBasepointTable = dalek_constants::RISTRETTO_BASEPOINT_TABLE;
+    pub static ref OT_C_TABLE: RistrettoBasepointTable = RistrettoBasepointTable::create(&OT_C);
+}
+
+// XOR a 16-byte slice into a Block (which will be used as an AES key)
+fn xor16(outar: &mut Block, inar: &[u8; 16]) {
+    for i in 0..16 {
+        outar[i] ^= inar[i];
+    }
+}
+
+// Encrypt a database of 2^r elements, where each element is a DbEntry,
+// using the 2*r provided keys (r pairs of keys).  Also add the provided
+// blinding factor to each element before encryption (the same blinding
+// factor for all elements).  Each element is encrypted in AES counter
+// mode, with the counter being the element number and the key computed
+// as the XOR of r of the provided keys, one from each pair, according
+// to the bits of the element number.  Outputs a byte vector containing
+// the encrypted database.
+fn encdb_xor_keys(db: &[DbEntry], keys: &[[u8; 16]], r: usize, blind: DbEntry) -> Vec<u8> {
+    let num_records: usize = 1 << r;
+    let mut ret = Vec::<u8>::with_capacity(num_records * mem::size_of::<DbEntry>());
+    for j in 0..num_records {
+        let mut key = Block::from([0u8; 16]);
+        for i in 0..r {
+            let bit = if (j & (1 << i)) == 0 { 0 } else { 1 };
+            xor16(&mut key, &keys[2 * i + bit]);
+        }
+        let aes = Aes128Enc::new(&key);
+        let mut block = Block::from([0u8; 16]);
+        block[0..8].copy_from_slice(&j.to_le_bytes());
+        aes.encrypt_block(&mut block);
+        let aeskeystream = DbEntry::from_le_bytes(block[0..8].try_into().unwrap());
+        let encelem = (db[j].wrapping_add(blind)) ^ aeskeystream;
+        ret.extend(encelem.to_le_bytes());
+    }
+    ret
+}
+
+// Generate the keys for encrypting the database
+fn gen_db_enc_keys(r: usize) -> Vec<[u8; 16]> {
+    let mut keys: Vec<[u8; 16]> = Vec::new();
+
+    let mut rng = rand::thread_rng();
+    for _ in 0..2 * r {
+        let mut k: [u8; 16] = [0; 16];
+        rng.fill_bytes(&mut k);
+        keys.push(k);
+    }
+    keys
+}
+
+// 1-out-of-2 Oblivious Transfer (OT)
+
+fn ot12_request(sel: Choice) -> ((Choice, Scalar), [u8; 32]) {
+    let Btable: &RistrettoBasepointTable = &OT_B_TABLE;
+    let C: &RistrettoPoint = &OT_C;
+    let mut rng = rand07::thread_rng();
+    let x = Scalar::random(&mut rng);
+    let xB = &x * Btable;
+    let CmxB = C - xB;
+    let P = RistrettoPoint::conditional_select(&xB, &CmxB, sel);
+    ((sel, x), P.compress().to_bytes())
+}
+
+fn ot12_serve(query: &[u8; 32], m0: &[u8; 16], m1: &[u8; 16]) -> [u8; 64] {
+    let Btable: &RistrettoBasepointTable = &OT_B_TABLE;
+    let Ctable: &RistrettoBasepointTable = &OT_C_TABLE;
+    let mut rng = rand07::thread_rng();
+    let y = Scalar::random(&mut rng);
+    let yB = &y * Btable;
+    let yC = &y * Ctable;
+    let P = CompressedRistretto::from_slice(query).decompress().unwrap();
+    let yP0 = y * P;
+    let yP1 = yC - yP0;
+    let mut HyP0 = Sha256::digest(yP0.compress().as_bytes());
+    for i in 0..16 {
+        HyP0[i] ^= m0[i];
+    }
+    let mut HyP1 = Sha256::digest(yP1.compress().as_bytes());
+    for i in 0..16 {
+        HyP1[i] ^= m1[i];
+    }
+    let mut ret = [0u8; 64];
+    ret[0..32].copy_from_slice(yB.compress().as_bytes());
+    ret[32..48].copy_from_slice(&HyP0[0..16]);
+    ret[48..64].copy_from_slice(&HyP1[0..16]);
+    ret
+}
+
+fn ot12_receive(state: (Choice, Scalar), response: &[u8; 64]) -> [u8; 16] {
+    let yB = CompressedRistretto::from_slice(&response[0..32])
+        .decompress()
+        .unwrap();
+    let yP = state.1 * yB;
+    let mut HyP = Sha256::digest(yP.compress().as_bytes());
+    for i in 0..16 {
+        HyP[i] ^= u8::conditional_select(&response[32 + i], &response[48 + i], state.0);
+    }
+    HyP[0..16].try_into().unwrap()
+}
+
+// Obliviously fetch the key for element q of the database (which has
+// 2^r elements total).  Each bit of q is used in a 1-out-of-2 OT to get
+// one of the keys in each of the r pairs of keys on the server side.
+// The resulting r keys are XORed together.
+
+fn otkey_request(q: usize, r: usize) -> (Vec<(Choice, Scalar)>, Vec<[u8; 32]>) {
+    let mut state: Vec<(Choice, Scalar)> = Vec::with_capacity(r);
+    let mut query: Vec<[u8; 32]> = Vec::with_capacity(r);
+    for i in 0..r {
+        let bit = ((q >> i) & 1) as u8;
+        let (si, qi) = ot12_request(bit.into());
+        state.push(si);
+        query.push(qi);
+    }
+    (state, query)
+}
+
+fn otkey_serve(query: Vec<[u8; 32]>, keys: &Vec<[u8; 16]>) -> Vec<[u8; 64]> {
+    let r = query.len();
+    assert!(keys.len() == 2 * r);
+    let mut response: Vec<[u8; 64]> = Vec::with_capacity(r);
+    for i in 0..r {
+        response.push(ot12_serve(&query[i], &keys[2 * i], &keys[2 * i + 1]));
+    }
+    response
+}
+
+fn otkey_receive(state: Vec<(Choice, Scalar)>, response: &Vec<[u8; 64]>) -> Block {
+    let r = state.len();
+    assert!(response.len() == r);
+    let mut key = Block::from([0u8; 16]);
+    for i in 0..r {
+        xor16(&mut key, &ot12_receive(state[i], &response[i]));
+    }
+    key
+}
+
+// Having received the key for element q with r parallel 1-out-of-2 OTs,
+// and having received the encrypted element with (non-symmetric) PIR,
+// use the key to decrypt the element.
+fn otkey_decrypt(key: &Block, q: usize, encelement: DbEntry) -> DbEntry {
+    let aes = Aes128Enc::new(key);
+    let mut block = Block::from([0u8; 16]);
+    block[0..8].copy_from_slice(&q.to_le_bytes());
+    aes.encrypt_block(&mut block);
+    let aeskeystream = DbEntry::from_le_bytes(block[0..8].try_into().unwrap());
+    encelement ^ aeskeystream
+}
+
+// Things that are only done once total, not once for each SPIR
+fn one_time_setup() {
+    // Resolve the lazy statics
+    let _B: &RistrettoPoint = &OT_B;
+    let _Btable: &RistrettoBasepointTable = &OT_B_TABLE;
+    let _C: &RistrettoPoint = &OT_C;
+    let _Ctable: &RistrettoBasepointTable = &OT_C_TABLE;
+}
+
+fn print_params_summary(params: &Params) {
+    let db_elem_size = params.item_size();
+    let total_size = params.num_items() * db_elem_size;
+    println!(
+        "Using a {} x {} byte database ({} bytes total)",
+        params.num_items(),
+        db_elem_size,
+        total_size
+    );
+}
+
+fn main() {
+    let args: Vec<String> = env::args().collect();
+    if args.len() != 2 {
+        println!("Usage: {} r\nr = log_2(num_records)", args[0]);
+        return;
+    }
+    let r: usize = args[1].parse().unwrap();
+    let num_records = 1 << r;
+
+    println!("===== ONE-TIME SETUP =====\n");
+
+    let otsetup_start = Instant::now();
+    let spiral_params = params::get_spiral_params(r);
+    let mut rng = rand::thread_rng();
+    one_time_setup();
+    let otsetup_us = otsetup_start.elapsed().as_micros();
+    print_params_summary(&spiral_params);
+    println!("OT one-time setup: {} µs", otsetup_us);
+
+    // One-time setup for the Spiral client
+    let spc_otsetup_start = Instant::now();
+    let mut clientrng = rand::thread_rng();
+    let mut client = Client::init(&spiral_params, &mut clientrng);
+    let pub_params = client.generate_keys();
+    let pub_params_buf = pub_params.serialize();
+    let spc_otsetup_us = spc_otsetup_start.elapsed().as_micros();
+    let spiral_blocking_factor = spiral_params.db_item_size / mem::size_of::<DbEntry>();
+    println!(
+        "Spiral client one-time setup: {} µs, {} bytes",
+        spc_otsetup_us,
+        pub_params_buf.len()
+    );
+
+    println!("\n===== PREPROCESSING =====\n");
+
+    // Spiral preprocessing: create a PIR lookup for an element at a
+    // random location
+    let spc_query_start = Instant::now();
+    let rand_idx = (rng.next_u64() as usize) % num_records;
+    let rand_pir_idx = rand_idx / spiral_blocking_factor;
+    println!("rand_idx = {} rand_pir_idx = {}", rand_idx, rand_pir_idx);
+    let spc_query = client.generate_query(rand_pir_idx);
+    let spc_query_buf = spc_query.serialize();
+    let spc_query_us = spc_query_start.elapsed().as_micros();
+    println!(
+        "Spiral query: {} µs, {} bytes",
+        spc_query_us,
+        spc_query_buf.len()
+    );
+
+    // Create the database encryption keys and do the OT to fetch the
+    // right one, but don't actually encrypt the database yet
+    let dbkeys = gen_db_enc_keys(r);
+    let otkeyreq_start = Instant::now();
+    let (keystate, keyquery) = otkey_request(rand_idx, r);
+    let keyquerysize = keyquery.len() * keyquery[0].len();
+    let otkeyreq_us = otkeyreq_start.elapsed().as_micros();
+    let otkeysrv_start = Instant::now();
+    let keyresponse = otkey_serve(keyquery, &dbkeys);
+    let keyrespsize = keyresponse.len() * keyresponse[0].len();
+    let otkeysrv_us = otkeysrv_start.elapsed().as_micros();
+    let otkeyrcv_start = Instant::now();
+    let otkey = otkey_receive(keystate, &keyresponse);
+    let otkeyrcv_us = otkeyrcv_start.elapsed().as_micros();
+    println!("key OT query in {} µs, {} bytes", otkeyreq_us, keyquerysize);
+    println!("key OT serve in {} µs, {} bytes", otkeysrv_us, keyrespsize);
+    println!("key OT receive in {} µs", otkeyrcv_us);
+
+    // Create a database with recognizable contents
+    let mut db: Vec<DbEntry> = ((0 as DbEntry)..(num_records as DbEntry))
+        .map(|x| 10000001 * x)
+        .collect();
+
+    println!("\n===== RUNTIME =====\n");
+
+    // Pick the record we actually want to query
+    let q = (rng.next_u64() as usize) % num_records;
+
+    // Compute the offset from the record index we're actually looking
+    // for to the random one we picked earlier.  Tell it to the server,
+    // who will rotate right the database by that amount before
+    // encrypting it.
+    let idx_offset = (num_records + rand_idx - q) % num_records;
+
+    println!("Send to server {} bytes", 8 /* sizeof(idx_offset) */);
+
+    // The server rotates, blinds, and encrypts the database
+    let blind: DbEntry = 20;
+    let encdb_start = Instant::now();
+    db.rotate_right(idx_offset);
+    let encdb = encdb_xor_keys(&db, &dbkeys, r, blind);
+    let encdb_us = encdb_start.elapsed().as_micros();
+    println!("Server encrypt database {} µs", encdb_us);
+
+    // Load the encrypted database into Spiral
+    let sps_loaddb_start = Instant::now();
+    let sps_db = load_db_from_seek(&spiral_params, &mut Cursor::new(encdb));
+    let sps_loaddb_us = sps_loaddb_start.elapsed().as_micros();
+    println!("Server load database {} µs", sps_loaddb_us);
+
+    // Do the PIR query
+    let sps_query_start = Instant::now();
+    let sps_query = Query::deserialize(&spiral_params, &spc_query_buf);
+    let sps_response = process_query(&spiral_params, &pub_params, &sps_query, sps_db.as_slice());
+    let sps_query_us = sps_query_start.elapsed().as_micros();
+    println!(
+        "Server compute response {} µs, {} bytes (*including* the above expansion time)",
+        sps_query_us,
+        sps_response.len()
+    );
+
+    // Decode the response to yield the whole Spiral block
+    let spc_recv_start = Instant::now();
+    let encdbblock = client.decode_response(sps_response.as_slice());
+    // Extract the one encrypted DbEntry we were looking for (and the
+    // only one we are able to decrypt)
+    let entry_in_block = rand_idx % spiral_blocking_factor;
+    let loc_in_block = entry_in_block * mem::size_of::<DbEntry>();
+    let loc_in_block_end = (entry_in_block + 1) * mem::size_of::<DbEntry>();
+    let encdbentry = DbEntry::from_le_bytes(
+        encdbblock[loc_in_block..loc_in_block_end]
+            .try_into()
+            .unwrap(),
+    );
+    let decdbentry = otkey_decrypt(&otkey, rand_idx, encdbentry);
+    let spc_recv_us = spc_recv_start.elapsed().as_micros();
+    println!("Client decode response {} µs", spc_recv_us);
+    println!("index = {}, Response = {}", q, decdbentry);
+}

+ 298 - 0
src/params.rs

@@ -0,0 +1,298 @@
+use spiral_rs::params::*;
+use spiral_rs::util::*;
+
+// Get the Spiral params for a database of 2^r 8-byte entries.  These
+// params are taken from spiral's params_store.json file, but adjusted
+// for 8-byte entries.
+
+pub fn get_spiral_params(r: usize) -> Params {
+    let json = match r {
+        15 => {
+            r#"{
+      "n": 2,
+      "nu_1": 6,
+      "nu_2": 4,
+      "p": 4,
+      "q2_bits": 13,
+      "t_gsw": 4,
+      "t_conv": 4,
+      "t_exp_left": 4,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 256
+    }"#
+        }
+        16 => {
+            r#"{
+      "n": 2,
+      "nu_1": 6,
+      "nu_2": 4,
+      "p": 4,
+      "q2_bits": 13,
+      "t_gsw": 4,
+      "t_conv": 4,
+      "t_exp_left": 4,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 512
+    }"#
+        }
+        17 => {
+            r#"{
+      "n": 2,
+      "nu_1": 6,
+      "nu_2": 4,
+      "p": 4,
+      "q2_bits": 13,
+      "t_gsw": 4,
+      "t_conv": 4,
+      "t_exp_left": 4,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 1024
+    }"#
+        }
+        18 => {
+            r#"{
+      "n": 2,
+      "nu_1": 6,
+      "nu_2": 4,
+      "p": 4,
+      "q2_bits": 13,
+      "t_gsw": 4,
+      "t_conv": 4,
+      "t_exp_left": 4,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 2048
+    }"#
+        }
+        19 => {
+            r#"{
+      "n": 2,
+      "nu_1": 7,
+      "nu_2": 4,
+      "p": 4,
+      "q2_bits": 13,
+      "t_gsw": 4,
+      "t_conv": 32,
+      "t_exp_left": 4,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 2048
+    }"#
+        }
+        20 => {
+            r#"{
+      "n": 2,
+      "nu_1": 7,
+      "nu_2": 4,
+      "p": 16,
+      "q2_bits": 16,
+      "t_gsw": 4,
+      "t_conv": 4,
+      "t_exp_left": 4,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 4096
+    }"#
+        }
+        21 => {
+            r#"{
+      "n": 2,
+      "nu_1": 7,
+      "nu_2": 5,
+      "p": 16,
+      "q2_bits": 16,
+      "t_gsw": 5,
+      "t_conv": 4,
+      "t_exp_left": 4,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 4096
+    }"#
+        }
+        22 => {
+            r#"{
+      "n": 2,
+      "nu_1": 8,
+      "nu_2": 5,
+      "p": 16,
+      "q2_bits": 16,
+      "t_gsw": 5,
+      "t_conv": 4,
+      "t_exp_left": 4,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 4096
+    }"#
+        }
+        23 => {
+            r#"{
+      "n": 2,
+      "nu_1": 8,
+      "nu_2": 6,
+      "p": 16,
+      "q2_bits": 16,
+      "t_gsw": 5,
+      "t_conv": 4,
+      "t_exp_left": 4,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 4096
+    }"#
+        }
+        24 => {
+            r#"{
+      "n": 2,
+      "nu_1": 8,
+      "nu_2": 6,
+      "p": 256,
+      "q2_bits": 20,
+      "t_gsw": 8,
+      "t_conv": 4,
+      "t_exp_left": 8,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 8192
+    }"#
+        }
+        25 => {
+            r#"{
+      "n": 2,
+      "nu_1": 9,
+      "nu_2": 6,
+      "p": 256,
+      "q2_bits": 20,
+      "t_gsw": 8,
+      "t_conv": 4,
+      "t_exp_left": 8,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 8192
+    }"#
+        }
+        26 => {
+            r#"{
+      "n": 2,
+      "nu_1": 9,
+      "nu_2": 7,
+      "p": 256,
+      "q2_bits": 20,
+      "t_gsw": 8,
+      "t_conv": 4,
+      "t_exp_left": 8,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 8192
+    }"#
+        }
+        27 => {
+            r#"{
+      "n": 2,
+      "nu_1": 9,
+      "nu_2": 8,
+      "p": 256,
+      "q2_bits": 20,
+      "t_gsw": 8,
+      "t_conv": 4,
+      "t_exp_left": 8,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 8192
+    }"#
+        }
+        28 => {
+            r#"{
+      "n": 2,
+      "nu_1": 10,
+      "nu_2": 8,
+      "p": 256,
+      "q2_bits": 20,
+      "t_gsw": 8,
+      "t_conv": 4,
+      "t_exp_left": 16,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 8192
+    }"#
+        }
+        29 => {
+            r#"{
+      "n": 2,
+      "nu_1": 10,
+      "nu_2": 9,
+      "p": 256,
+      "q2_bits": 23,
+      "t_gsw": 9,
+      "t_conv": 4,
+      "t_exp_left": 16,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 8192
+    }"#
+        }
+        30 => {
+            r#"{
+      "n": 4,
+      "nu_1": 10,
+      "nu_2": 8,
+      "p": 256,
+      "q2_bits": 20,
+      "t_gsw": 8,
+      "t_conv": 4,
+      "t_exp_left": 16,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 32768
+    }"#
+        }
+        31 => {
+            r#"{
+      "n": 8,
+      "nu_1": 10,
+      "nu_2": 8,
+      "p": 256,
+      "q2_bits": 20,
+      "t_gsw": 8,
+      "t_conv": 4,
+      "t_exp_left": 16,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 65536
+    }"#
+        }
+        32 => {
+            r#"{
+      "n": 8,
+      "nu_1": 10,
+      "nu_2": 9,
+      "p": 256,
+      "q2_bits": 20,
+      "t_gsw": 10,
+      "t_conv": 32,
+      "t_exp_left": 16,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 65536
+    }"#
+        }
+        33 => {
+            r#"{
+      "n": 8,
+      "nu_1": 10,
+      "nu_2": 10,
+      "p": 256,
+      "q2_bits": 20,
+      "t_gsw": 10,
+      "t_conv": 32,
+      "t_exp_left": 16,
+      "t_exp_right": 56,
+      "instances": 1,
+      "db_item_size": 65536
+    }"#
+        }
+        _ => panic!(),
+    };
+    params_from_json(json)
+}