@parlor/shell-desktop 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +10 -2
- package/assets/icons/128x128.png +0 -0
- package/assets/icons/128x128@2x.png +0 -0
- package/assets/icons/32x32.png +0 -0
- package/assets/icons/icon.icns +0 -0
- package/assets/icons/icon.ico +0 -0
- package/dist/build.d.ts +3 -0
- package/dist/build.d.ts.map +1 -0
- package/dist/build.js +18 -0
- package/dist/build.js.map +1 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.d.ts.map +1 -0
- package/dist/dev.js +28 -0
- package/dist/dev.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/prerequisites.d.ts +4 -0
- package/dist/prerequisites.d.ts.map +1 -0
- package/dist/prerequisites.js +12 -0
- package/dist/prerequisites.js.map +1 -0
- package/dist/project.d.ts +48 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +243 -0
- package/dist/project.js.map +1 -0
- package/dist/target-triple.d.ts +3 -0
- package/dist/target-triple.d.ts.map +1 -0
- package/dist/target-triple.js +10 -0
- package/dist/target-triple.js.map +1 -0
- package/dist/tauri-cli.d.ts +9 -0
- package/dist/tauri-cli.d.ts.map +1 -0
- package/dist/tauri-cli.js +42 -0
- package/dist/tauri-cli.js.map +1 -0
- package/package.json +45 -3
- package/rust/Cargo.toml +26 -0
- package/rust/src/control.rs +104 -0
- package/rust/src/device_key.rs +282 -0
- package/rust/src/dialog.rs +22 -0
- package/rust/src/host.rs +338 -0
- package/rust/src/keystore.rs +442 -0
- package/rust/src/lib.rs +20 -0
- package/rust/src/secret_store.rs +42 -0
- package/rust/src/session.rs +100 -0
- package/rust/src/vault.rs +133 -0
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
pub struct PublicPoint {
|
|
2
|
+
pub x: Vec<u8>,
|
|
3
|
+
pub y: Vec<u8>,
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
#[cfg(target_os = "macos")]
|
|
7
|
+
pub use apple::{find, presence_key, signing_key};
|
|
8
|
+
|
|
9
|
+
#[cfg(not(target_os = "macos"))]
|
|
10
|
+
pub use unsupported::{find, presence_key, signing_key};
|
|
11
|
+
|
|
12
|
+
#[cfg(target_os = "macos")]
|
|
13
|
+
mod apple {
|
|
14
|
+
use std::ffi::c_void;
|
|
15
|
+
use std::sync::Mutex;
|
|
16
|
+
use std::sync::mpsc;
|
|
17
|
+
|
|
18
|
+
use block2::RcBlock;
|
|
19
|
+
use core_foundation::base::ToVoid;
|
|
20
|
+
use core_foundation::dictionary::{CFDictionary, CFMutableDictionary};
|
|
21
|
+
use objc2::runtime::Bool;
|
|
22
|
+
use objc2_foundation::{NSError, NSString};
|
|
23
|
+
use objc2_local_authentication::{LAContext, LAPolicy};
|
|
24
|
+
use security_framework::key::{Algorithm, SecKey};
|
|
25
|
+
use security_framework_sys::item::kSecAttrTokenID;
|
|
26
|
+
|
|
27
|
+
use super::PublicPoint;
|
|
28
|
+
use crate::device_key::{der_to_raw, report_of};
|
|
29
|
+
|
|
30
|
+
use crate::secret_store::{Backend, backend};
|
|
31
|
+
|
|
32
|
+
const SIGNING: &str = "signing";
|
|
33
|
+
const PRESENCE: &str = "presence";
|
|
34
|
+
const PRESENCE_REASON: &str = "Confirm it is you to answer your agent";
|
|
35
|
+
|
|
36
|
+
static CREATING: Mutex<()> = Mutex::new(());
|
|
37
|
+
|
|
38
|
+
pub struct Key {
|
|
39
|
+
key: SecKey,
|
|
40
|
+
presence: bool,
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn dictionary(pairs: Vec<(*const c_void, *const c_void)>) -> CFDictionary {
|
|
44
|
+
let mut dictionary = CFMutableDictionary::new();
|
|
45
|
+
for (key, value) in pairs {
|
|
46
|
+
dictionary.set(key, value);
|
|
47
|
+
}
|
|
48
|
+
dictionary.to_immutable()
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
mod keychain {
|
|
52
|
+
use core_foundation::base::{TCFType, ToVoid};
|
|
53
|
+
use core_foundation::boolean::CFBoolean;
|
|
54
|
+
use core_foundation::data::CFData;
|
|
55
|
+
use core_foundation::number::CFNumber;
|
|
56
|
+
use core_foundation::string::CFString;
|
|
57
|
+
use core_foundation_sys::base::CFTypeRef;
|
|
58
|
+
use core_foundation_sys::string::CFStringRef;
|
|
59
|
+
use security_framework::access_control::{ProtectionMode, SecAccessControl};
|
|
60
|
+
use security_framework::key::SecKey;
|
|
61
|
+
use security_framework_sys::access_control::{
|
|
62
|
+
kSecAccessControlPrivateKeyUsage, kSecAccessControlUserPresence,
|
|
63
|
+
};
|
|
64
|
+
use security_framework_sys::base::{SecKeyRef, errSecItemNotFound};
|
|
65
|
+
use security_framework_sys::item::{
|
|
66
|
+
kSecAttrAccessControl, kSecAttrIsPermanent, kSecAttrKeyClass, kSecAttrKeyClassPrivate,
|
|
67
|
+
kSecAttrKeySizeInBits, kSecAttrKeyType, kSecAttrKeyTypeECSECPrimeRandom, kSecAttrLabel,
|
|
68
|
+
kSecAttrTokenID, kSecAttrTokenIDSecureEnclave, kSecClass, kSecClassKey,
|
|
69
|
+
kSecPrivateKeyAttrs, kSecReturnRef,
|
|
70
|
+
};
|
|
71
|
+
use security_framework_sys::keychain_item::SecItemCopyMatching;
|
|
72
|
+
|
|
73
|
+
use super::{can_evaluate, dictionary};
|
|
74
|
+
|
|
75
|
+
#[link(name = "Security", kind = "framework")]
|
|
76
|
+
unsafe extern "C" {
|
|
77
|
+
static kSecAttrApplicationTag: CFStringRef;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
fn tag(identifier: &str, name: &str) -> Vec<u8> {
|
|
81
|
+
format!("{identifier}.device-key.{name}").into_bytes()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn lookup(tag: &[u8]) -> Result<Option<SecKey>, String> {
|
|
85
|
+
let tag = CFData::from_buffer(tag);
|
|
86
|
+
let wanted = CFBoolean::true_value();
|
|
87
|
+
let query = unsafe {
|
|
88
|
+
dictionary(vec![
|
|
89
|
+
(kSecClass.to_void(), kSecClassKey.to_void()),
|
|
90
|
+
(kSecAttrApplicationTag.to_void(), tag.to_void()),
|
|
91
|
+
(
|
|
92
|
+
kSecAttrKeyClass.to_void(),
|
|
93
|
+
kSecAttrKeyClassPrivate.to_void(),
|
|
94
|
+
),
|
|
95
|
+
(kSecReturnRef.to_void(), wanted.to_void()),
|
|
96
|
+
])
|
|
97
|
+
};
|
|
98
|
+
let mut found: CFTypeRef = std::ptr::null();
|
|
99
|
+
let status = unsafe { SecItemCopyMatching(query.as_concrete_TypeRef(), &mut found) };
|
|
100
|
+
if status == errSecItemNotFound {
|
|
101
|
+
return Ok(None);
|
|
102
|
+
}
|
|
103
|
+
if status != 0 || found.is_null() {
|
|
104
|
+
return Err(format!("Keychain lookup failed ({status})"));
|
|
105
|
+
}
|
|
106
|
+
Ok(Some(unsafe {
|
|
107
|
+
SecKey::wrap_under_create_rule(found as SecKeyRef)
|
|
108
|
+
}))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
#[allow(deprecated)]
|
|
112
|
+
fn generate(
|
|
113
|
+
tag: &[u8],
|
|
114
|
+
enclave: bool,
|
|
115
|
+
presence: bool,
|
|
116
|
+
label: &str,
|
|
117
|
+
) -> Result<SecKey, String> {
|
|
118
|
+
let tag = CFData::from_buffer(tag);
|
|
119
|
+
let permanent = CFBoolean::true_value();
|
|
120
|
+
let size = CFNumber::from(256);
|
|
121
|
+
let label = CFString::new(label);
|
|
122
|
+
let access = if enclave {
|
|
123
|
+
let flags = kSecAccessControlPrivateKeyUsage
|
|
124
|
+
| if presence {
|
|
125
|
+
kSecAccessControlUserPresence
|
|
126
|
+
} else {
|
|
127
|
+
0
|
|
128
|
+
};
|
|
129
|
+
Some(
|
|
130
|
+
SecAccessControl::create_with_protection(
|
|
131
|
+
Some(ProtectionMode::AccessibleWhenUnlockedThisDeviceOnly),
|
|
132
|
+
flags,
|
|
133
|
+
)
|
|
134
|
+
.map_err(|e| e.to_string())?,
|
|
135
|
+
)
|
|
136
|
+
} else {
|
|
137
|
+
None
|
|
138
|
+
};
|
|
139
|
+
let mut private = vec![
|
|
140
|
+
(
|
|
141
|
+
unsafe { kSecAttrIsPermanent }.to_void(),
|
|
142
|
+
permanent.to_void(),
|
|
143
|
+
),
|
|
144
|
+
(unsafe { kSecAttrApplicationTag }.to_void(), tag.to_void()),
|
|
145
|
+
];
|
|
146
|
+
if let Some(access) = &access {
|
|
147
|
+
private.push((unsafe { kSecAttrAccessControl }.to_void(), access.to_void()));
|
|
148
|
+
}
|
|
149
|
+
let private = dictionary(private);
|
|
150
|
+
let mut attributes = vec![
|
|
151
|
+
(
|
|
152
|
+
unsafe { kSecAttrKeyType }.to_void(),
|
|
153
|
+
unsafe { kSecAttrKeyTypeECSECPrimeRandom }.to_void(),
|
|
154
|
+
),
|
|
155
|
+
(unsafe { kSecAttrKeySizeInBits }.to_void(), size.to_void()),
|
|
156
|
+
(unsafe { kSecAttrLabel }.to_void(), label.to_void()),
|
|
157
|
+
(unsafe { kSecPrivateKeyAttrs }.to_void(), private.to_void()),
|
|
158
|
+
];
|
|
159
|
+
if enclave {
|
|
160
|
+
attributes.push((
|
|
161
|
+
unsafe { kSecAttrTokenID }.to_void(),
|
|
162
|
+
unsafe { kSecAttrTokenIDSecureEnclave }.to_void(),
|
|
163
|
+
));
|
|
164
|
+
}
|
|
165
|
+
// `GenerateKeyOptions` cannot carry the application tag the key is found by
|
|
166
|
+
SecKey::generate(dictionary(attributes)).map_err(|e| e.to_string())
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
pub fn stored(identifier: &str, name: &str) -> Result<Option<SecKey>, String> {
|
|
170
|
+
lookup(&tag(identifier, name))
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
pub fn create(
|
|
174
|
+
identifier: &str,
|
|
175
|
+
name: &str,
|
|
176
|
+
presence: bool,
|
|
177
|
+
) -> Result<Option<SecKey>, String> {
|
|
178
|
+
let tag = tag(identifier, name);
|
|
179
|
+
let label = format!("{identifier} device {name} key");
|
|
180
|
+
match generate(&tag, true, presence, &label) {
|
|
181
|
+
Ok(key) => Ok(Some(key)),
|
|
182
|
+
Err(_) if presence && !can_evaluate() => Ok(None),
|
|
183
|
+
Err(_) => generate(&tag, false, presence, &label).map(Some),
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
#[cfg(test)]
|
|
188
|
+
mod tests {
|
|
189
|
+
use core_foundation::base::{TCFType, ToVoid};
|
|
190
|
+
use core_foundation::data::CFData;
|
|
191
|
+
use security_framework_sys::item::{kSecClass, kSecClassKey};
|
|
192
|
+
use security_framework_sys::keychain_item::SecItemDelete;
|
|
193
|
+
|
|
194
|
+
use super::super::dictionary;
|
|
195
|
+
use super::{create, kSecAttrApplicationTag, stored, tag};
|
|
196
|
+
|
|
197
|
+
#[test]
|
|
198
|
+
fn the_keychain_finds_the_key_it_created_by_its_tag() {
|
|
199
|
+
let identifier = format!("dev.parlor.test.keystore.{}", std::process::id());
|
|
200
|
+
assert!(stored(&identifier, "signing").unwrap().is_none());
|
|
201
|
+
let created = create(&identifier, "signing", false).unwrap().unwrap();
|
|
202
|
+
let found = stored(&identifier, "signing").unwrap().unwrap();
|
|
203
|
+
let public = |key: &security_framework::key::SecKey| {
|
|
204
|
+
key.public_key()
|
|
205
|
+
.and_then(|public| public.external_representation())
|
|
206
|
+
.map(|data| data.bytes().to_vec())
|
|
207
|
+
};
|
|
208
|
+
assert_eq!(public(&found), public(&created));
|
|
209
|
+
let tag = CFData::from_buffer(&tag(&identifier, "signing"));
|
|
210
|
+
let query = unsafe {
|
|
211
|
+
dictionary(vec![
|
|
212
|
+
(kSecClass.to_void(), kSecClassKey.to_void()),
|
|
213
|
+
(kSecAttrApplicationTag.to_void(), tag.to_void()),
|
|
214
|
+
])
|
|
215
|
+
};
|
|
216
|
+
assert_eq!(unsafe { SecItemDelete(query.as_concrete_TypeRef()) }, 0);
|
|
217
|
+
assert!(stored(&identifier, "signing").unwrap().is_none());
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
mod vault {
|
|
223
|
+
use base64::Engine;
|
|
224
|
+
use base64::engine::general_purpose::STANDARD;
|
|
225
|
+
use core_foundation::base::{TCFType, ToVoid};
|
|
226
|
+
use core_foundation::data::CFData;
|
|
227
|
+
use core_foundation::error::CFError;
|
|
228
|
+
use core_foundation::number::CFNumber;
|
|
229
|
+
use security_framework::key::SecKey;
|
|
230
|
+
use security_framework_sys::item::{
|
|
231
|
+
kSecAttrKeyClass, kSecAttrKeyClassPrivate, kSecAttrKeySizeInBits, kSecAttrKeyType,
|
|
232
|
+
kSecAttrKeyTypeECSECPrimeRandom,
|
|
233
|
+
};
|
|
234
|
+
use security_framework_sys::key::SecKeyCreateWithData;
|
|
235
|
+
|
|
236
|
+
use super::{can_evaluate, dictionary};
|
|
237
|
+
use crate::vault as dev_vault;
|
|
238
|
+
|
|
239
|
+
fn entry(name: &str) -> String {
|
|
240
|
+
format!("device-key.{name}")
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
pub fn stored(identifier: &str, name: &str) -> Result<Option<SecKey>, String> {
|
|
244
|
+
let Some(encoded) = dev_vault::get(identifier, &entry(name))? else {
|
|
245
|
+
return Ok(None);
|
|
246
|
+
};
|
|
247
|
+
let bytes = STANDARD.decode(encoded).map_err(|e| e.to_string())?;
|
|
248
|
+
let data = CFData::from_buffer(&bytes);
|
|
249
|
+
let attributes = unsafe {
|
|
250
|
+
dictionary(vec![
|
|
251
|
+
(
|
|
252
|
+
kSecAttrKeyType.to_void(),
|
|
253
|
+
kSecAttrKeyTypeECSECPrimeRandom.to_void(),
|
|
254
|
+
),
|
|
255
|
+
(
|
|
256
|
+
kSecAttrKeyClass.to_void(),
|
|
257
|
+
kSecAttrKeyClassPrivate.to_void(),
|
|
258
|
+
),
|
|
259
|
+
])
|
|
260
|
+
};
|
|
261
|
+
let mut error = std::ptr::null_mut();
|
|
262
|
+
let key = unsafe {
|
|
263
|
+
SecKeyCreateWithData(
|
|
264
|
+
data.as_concrete_TypeRef(),
|
|
265
|
+
attributes.as_concrete_TypeRef(),
|
|
266
|
+
&mut error,
|
|
267
|
+
)
|
|
268
|
+
};
|
|
269
|
+
if key.is_null() {
|
|
270
|
+
return Err(unsafe { CFError::wrap_under_create_rule(error) }.to_string());
|
|
271
|
+
}
|
|
272
|
+
Ok(Some(unsafe { SecKey::wrap_under_create_rule(key) }))
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
pub fn create(
|
|
276
|
+
identifier: &str,
|
|
277
|
+
name: &str,
|
|
278
|
+
presence: bool,
|
|
279
|
+
) -> Result<Option<SecKey>, String> {
|
|
280
|
+
if presence && !can_evaluate() {
|
|
281
|
+
return Ok(None);
|
|
282
|
+
}
|
|
283
|
+
let size = CFNumber::from(256);
|
|
284
|
+
let attributes = dictionary(vec![
|
|
285
|
+
(
|
|
286
|
+
unsafe { kSecAttrKeyType }.to_void(),
|
|
287
|
+
unsafe { kSecAttrKeyTypeECSECPrimeRandom }.to_void(),
|
|
288
|
+
),
|
|
289
|
+
(unsafe { kSecAttrKeySizeInBits }.to_void(), size.to_void()),
|
|
290
|
+
]);
|
|
291
|
+
#[allow(deprecated)]
|
|
292
|
+
let key = SecKey::generate(attributes).map_err(|e| e.to_string())?;
|
|
293
|
+
let private = key
|
|
294
|
+
.external_representation()
|
|
295
|
+
.ok_or("The new key cannot be exported.")?;
|
|
296
|
+
dev_vault::set(
|
|
297
|
+
identifier,
|
|
298
|
+
&entry(name),
|
|
299
|
+
Some(&STANDARD.encode(private.bytes())),
|
|
300
|
+
)?;
|
|
301
|
+
Ok(Some(key))
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
fn can_evaluate() -> bool {
|
|
306
|
+
let context = unsafe { LAContext::new() };
|
|
307
|
+
unsafe { context.canEvaluatePolicy_error(LAPolicy::DeviceOwnerAuthentication) }.is_ok()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
fn evaluate() -> Result<(), String> {
|
|
311
|
+
let context = unsafe { LAContext::new() };
|
|
312
|
+
let (sender, receiver) = mpsc::channel();
|
|
313
|
+
let reply = RcBlock::new(move |success: Bool, _error: *mut NSError| {
|
|
314
|
+
let _ = sender.send(success.as_bool());
|
|
315
|
+
});
|
|
316
|
+
unsafe {
|
|
317
|
+
context.evaluatePolicy_localizedReason_reply(
|
|
318
|
+
LAPolicy::DeviceOwnerAuthentication,
|
|
319
|
+
&NSString::from_str(PRESENCE_REASON),
|
|
320
|
+
&reply,
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
match receiver.recv() {
|
|
324
|
+
Ok(true) => Ok(()),
|
|
325
|
+
_ => Err("The presence check did not complete.".to_string()),
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
fn is_enclave(key: &SecKey) -> bool {
|
|
330
|
+
key.attributes()
|
|
331
|
+
.find(unsafe { kSecAttrTokenID }.to_void())
|
|
332
|
+
.is_some()
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
fn stored(identifier: &str, name: &str) -> Result<Option<SecKey>, String> {
|
|
336
|
+
match backend() {
|
|
337
|
+
Backend::Keychain => keychain::stored(identifier, name),
|
|
338
|
+
Backend::Vault => vault::stored(identifier, name),
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
fn create(identifier: &str, name: &str, presence: bool) -> Result<Option<SecKey>, String> {
|
|
343
|
+
match backend() {
|
|
344
|
+
Backend::Keychain => keychain::create(identifier, name, presence),
|
|
345
|
+
Backend::Vault => vault::create(identifier, name, presence),
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fn open(identifier: &str, name: &str) -> Result<Option<Key>, String> {
|
|
350
|
+
let presence = name == PRESENCE;
|
|
351
|
+
Ok(stored(identifier, name)?.map(|key| Key { key, presence }))
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
fn open_or_create(identifier: &str, name: &str) -> Result<Option<Key>, String> {
|
|
355
|
+
if let Some(key) = open(identifier, name)? {
|
|
356
|
+
return Ok(Some(key));
|
|
357
|
+
}
|
|
358
|
+
let _creating = CREATING.lock().map_err(|e| e.to_string())?;
|
|
359
|
+
if let Some(key) = open(identifier, name)? {
|
|
360
|
+
return Ok(Some(key));
|
|
361
|
+
}
|
|
362
|
+
let presence = name == PRESENCE;
|
|
363
|
+
Ok(create(identifier, name, presence)?.map(|key| Key { key, presence }))
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
pub fn signing_key(identifier: &str) -> Result<Key, String> {
|
|
367
|
+
open_or_create(identifier, SIGNING)?.ok_or_else(|| "No signing key.".to_string())
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
pub fn presence_key(identifier: &str) -> Result<Option<Key>, String> {
|
|
371
|
+
open_or_create(identifier, PRESENCE)
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
pub fn find(identifier: &str, key_id: &str) -> Result<Key, String> {
|
|
375
|
+
for name in [SIGNING, PRESENCE] {
|
|
376
|
+
if let Some(key) = open(identifier, name)?
|
|
377
|
+
&& report_of(&key.point()?).key_id == key_id
|
|
378
|
+
{
|
|
379
|
+
return Ok(key);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
Err("This device has no such key.".to_string())
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
impl Key {
|
|
386
|
+
pub fn point(&self) -> Result<PublicPoint, String> {
|
|
387
|
+
let representation = self
|
|
388
|
+
.key
|
|
389
|
+
.public_key()
|
|
390
|
+
.and_then(|public| public.external_representation())
|
|
391
|
+
.ok_or("The public key is unavailable.")?;
|
|
392
|
+
let bytes = representation.bytes();
|
|
393
|
+
if bytes.len() != 65 || bytes[0] != 4 {
|
|
394
|
+
return Err("The public key is not an uncompressed P-256 point.".to_string());
|
|
395
|
+
}
|
|
396
|
+
Ok(PublicPoint {
|
|
397
|
+
x: bytes[1..33].to_vec(),
|
|
398
|
+
y: bytes[33..].to_vec(),
|
|
399
|
+
})
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
pub fn sign(&self, payload: &[u8]) -> Result<[u8; 64], String> {
|
|
403
|
+
if self.presence && !is_enclave(&self.key) {
|
|
404
|
+
evaluate()?;
|
|
405
|
+
}
|
|
406
|
+
let der = self
|
|
407
|
+
.key
|
|
408
|
+
.create_signature(Algorithm::ECDSASignatureMessageX962SHA256, payload)
|
|
409
|
+
.map_err(|e| e.to_string())?;
|
|
410
|
+
der_to_raw(&der)
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
#[cfg(not(target_os = "macos"))]
|
|
416
|
+
mod unsupported {
|
|
417
|
+
use super::PublicPoint;
|
|
418
|
+
|
|
419
|
+
pub struct Key;
|
|
420
|
+
|
|
421
|
+
pub fn signing_key(_identifier: &str) -> Result<Key, String> {
|
|
422
|
+
Err("unsupported".to_string())
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
pub fn presence_key(_identifier: &str) -> Result<Option<Key>, String> {
|
|
426
|
+
Err("unsupported".to_string())
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
pub fn find(_identifier: &str, _key_id: &str) -> Result<Key, String> {
|
|
430
|
+
Err("unsupported".to_string())
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
impl Key {
|
|
434
|
+
pub fn point(&self) -> Result<PublicPoint, String> {
|
|
435
|
+
Err("unsupported".to_string())
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
pub fn sign(&self, _payload: &[u8]) -> Result<[u8; 64], String> {
|
|
439
|
+
Err("unsupported".to_string())
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
package/rust/src/lib.rs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#[cfg(unix)]
|
|
2
|
+
mod control;
|
|
3
|
+
mod device_key;
|
|
4
|
+
#[cfg(unix)]
|
|
5
|
+
mod dialog;
|
|
6
|
+
#[cfg(unix)]
|
|
7
|
+
mod host;
|
|
8
|
+
mod keystore;
|
|
9
|
+
#[cfg(target_os = "macos")]
|
|
10
|
+
mod secret_store;
|
|
11
|
+
mod session;
|
|
12
|
+
#[cfg(target_os = "macos")]
|
|
13
|
+
mod vault;
|
|
14
|
+
|
|
15
|
+
pub use device_key::{parlor_device_key_public, parlor_device_key_sign};
|
|
16
|
+
#[cfg(unix)]
|
|
17
|
+
pub use host::{
|
|
18
|
+
parlor_host_begin, parlor_host_bundled, parlor_host_finish, parlor_host_status, setup,
|
|
19
|
+
};
|
|
20
|
+
pub use session::{parlor_session_get, parlor_session_set};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
use std::sync::OnceLock;
|
|
2
|
+
|
|
3
|
+
use security_framework::os::macos::code_signing::{Flags, SecCode, SecRequirement};
|
|
4
|
+
|
|
5
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
6
|
+
pub enum Backend {
|
|
7
|
+
Keychain,
|
|
8
|
+
Vault,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
static BACKEND: OnceLock<Backend> = OnceLock::new();
|
|
12
|
+
|
|
13
|
+
/// The Keychain for a build signed by an Apple-issued certificate, whose items it partitions by Team ID; the vault for
|
|
14
|
+
/// any other build, see `crate::vault`.
|
|
15
|
+
pub fn backend() -> Backend {
|
|
16
|
+
*BACKEND.get_or_init(|| {
|
|
17
|
+
if signed_by_apple() {
|
|
18
|
+
Backend::Keychain
|
|
19
|
+
} else {
|
|
20
|
+
Backend::Vault
|
|
21
|
+
}
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn signed_by_apple() -> bool {
|
|
26
|
+
let Ok(requirement) = "anchor apple generic".parse::<SecRequirement>() else {
|
|
27
|
+
return false;
|
|
28
|
+
};
|
|
29
|
+
SecCode::for_self(Flags::NONE)
|
|
30
|
+
.and_then(|code| code.check_validity(Flags::NONE, &requirement))
|
|
31
|
+
.is_ok()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
#[cfg(test)]
|
|
35
|
+
mod tests {
|
|
36
|
+
use super::*;
|
|
37
|
+
|
|
38
|
+
#[test]
|
|
39
|
+
fn an_ad_hoc_build_keeps_its_secrets_in_the_vault() {
|
|
40
|
+
assert_eq!(backend(), Backend::Vault);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
use tauri::AppHandle;
|
|
2
|
+
|
|
3
|
+
const ACCOUNT: &str = "parlor.session";
|
|
4
|
+
|
|
5
|
+
#[cfg(target_os = "macos")]
|
|
6
|
+
mod store {
|
|
7
|
+
use super::ACCOUNT;
|
|
8
|
+
use crate::secret_store::{Backend, backend};
|
|
9
|
+
use crate::vault;
|
|
10
|
+
|
|
11
|
+
pub fn get(service: &str) -> Result<Option<String>, String> {
|
|
12
|
+
match backend() {
|
|
13
|
+
Backend::Keychain => keychain::get(service),
|
|
14
|
+
Backend::Vault => vault::get(service, ACCOUNT),
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub fn set(service: &str, token: Option<&str>) -> Result<(), String> {
|
|
19
|
+
match backend() {
|
|
20
|
+
Backend::Keychain => keychain::set(service, token),
|
|
21
|
+
Backend::Vault => vault::set(service, ACCOUNT, token),
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
pub(super) mod keychain {
|
|
26
|
+
use super::super::ACCOUNT;
|
|
27
|
+
use security_framework::passwords::{
|
|
28
|
+
delete_generic_password, get_generic_password, set_generic_password,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const ITEM_NOT_FOUND: i32 = -25300;
|
|
32
|
+
|
|
33
|
+
pub fn get(service: &str) -> Result<Option<String>, String> {
|
|
34
|
+
match get_generic_password(service, ACCOUNT) {
|
|
35
|
+
Ok(bytes) => String::from_utf8(bytes)
|
|
36
|
+
.map(Some)
|
|
37
|
+
.map_err(|e| e.to_string()),
|
|
38
|
+
Err(error) if error.code() == ITEM_NOT_FOUND => Ok(None),
|
|
39
|
+
Err(error) => Err(error.to_string()),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
pub fn set(service: &str, token: Option<&str>) -> Result<(), String> {
|
|
44
|
+
match token {
|
|
45
|
+
Some(token) => set_generic_password(service, ACCOUNT, token.as_bytes())
|
|
46
|
+
.map_err(|e| e.to_string()),
|
|
47
|
+
None => match delete_generic_password(service, ACCOUNT) {
|
|
48
|
+
Ok(()) => Ok(()),
|
|
49
|
+
Err(error) if error.code() == ITEM_NOT_FOUND => Ok(()),
|
|
50
|
+
Err(error) => Err(error.to_string()),
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#[cfg(not(target_os = "macos"))]
|
|
58
|
+
mod store {
|
|
59
|
+
pub fn get(_service: &str) -> Result<Option<String>, String> {
|
|
60
|
+
Err("unsupported".to_string())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub fn set(_service: &str, _token: Option<&str>) -> Result<(), String> {
|
|
64
|
+
Err("unsupported".to_string())
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#[tauri::command]
|
|
69
|
+
pub async fn parlor_session_get(app: AppHandle) -> Result<Option<String>, String> {
|
|
70
|
+
let service = app.config().identifier.clone();
|
|
71
|
+
tauri::async_runtime::spawn_blocking(move || store::get(&service))
|
|
72
|
+
.await
|
|
73
|
+
.map_err(|e| e.to_string())?
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[tauri::command]
|
|
77
|
+
pub async fn parlor_session_set(app: AppHandle, token: Option<String>) -> Result<(), String> {
|
|
78
|
+
let service = app.config().identifier.clone();
|
|
79
|
+
tauri::async_runtime::spawn_blocking(move || store::set(&service, token.as_deref()))
|
|
80
|
+
.await
|
|
81
|
+
.map_err(|e| e.to_string())?
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[cfg(all(test, target_os = "macos"))]
|
|
85
|
+
mod tests {
|
|
86
|
+
use super::store::keychain;
|
|
87
|
+
|
|
88
|
+
#[test]
|
|
89
|
+
fn the_keychain_keeps_a_session_until_it_is_cleared() {
|
|
90
|
+
let service = format!("dev.parlor.test.session.{}", std::process::id());
|
|
91
|
+
assert_eq!(keychain::get(&service).unwrap(), None);
|
|
92
|
+
keychain::set(&service, Some("token-1")).unwrap();
|
|
93
|
+
assert_eq!(
|
|
94
|
+
keychain::get(&service).unwrap(),
|
|
95
|
+
Some("token-1".to_string())
|
|
96
|
+
);
|
|
97
|
+
keychain::set(&service, None).unwrap();
|
|
98
|
+
assert_eq!(keychain::get(&service).unwrap(), None);
|
|
99
|
+
}
|
|
100
|
+
}
|