Skip to content

Commit 58a6366

Browse files
committed
Store private key in keychain
We store it so it can only be accessed by our developer ID.
1 parent 332074e commit 58a6366

File tree

4 files changed

+219
-43
lines changed

4 files changed

+219
-43
lines changed

README.md

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,24 @@
1-
# boats's personal barricade - pkgx updates
1+
# boats's personal barricade
22

33
This is a tool to automatically sign git commits, replacing gpg for that
44
purpose. It is very opinionated, and only useful if you use gpg the same way I
55
do.
66

7-
## Updates
7+
## `pkgx` Updates
88

9-
updated to edition 2021 by pkgx
9+
* Updated to edition 2021 by pkgx
10+
* Stores the private key in the macOS keychain such that only this tool (when
11+
codesigned) can access it.
1012

11-
## How to install
13+
## How to Install
1214

1315
```sh
1416
git clone https://github.com/pkgxdev/bpb-pkgx
1517
cd bpb-pkgx
1618
cargo install --path .
1719
```
1820

19-
## How to set up
21+
## How to Set Up
2022

2123
Once you've installed this program, you should run the `bpb init` subcommand.
2224
This command expects you to pass a userid argument. For example, this is how I
@@ -29,11 +31,13 @@ bpb init "withoutboats <boats@mozilla.com>"
2931
You can pass any string you want as your userid, but `"$NAME <$EMAIL>"` is the
3032
conventional standard for OpenPGP userids.
3133

32-
This will create a file at ~/.bpb_keys.toml. This file contains your bpb public
33-
and private keys.
34+
This will create a file at ~/.bpb_keys.toml. This file contains your public
35+
key.
3436

35-
It also prints your public key in OpenPGP format, so that you can upload it
36-
again. You can print your public key more times with:
37+
The private and public keys are output as JSON. This is the only time this
38+
tool will expose your private key publicly.
39+
40+
You can print your public key more times with:
3741

3842
```sh
3943
bpb print
@@ -43,27 +47,19 @@ If you want to use it to sign git commits, you also need to inform git to call
4347
it instead of gpg. You can do this with this command:
4448

4549
```sh
46-
git config --global gpg.program bpb
50+
git config --global gpg.program bpb_pkgx
4751
```
4852

4953
You should also provide the public key to people who want to verify your
50-
commits. Personally, I just upload the public key to GitHub; you may have other
51-
requirements.
54+
commits. Personally, I just upload the public key to GitHub; you may have
55+
other requirements.
5256

53-
## How it replaces gpg
57+
## How it Replaces GPG
5458

55-
If this program receives a `-s` argument, it reads from stdin and then writes a
56-
signature to stdout. If it receives any arguments it doesn't recognize, it
59+
If this program receives a `-s` argument, it reads from stdin and then writes
60+
a signature to stdout. If it receives any arguments it doesn't recognize, it
5761
delegates to the gpg binary in your path.
5862

5963
This means that this program can be used to replace gpg as a signing tool, but
60-
it does not replace any other functionality. For example, if you want to verify
61-
the signatures on other peoples' git commits, it will shell out to gpg.
62-
63-
## Storing your private key
64-
65-
By default, your private key is stored as a hex string in `~/.bpb_keys.toml`.
66-
However, if you are uncomfortable with the possibility of someone reading your
67-
private key from your home directory, you can instead store it somewhere else.
68-
To do this, replace the `key` field with a `program` field, and `bpb` will run
69-
this program, expecting it to print your key to stdout.
64+
it does not replace any other functionality. For example, if you want to
65+
verify the signatures on other peoples' git commits, it will shell out to gpg.

src/config.rs

Lines changed: 16 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ use failure::Error;
44

55
use crate::key_data::KeyData;
66

7+
use crate::keychain::get_keychain_item;
8+
use crate::keychain::add_keychain_item;
9+
710
#[derive(Serialize, Deserialize)]
811
pub struct Config {
912
public: PublicKey,
@@ -29,13 +32,17 @@ impl Config {
2932
}
3033

3134
pub fn load(file: &mut impl Read) -> Result<Config, Error> {
32-
let mut buf = vec![];
33-
file.read_to_end(&mut buf)?;
34-
Ok(toml::from_slice(&buf)?)
35+
let service: &str = "xyz.tea.BASE.bpb";
36+
let account: &str = "example_account";
37+
let str = get_keychain_item(service, account).unwrap();
38+
Ok(toml::from_str(&str)?)
3539
}
3640

3741
pub fn write(&self, file: &mut impl Write) -> Result<(), Error> {
38-
Ok(file.write_all(&toml::to_vec(self)?)?)
42+
let secret = toml::to_string(self)?;
43+
let service = "xyz.tea.BASE.bpb";
44+
let account = "example_account"; //self.user_id();
45+
add_keychain_item(service, account, &secret)
3946
}
4047

4148
pub fn timestamp(&self) -> u64 {
@@ -46,10 +53,6 @@ impl Config {
4653
&self.public.userid
4754
}
4855

49-
// pub fn public(&self) -> &str {
50-
// &self.public.key
51-
// }
52-
5356
pub fn secret(&self) -> Result<[u8; 32], Error> {
5457
self.secret.secret()
5558
}
@@ -70,16 +73,11 @@ struct SecretKey {
7073

7174
impl SecretKey {
7275
fn secret(&self) -> Result<[u8; 32], Error> {
73-
if let Some(key) = &self.key {
74-
to_32_bytes(key)
75-
} else if let Some(cmd) = &self.program {
76-
let mut args = cmd.split_whitespace();
77-
let cmd = args.next().ok_or(failure::err_msg("Missing command"))?;
78-
let output = std::process::Command::new(cmd).args(args).output().unwrap();
79-
to_32_bytes(&String::from_utf8(output.stdout)?)
80-
} else {
81-
bail!("No secret key or program specified")
82-
}
76+
if let Some(key) = &self.key {
77+
to_32_bytes(key)
78+
} else {
79+
bail!("No secret key or program specified")
80+
}
8381
}
8482
}
8583

src/keychain.rs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
use std::ffi::{CString, c_void};
2+
use failure::Error;
3+
use std::ptr;
4+
5+
#[link(name = "Security", kind = "framework")]
6+
extern "C" {
7+
fn SecItemAdd(attributes: *const c_void, result: *mut *const c_void) -> i32;
8+
fn SecItemCopyMatching(query: *const c_void, result: *mut *const c_void) -> i32;
9+
fn SecItemDelete(query: *const c_void) -> i32;
10+
11+
static kSecClass: *const c_void;
12+
static kSecClassGenericPassword: *const c_void;
13+
static kSecAttrService: *const c_void;
14+
static kSecAttrAccount: *const c_void;
15+
static kSecValueData: *const c_void;
16+
static kSecReturnData: *const c_void;
17+
static kCFBooleanTrue: *const c_void;
18+
static kSecAttrAccessGroup: *const c_void;
19+
}
20+
21+
const ERR_SUCCESS: i32 = 0;
22+
23+
pub fn add_keychain_item(service: &str, account: &str, secret: &str) -> Result<(), Error> {
24+
let service = CString::new(service).unwrap();
25+
let account = CString::new(account).unwrap();
26+
let secret_bytes = secret.as_bytes();
27+
28+
let cf_service = unsafe {
29+
CFStringCreateWithCString(ptr::null(), service.as_ptr(), 0x08000100) // UTF-8 encoding
30+
};
31+
let cf_account = unsafe {
32+
CFStringCreateWithCString(ptr::null(), account.as_ptr(), 0x08000100) // UTF-8 encoding
33+
};
34+
let cf_data = unsafe { CFDataCreate(ptr::null(), secret_bytes.as_ptr(), secret_bytes.len()) };
35+
let cf_access_group = unsafe {
36+
CFStringCreateWithCString(
37+
ptr::null(),
38+
"7WV56FL599.keychain_tool".as_ptr() as *const i8, // Fixed pointer type
39+
0x08000100,
40+
)
41+
};
42+
43+
assert!(!cf_service.is_null(), "Failed to create CFString for service");
44+
assert!(!cf_account.is_null(), "Failed to create CFString for account");
45+
assert!(!cf_data.is_null(), "Failed to create CFData for secret");
46+
assert!(!cf_access_group.is_null(), "Failed to create CFString for access group");
47+
48+
let attributes = vec![
49+
(unsafe { kSecClass }, unsafe { kSecClassGenericPassword }),
50+
(unsafe { kSecAttrService }, cf_service),
51+
(unsafe { kSecAttrAccount }, cf_account),
52+
(unsafe { kSecValueData }, cf_data),
53+
(unsafe { kSecAttrAccessGroup }, cf_access_group), // Added access group
54+
];
55+
56+
let attributes_ptr = attributes_to_dict(&attributes);
57+
assert!(!attributes_ptr.is_null(), "CFDictionary creation failed!");
58+
59+
unsafe {
60+
let mut result: *mut c_void = ptr::null_mut();
61+
let status = SecItemAdd(attributes_ptr, &mut result as *mut *mut c_void as *mut *const c_void);
62+
63+
CFRelease(attributes_ptr); // Release dictionary
64+
CFRelease(cf_service); // Release service string
65+
CFRelease(cf_account); // Release account string
66+
CFRelease(cf_data); // Release data
67+
CFRelease(cf_access_group); // Release access group string
68+
69+
if status == ERR_SUCCESS {
70+
Ok(())
71+
} else {
72+
Err(failure::err_msg(format!("SecItemAdd failed with status: {}", status)))
73+
}
74+
}
75+
}
76+
77+
pub fn get_keychain_item(service: &str, account: &str) -> Result<String, String> {
78+
let service = CString::new(service).unwrap();
79+
let account = CString::new(account).unwrap();
80+
81+
let cf_service = unsafe {
82+
CFStringCreateWithCString(ptr::null(), service.as_ptr() as *const i8, 0x08000100) // UTF-8 encoding
83+
};
84+
let cf_account = unsafe {
85+
CFStringCreateWithCString(ptr::null(), account.as_ptr() as *const i8, 0x08000100) // UTF-8 encoding
86+
};
87+
88+
assert!(!cf_service.is_null(), "Failed to create CFString for service");
89+
assert!(!cf_account.is_null(), "Failed to create CFString for account");
90+
91+
let query = vec![
92+
(unsafe { kSecClass }, unsafe { kSecClassGenericPassword }),
93+
(unsafe { kSecAttrService }, cf_service),
94+
(unsafe { kSecAttrAccount }, cf_account),
95+
(unsafe { kSecReturnData }, unsafe { kCFBooleanTrue }),
96+
];
97+
98+
let query_ptr = attributes_to_dict(&query);
99+
assert!(!query_ptr.is_null(), "CFDictionary creation failed for query!");
100+
101+
unsafe {
102+
let mut result: *mut c_void = ptr::null_mut();
103+
let status = SecItemCopyMatching(query_ptr, &mut result as *mut *mut c_void as *mut *const c_void);
104+
105+
CFRelease(query_ptr); // Release query dictionary
106+
CFRelease(cf_service); // Release service string
107+
CFRelease(cf_account); // Release account string
108+
109+
if status == ERR_SUCCESS {
110+
assert!(!result.is_null(), "SecItemCopyMatching returned a null result");
111+
112+
// Convert the result to a Rust String
113+
let data_ptr = CFDataGetBytePtr(result);
114+
let data_len = CFDataGetLength(result);
115+
let bytes = std::slice::from_raw_parts(data_ptr, data_len);
116+
let secret = String::from_utf8_lossy(bytes).to_string();
117+
118+
CFRelease(result); // Release result data
119+
120+
Ok(secret)
121+
} else {
122+
Err(format!("SecItemCopyMatching failed with status: {}", status))
123+
}
124+
}
125+
}
126+
127+
// Helpers for Keychain Constants and Conversions
128+
129+
#[link(name = "CoreFoundation", kind = "framework")]
130+
extern "C" {
131+
fn CFRelease(cf: *const c_void);
132+
fn CFDataGetLength(data: *const c_void) -> usize;
133+
fn CFDataGetBytePtr(data: *const c_void) -> *const u8;
134+
}
135+
136+
extern "C" {
137+
fn CFDictionaryCreate(
138+
allocator: *const c_void,
139+
keys: *const *const c_void,
140+
values: *const *const c_void,
141+
count: usize,
142+
key_callbacks: *const c_void,
143+
value_callbacks: *const c_void,
144+
) -> *const c_void;
145+
146+
fn CFStringCreateWithCString(
147+
allocator: *const c_void,
148+
cstr: *const i8,
149+
encoding: u32,
150+
) -> *const c_void;
151+
152+
fn CFDataCreate(
153+
allocator: *const c_void,
154+
bytes: *const u8,
155+
length: usize,
156+
) -> *const c_void;
157+
}
158+
159+
fn attributes_to_dict(attrs: &[(/* key: */ *const c_void, /* value: */ *const c_void)]) -> *const c_void {
160+
let keys: Vec<*const c_void> = attrs.iter().map(|(key, _)| *key).collect();
161+
let values: Vec<*const c_void> = attrs.iter().map(|(_, value)| *value).collect();
162+
163+
unsafe {
164+
CFDictionaryCreate(
165+
ptr::null(),
166+
keys.as_ptr(),
167+
values.as_ptr(),
168+
attrs.len(),
169+
ptr::null(),
170+
ptr::null(),
171+
)
172+
}
173+
}
174+
175+
fn CFDataToString(data: *const c_void) -> String {
176+
unsafe {
177+
let length = CFDataGetLength(data);
178+
let bytes = CFDataGetBytePtr(data);
179+
String::from_utf8(Vec::from_raw_parts(bytes as *mut u8, length, length)).unwrap()
180+
}
181+
}

src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ extern crate serde_derive;
55

66
mod config;
77
mod key_data;
8+
mod keychain;
89
mod tests;
910

1011
use std::time::SystemTime;

0 commit comments

Comments
 (0)