Crate perfume

Crate perfume 

Source
Expand description

Impromptu conversion of sensitive metadata to persistent random names. Given the same identifier, always returns the same random name. Provides a portable and secure hash-based storage mechanism so that sensitive information does not need to be stored.

§Examples

examples/remote_store_ureq.rs

use std::io::Error;
use std::result::Result;

use bytes::Bytes;
use const_env::env_item;

use perfume::identity::{ConnectionBridge, Population, RemoteStore};

mod common;
use common::test_server;

// generated for this example with `TMPDIR=/tmp cargo run -F codegen`
include!(concat!(env!("TMPDIR"), "/perfume.rs"));

#[env_item]
const PERFUME_SECRET: &[u8] = b"3D5aPzC0jwT25eAWlEa4FcW8d9FNz00g";

const BHUTANESE: Population = Population {
    domain: "bt",
    secret: PERFUME_SECRET,            // 32 bytes for keyed hasher
    ingredients: &PERFUME_INGREDIENTS, // see build.rs example below
};

fn main() {
    let _server_handle = test_server("127.0.0.1:9090");

    let mut store = RemoteStore {
        bridge: ExampleBridge {
            url: "http://localhost:9090".try_into().unwrap(),
            domain: BHUTANESE.domain.to_string(),
        },
    };

    let user1 = BHUTANESE.identity("flying@wom.bt", &mut store).unwrap();
    let user2 = BHUTANESE.identity("fast@serpent.bt", &mut store).unwrap();
    let user3 = BHUTANESE.identity("yogi@garbha.bt", &mut store).unwrap();
    println!(
        "{}\n{}\n{}",
        user1.friendly_name, user2.friendly_name, user3.friendly_name
    );

    assert_eq!(
        BHUTANESE.identity("flying@wom.bt", &mut store).unwrap(),
        user1
    );

    // storage is based on the 64 character hash output of the identifier "flying@wom.bt"
    let stored_blob = store
        .bridge
        .get(user1.storage.key.as_str()) // storage key is the first 3 characters of the hash
        .unwrap()
        .unwrap();
    assert_eq!(
        String::from_utf8_lossy(stored_blob.as_ref()),
        // first line of the blob is the last 61 characters of the hash,
        // followed by an offset into a list of random names
        String::from_utf8_lossy(
            [user1.storage.digest.as_str().as_bytes(), b"     0"]
                .concat()
                .as_ref()
        )
    );
}

struct ExampleBridge {
    url: http::Uri,
    domain: String,
}

impl ConnectionBridge for ExampleBridge {
    fn get(&self, key: &str) -> Result<Option<Bytes>, Error> {
        let resource_url = format!("{}{}/{}", self.url, self.domain, key);
        let response = ureq::get(&resource_url)
            .config()
            .http_status_as_error(false)
            .build()
            .call()
            .map_err(|e| Error::other(format!("IO failure on request to {resource_url}: {e}")))?;
        match response.status() {
            http::StatusCode::OK => {
                let body = response.into_body().read_to_vec().map_err(|e| {
                    Error::other(format!(
                        "error parsing response body on request to {resource_url}: {e}"
                    ))
                })?;
                Ok(Some(Bytes::from(body)))
            }
            http::StatusCode::NOT_FOUND => Ok(None),
            unexpected => Err(Error::other(format!(
                "unexpected HTTP response on request to {resource_url}: {unexpected}"
            ))),
        }
    }

    fn put(&self, key: &str, body: Bytes) -> Result<(), Error> {
        let resource_url = format!("{}{}/{}", self.url, self.domain, key);
        let response = ureq::put(&resource_url)
            .config()
            .http_status_as_error(false)
            .build()
            .send(&body[..])
            .map_err(|e| Error::other(format!("IO failure on request to {resource_url}: {e}")))?;
        match response.status() {
            http::StatusCode::OK => Ok(()),
            unexpected => Err(Error::other(format!(
                "unexpected HTTP response on request to {resource_url}: {unexpected}"
            ))),
        }
    }

    async fn get_async(&self, _key: &str) -> Result<Option<Bytes>, Error> {
        unimplemented!()
    }

    async fn put_async(&self, _key: &str, _body: Bytes) -> Result<(), Error> {
        unimplemented!()
    }
}

Cargo.toml

[build-dependencies]
perfume = { version = "0.1", features = ["codegen"] }

[dependencies]
perfume = "0.1"
phf = { version = "0.12", default-features = false }

build.rs

use perfume::codegen;

let out_dir = std::env::var_os("OUT_DIR").unwrap();
let out_path = std::path::Path::new(&out_dir).join("perfume.rs");
codegen::ingredients(
    "PERFUME_INGREDIENTS",
    codegen::PopulationSize::Bhutan, // chosen only once
    "data/gerunds.txt",
    "data/colors.txt",
    "data/animals.txt",
    out_path,
).unwrap_or_else(|e| panic!("{e}"));

Include the generated code in a module using include!(concat!(env!("OUT_DIR"), "/perfume.rs"));

The word lists such as gerunds.txt can be found in the git repository.

Modules§

hex_string
An explicitly sized string of lowercase hexadecimal characters.
identity
Persistent random name generator.

Enums§

Error
All errors generated by this crate.

Constants§

STORAGE_DIGEST_LENGTH
The number of hex characters to use to use in each crate::identity::Storage object digest, 61.
STORAGE_KEY_LENGTH
The number of hex characters to use to use in each crate::identity::Storage object key, 3. 4096 possible storage keys.