Skip to content

Commit

Permalink
Store a key locally
Browse files Browse the repository at this point in the history
  • Loading branch information
lpil committed Nov 27, 2024
1 parent 4b81460 commit 463bdab
Show file tree
Hide file tree
Showing 9 changed files with 807 additions and 56 deletions.
602 changes: 601 additions & 1 deletion Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions compiler-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ same-file = "1"
opener = "0"
# Pubgrub dependency resolution algorithm
pubgrub = "0"

camino = { workspace = true, features = ["serde1"] }
async-trait.workspace = true
base16.workspace = true
Expand Down
199 changes: 173 additions & 26 deletions compiler-cli/src/hex.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
use crate::{cli, http::HttpClient};
use camino::Utf8PathBuf;
use gleam_core::{
encryption,
hex::{self, RetirementReason},
io::HttpClient as _,
paths::global_hexpm_credentials_path,
Error, Result,
};

use crate::{cli, http::HttpClient};
use std::time::SystemTime;

const USER_PROMPT: &str = "https://hex.pm username";
const USER_KEY: &str = "HEXPM_USER";
const USER_ENV_NAME: &str = "HEXPM_USER";
const PASS_PROMPT: &str = "https://hex.pm password";
const PASS_KEY: &str = "HEXPM_PASS";
const API_KEY: &str = "HEXPM_API_KEY";
const LOCAL_PASS_PROMPT: &str = "Local password";
const PASS_ENV_NAME: &str = "HEXPM_PASS";
const API_ENV_NAME: &str = "HEXPM_API_KEY";

/// A helper trait that handles the provisioning and destruction of a Hex API key.
pub trait ApiKeyCommand {
Expand All @@ -26,12 +30,12 @@ pub trait ApiKeyCommand {
runtime: &tokio::runtime::Runtime,
hex_config: &hexpm::Config,
) -> Result<()> {
let hostname = crate::publish::get_hostname();
let hostname = generate_api_key_name();
let http = HttpClient::new();

// Get login creds from user
let username = std::env::var(USER_KEY).or_else(|_| cli::ask(USER_PROMPT))?;
let password = std::env::var(PASS_KEY).or_else(|_| cli::ask_password(PASS_PROMPT))?;
let username = std::env::var(USER_ENV_NAME).or_else(|_| cli::ask(USER_PROMPT))?;
let password = std::env::var(PASS_ENV_NAME).or_else(|_| cli::ask_password(PASS_PROMPT))?;

// Get API key
let api_key = runtime.block_on(hex::create_api_key(
Expand All @@ -52,7 +56,10 @@ pub trait ApiKeyCommand {
let runtime = tokio::runtime::Runtime::new().expect("Unable to start Tokio async runtime");
let hex_config = hexpm::Config::new();

let api_key = std::env::var(API_KEY).unwrap_or_default().trim().to_owned();
let api_key = std::env::var(API_ENV_NAME)
.unwrap_or_default()
.trim()
.to_owned();

if api_key.is_empty() {
self.with_new_api_key(&runtime, &hex_config)
Expand Down Expand Up @@ -203,25 +210,165 @@ impl ApiKeyCommand for RevertCommand {
}
}

pub(crate) fn create_api_key(name: String) -> std::result::Result<(), Error> {
let runtime = tokio::runtime::Runtime::new().expect("Unable to start Tokio async runtime");
let hex_config = hexpm::Config::new();
let http = HttpClient::new();
pub(crate) fn authenticate() -> Result<()> {
let mut auth = HexAuthentication::new();

// Get login creds from user
let username = std::env::var(USER_KEY).or_else(|_| cli::ask(USER_PROMPT))?;
let password = std::env::var(PASS_KEY).or_else(|_| cli::ask_password(PASS_PROMPT))?;
if auth.has_stored_api_key() {
let question = "You already have a local Hex API token. Would you
like to replace it with a new one?";
if !cli::confirm(question)? {
return Ok(());
}
}

// Get API key
let api_key = runtime.block_on(hex::create_api_key(
&name,
&username,
&password,
&hex_config,
&http,
))?;
_ = auth.create_and_store_api_key()?;
Ok(())
}

println!("{api_key}");
#[derive(Debug)]
pub struct EncryptedApiKey {

Check failure on line 229 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-unknown-linux-gnu)

struct `EncryptedApiKey` is never constructed

Check failure on line 229 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-unknown-linux-musl)

struct `EncryptedApiKey` is never constructed

Check failure on line 229 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, aarch64-unknown-linux-gnu)

struct `EncryptedApiKey` is never constructed

Check failure on line 229 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, aarch64-unknown-linux-musl)

struct `EncryptedApiKey` is never constructed

Check failure on line 229 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-apple-darwin)

struct `EncryptedApiKey` is never constructed

Check failure on line 229 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-pc-windows-msvc)

struct `EncryptedApiKey` is never constructed

Check failure on line 229 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, aarch64-apple-darwin, macos-latest, false, true)

struct `EncryptedApiKey` is never constructed
name: String,
encrypted: String,
}

Ok(())
#[derive(Debug)]
pub struct UnencryptedApiKey {
name: String,

Check failure on line 236 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-unknown-linux-gnu)

fields `name` and `unencrypted` are never read

Check failure on line 236 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-unknown-linux-musl)

fields `name` and `unencrypted` are never read

Check failure on line 236 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, aarch64-unknown-linux-gnu)

fields `name` and `unencrypted` are never read

Check failure on line 236 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, aarch64-unknown-linux-musl)

fields `name` and `unencrypted` are never read

Check failure on line 236 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-apple-darwin)

fields `name` and `unencrypted` are never read

Check failure on line 236 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-pc-windows-msvc)

fields `name` and `unencrypted` are never read

Check failure on line 236 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, aarch64-apple-darwin, macos-latest, false, true)

fields `name` and `unencrypted` are never read
unencrypted: String,
}

pub struct HexAuthentication {
runtime: tokio::runtime::Runtime,
http: HttpClient,
stored_api_key_path: Utf8PathBuf,
}

impl HexAuthentication {
/// Reads the stored API key from disc, if it exists.
///
pub fn new() -> Self {
Self {
runtime: tokio::runtime::Runtime::new().expect("Unable to start Tokio async runtime"),
http: HttpClient::new(),
stored_api_key_path: global_hexpm_credentials_path(),
}
}

/// Create a new API key, removing the previous one if it already exists.
///
pub fn create_and_store_api_key(&mut self) -> Result<UnencryptedApiKey> {
if self.stored_api_key_path.exists() {
self.remove_stored_api_key()?;
}

let name = generate_api_key_name();
let hex_config = hexpm::Config::new();

// Get login creds from user
let username = ask_username()?;
let password = ask_password()?;

// Get API key
let future = hex::create_api_key(&name, &username, &password, &hex_config, &self.http);
let api_key = self.runtime.block_on(future)?;

println!(
"
Please enter a new unique password. This will be used to locally
encrypt your Hex API key.
"
);
let password = ask_local_password()?;
let encrypted = encryption::encrypt_with_passphrase(api_key.as_bytes(), &password)?;

crate::fs::write(&self.stored_api_key_path, &format!("{name}\n{encrypted}"))?;
println!(
"Encrypted Hex API key written to {path}",
path = self.stored_api_key_path
);

Ok(UnencryptedApiKey {
name,
unencrypted: api_key,
})
}

pub fn has_stored_api_key(&self) -> bool {
self.stored_api_key_path.exists()
}

/// Get an API key from
/// 1. the HEXPM_API_KEY env var
/// 2. the file system (encrypted)
/// 3. the Hex API
pub fn get_or_create_api_key(&mut self) -> Result<String> {

Check failure on line 304 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-unknown-linux-gnu)

associated items `get_or_create_api_key`, `load_env_api_key`, `load_stored_api_key`, and `read_stored_api_key` are never used

Check failure on line 304 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-unknown-linux-musl)

associated items `get_or_create_api_key`, `load_env_api_key`, `load_stored_api_key`, and `read_stored_api_key` are never used

Check failure on line 304 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, aarch64-unknown-linux-gnu)

associated items `get_or_create_api_key`, `load_env_api_key`, `load_stored_api_key`, and `read_stored_api_key` are never used

Check failure on line 304 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, aarch64-unknown-linux-musl)

associated items `get_or_create_api_key`, `load_env_api_key`, `load_stored_api_key`, and `read_stored_api_key` are never used

Check failure on line 304 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-apple-darwin)

associated items `get_or_create_api_key`, `load_env_api_key`, `load_stored_api_key`, and `read_stored_api_key` are never used

Check failure on line 304 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, x86_64-pc-windows-msvc)

associated items `get_or_create_api_key`, `load_env_api_key`, `load_stored_api_key`, and `read_stored_api_key` are never used

Check failure on line 304 in compiler-cli/src/hex.rs

View workflow job for this annotation

GitHub Actions / test (stable, aarch64-apple-darwin, macos-latest, false, true)

associated items `get_or_create_api_key`, `load_env_api_key`, `load_stored_api_key`, and `read_stored_api_key` are never used
if let Some(key) = Self::load_env_api_key()? {
return Ok(key);
}

if let Some(key) = Self::load_stored_api_key()? {
return Ok(key);
}

Ok(self.create_and_store_api_key()?.unencrypted)
}

fn load_env_api_key() -> Result<Option<String>> {
let api_key = std::env::var(API_ENV_NAME).unwrap_or_default();
if api_key.trim().is_empty() {
return Ok(None);
} else {
Ok(Some(api_key))
}
}

fn load_stored_api_key() -> Result<Option<String>> {
let Some(EncryptedApiKey { encrypted, .. }) = Self::read_stored_api_key()? else {
return Ok(None);
};
let password = ask_local_password()?;
let key = encryption::decrypt_with_passphrase(encrypted.as_bytes(), &password)?;
Ok(Some(key))
}

fn read_stored_api_key() -> Result<Option<EncryptedApiKey>> {
let path = global_hexpm_credentials_path();
if !path.exists() {
return Ok(None);
}
let text = crate::fs::read(&path)?;
let mut chunks = text.splitn(2, '\n');
let name = chunks.next().unwrap().to_string();
let encrypted = chunks.next().unwrap().to_string();
Ok(Some(EncryptedApiKey { name, encrypted }))
}

fn remove_stored_api_key(&mut self) -> Result<()> {
todo!()
}
}

fn ask_local_password() -> std::result::Result<String, Error> {
std::env::var(PASS_ENV_NAME).or_else(|_| cli::ask_password(LOCAL_PASS_PROMPT))
}

fn ask_password() -> std::result::Result<String, Error> {
std::env::var(PASS_ENV_NAME).or_else(|_| cli::ask_password(PASS_PROMPT))
}

fn ask_username() -> std::result::Result<String, Error> {
std::env::var(USER_ENV_NAME).or_else(|_| cli::ask(USER_PROMPT))
}

// TODO: move to authenticator
pub fn generate_api_key_name() -> String {
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.expect("This function must only be called after January 1, 1970. Sorry time traveller!")
.as_secs();
let name = hostname::get()
.expect("Looking up hostname")
.to_string_lossy()
.to_string();
format!("{name}-{timestamp}")
}
21 changes: 3 additions & 18 deletions compiler-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,23 +406,8 @@ enum Hex {
version: Option<String>,
},

/// Create a Hex API key
CreateKey {
/// The name of the API key
name: String,
// TODO: support configurable permissions.
// /// The permissions the key will have.
// ///
// /// - api:read API read access.
// /// - api:write API write access.
// /// - repository:ORG Access to repositories for an organisation.
// /// - repositories Access to repositories for all your organisations.
// ///
// /// This flag can be given multiple times.
// ///
// #[arg(verbatim_doc_comment, long = "permission")]
// permissions: Vec<String>,
},
/// Authenticate with Hex
Authenticate,
}

#[derive(Subcommand, Debug)]
Expand Down Expand Up @@ -502,7 +487,7 @@ fn main() {

Command::Deps(Dependencies::Update(options)) => dependencies::update(options.packages),

Command::Hex(Hex::CreateKey { name }) => hex::create_api_key(name),
Command::Hex(Hex::Authenticate) => hex::authenticate(),

Command::New(options) => new::create(options, COMPILER_VERSION),

Expand Down
7 changes: 0 additions & 7 deletions compiler-cli/src/publish.rs
Original file line number Diff line number Diff line change
Expand Up @@ -735,13 +735,6 @@ fn prevent_publish_git_dependency() {
);
}

pub fn get_hostname() -> String {
hostname::get()
.expect("Looking up hostname")
.to_string_lossy()
.to_string()
}

fn quotes(x: &str) -> String {
format!(r#"<<"{x}">>"#)
}
3 changes: 3 additions & 0 deletions compiler-core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ bimap = "0.6.3"
# Parsing of arbitrary width int values
num-bigint = "0.4.6"
num-traits = "0.2.19"
# Encryption
age = { version = "0.11", features = ["armor"] }

async-trait.workspace = true
base16.workspace = true
bytes.workspace = true
Expand Down
16 changes: 16 additions & 0 deletions compiler-core/src/encryption.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use crate::Result;

pub fn encrypt_with_passphrase(message: &[u8], passphrase: &str) -> Result<String> {
let passphrase = age::secrecy::SecretString::from(passphrase);
let recipient = age::scrypt::Recipient::new(passphrase.clone());
let encrypted = age::encrypt_and_armor(&recipient, message).unwrap();
Ok(encrypted)
}

pub fn decrypt_with_passphrase(encrypted_message: &[u8], passphrase: &str) -> Result<String> {
let passphrase = age::secrecy::SecretString::from(passphrase);
let identity = age::scrypt::Identity::new(passphrase);
let decrypted = age::decrypt(&identity, encrypted_message).unwrap();
let decrypted = String::from_utf8(decrypted).unwrap();
Ok(decrypted)
}
1 change: 1 addition & 0 deletions compiler-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub mod config;
pub mod dependency;
pub mod diagnostic;
pub mod docs;
pub mod encryption;
pub mod erlang;
pub mod error;
pub mod fix;
Expand Down
13 changes: 9 additions & 4 deletions compiler-core/src/paths.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,11 +126,16 @@ pub fn global_package_cache_package_tarball(package_name: &str, version: &str) -
global_packages_cache().join(format!("{package_name}-{version}.tar"))
}

pub fn global_hexpm_credentials_path() -> Utf8PathBuf {
global_hexpm_cache().join("credentials")
}

fn global_hexpm_cache() -> Utf8PathBuf {
default_global_gleam_cache().join("hex").join("hexpm")
}

fn global_packages_cache() -> Utf8PathBuf {
default_global_gleam_cache()
.join("hex")
.join("hexpm")
.join("packages")
global_hexpm_cache().join("packages")
}

pub fn default_global_gleam_cache() -> Utf8PathBuf {
Expand Down

0 comments on commit 463bdab

Please sign in to comment.