Skip to main content

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.