Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weโ€™ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PM-14863] Force unlock when keys are cleared / on first unlock and fix account switching behavior #11994

Merged
merged 13 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/desktop/desktop_native/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/desktop/desktop_native/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ ssh-key = { version = "0.6.6", default-features = false, features = [
"rsa",
"getrandom",
] }
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", branch = "km/pm-10098/clean-russh-implementation" }
bitwarden-russh = { git = "https://github.com/bitwarden/bitwarden-russh.git", branch = "km/PM-14863/allow-unlock-on-list" }
coroiu marked this conversation as resolved.
Show resolved Hide resolved
tokio = { version = "=1.40.0", features = ["io-util", "sync", "macros", "net"] }
tokio-stream = { version = "=0.1.15", features = ["net"] }
tokio-util = "=0.7.12"
Expand Down
38 changes: 38 additions & 0 deletions apps/desktop/desktop_native/core/src/ssh_agent/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub struct BitwardenDesktopAgent {
show_ui_request_tx: tokio::sync::mpsc::Sender<(u32, String)>,
get_ui_response_rx: Arc<Mutex<tokio::sync::broadcast::Receiver<(u32, bool)>>>,
request_id: Arc<Mutex<u32>>,
// before first unlock, or after account switching, listing keys should require an unlock to get a list of public keys
coroiu marked this conversation as resolved.
Show resolved Hide resolved
needs_unlock: Arc<Mutex<bool>>,
}

impl BitwardenDesktopAgent {
Expand All @@ -46,6 +48,28 @@ impl ssh_agent::Agent for BitwardenDesktopAgent {
}
false
}

async fn can_list(&self) -> bool {
let needs_unlock = self.needs_unlock.lock().await;
if !*needs_unlock {
return true;
}
drop(needs_unlock);

let request_id = self.get_request_id().await;

let mut rx_channel = self.get_ui_response_rx.lock().await.resubscribe();
self.show_ui_request_tx
.send((request_id, "".to_string()))
.await
.expect("Should send request to ui");
while let Ok((id, response)) = rx_channel.recv().await {
if id == request_id {
return response;
}
}
false
}
}

impl BitwardenDesktopAgent {
Expand All @@ -65,6 +89,10 @@ impl BitwardenDesktopAgent {
let keystore = &mut self.keystore;
keystore.0.write().expect("RwLock is not poisoned").clear();

let mut needs_unlock = self.needs_unlock.blocking_lock();
*needs_unlock = false;
drop(needs_unlock);

for (key, name, cipher_id) in new_keys.iter() {
match parse_key_safe(&key) {
Ok(private_key) => {
Expand Down Expand Up @@ -102,6 +130,16 @@ impl BitwardenDesktopAgent {
});
Ok(())
}

pub fn clear_keys(&mut self) -> Result<(), anyhow::Error> {
let keystore = &mut self.keystore;
keystore.0.write().expect("RwLock is not poisoned").clear();
let mut needs_unlock = self.needs_unlock.blocking_lock();
*needs_unlock = true;
drop(needs_unlock);
coroiu marked this conversation as resolved.
Show resolved Hide resolved

Ok(())
}
}

fn parse_key_safe(pem: &str) -> Result<ssh_key::private::PrivateKey, anyhow::Error> {
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/desktop_native/core/src/ssh_agent/unix.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ impl BitwardenDesktopAgent {
show_ui_request_tx: auth_request_tx,
get_ui_response_rx: auth_response_rx,
request_id: Arc::new(tokio::sync::Mutex::new(0)),
needs_unlock: Arc::new(tokio::sync::Mutex::new(true)),
};
let cloned_agent_state = agent.clone();
tokio::spawn(async move {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ impl BitwardenDesktopAgent {
get_ui_response_rx: auth_response_rx,
cancellation_token: CancellationToken::new(),
request_id: Arc::new(tokio::sync::Mutex::new(0)),
needs_unlock: Arc::new(tokio::sync::Mutex::new(true)),
};
let stream = named_pipe_listener_stream::NamedPipeServerStream::new(
agent_state.cancellation_token.clone(),
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/desktop_native/napi/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export declare namespace sshagent {
export function setKeys(agentState: SshAgentState, newKeys: Array<PrivateKey>): void
export function lock(agentState: SshAgentState): void
export function importKey(encodedKey: string, password: string): SshKeyImportResult
export function clearKeys(agentState: SshAgentState): void
export function generateKeypair(keyAlgorithm: string): Promise<SshKey>
export class SshAgentState { }
}
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/desktop_native/napi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,12 @@ pub mod sshagent {
Ok(result.into())
}

#[napi]
pub fn clear_keys(agent_state: &mut SshAgentState) -> napi::Result<()> {
let bitwarden_agent_state = &mut agent_state.state;
bitwarden_agent_state.clear_keys().map_err(|e| napi::Error::from_reason(e.to_string()))
}

#[napi]
pub async fn generate_keypair(key_algorithm: String) -> napi::Result<SshKey> {
desktop_core::ssh_agent::generator::generate_keypair(key_algorithm)
Expand Down
6 changes: 6 additions & 0 deletions apps/desktop/src/platform/main/main-ssh-agent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,5 +111,11 @@
sshagent.lock(this.agentState);
}
});

ipcMain.handle("sshagent.clearkeys", async (event: any) => {

Check warning on line 115 in apps/desktop/src/platform/main/main-ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/main/main-ssh-agent.service.ts#L115

Added line #L115 was not covered by tests
if (this.agentState != null) {
sshagent.clearKeys(this.agentState);

Check warning on line 117 in apps/desktop/src/platform/main/main-ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/main/main-ssh-agent.service.ts#L117

Added line #L117 was not covered by tests
}
});
}
}
3 changes: 3 additions & 0 deletions apps/desktop/src/platform/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
lock: async () => {
return await ipcRenderer.invoke("sshagent.lock");
},
clearKeys: async () => {
return await ipcRenderer.invoke("sshagent.clearkeys");

Check warning on line 60 in apps/desktop/src/platform/preload.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/preload.ts#L59-L60

Added lines #L59 - L60 were not covered by tests
},
importKey: async (key: string, password: string): Promise<ssh.SshKeyImportResult> => {
const res = await ipcRenderer.invoke("sshagent.importkey", {
privateKey: key,
Expand Down
40 changes: 36 additions & 4 deletions apps/desktop/src/platform/services/ssh-agent.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from,
map,
of,
skip,
Subject,
switchMap,
takeUntil,
Expand All @@ -17,6 +18,7 @@
withLatestFrom,
} from "rxjs";

import { AccountService } from "@bitwarden/common/auth/abstractions/account.service";

Check warning on line 21 in apps/desktop/src/platform/services/ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/services/ssh-agent.service.ts#L21

Added line #L21 was not covered by tests
import { AuthService } from "@bitwarden/common/auth/abstractions/auth.service";
import { AuthenticationStatus } from "@bitwarden/common/auth/enums/authentication-status";
import { FeatureFlag } from "@bitwarden/common/enums/feature-flag.enum";
Expand Down Expand Up @@ -52,6 +54,7 @@
private i18nService: I18nService,
private desktopSettingsService: DesktopSettingsService,
private configService: ConfigService,
private accountService: AccountService,

Check warning on line 57 in apps/desktop/src/platform/services/ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/services/ssh-agent.service.ts#L57

Added line #L57 was not covered by tests
) {}

async init() {
Expand Down Expand Up @@ -110,19 +113,39 @@
),
),
// This concatMap handles showing the dialog to approve the request.
concatMap(([message, decryptedCiphers]) => {
concatMap(([message, ciphers]) => {
const cipherId = message.cipherId as string;
const requestId = message.requestId as number;

if (decryptedCiphers === undefined) {
// cipherid is empty has the special meaning of "unlock in order to list public keys"
if (cipherId == "") {
Copy link
Contributor

@coroiu coroiu Nov 19, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: I might be misunderstanding how this works, but my first thought is that we should use a proper message variable or message type to indicate this and not add hidden meaning to unrelated

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that makes sense. We are extending this for other information aswell (information about the ssh request in the future, and process information in #12065) so we need to extend this either way.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice! I get the feeling that we're gonna need to take a proper look at how to consolidate this with passkeys, but not in this PR :)

return of(true).pipe(
switchMap(async (result) => {
coroiu marked this conversation as resolved.
Show resolved Hide resolved
const sshCiphers = ciphers.filter(

Check warning on line 124 in apps/desktop/src/platform/services/ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/services/ssh-agent.service.ts#L122-L124

Added lines #L122 - L124 were not covered by tests
(cipher) => cipher.type === CipherType.SshKey && !cipher.isDeleted,
);
const keys = sshCiphers.map((cipher) => {
return {

Check warning on line 128 in apps/desktop/src/platform/services/ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/services/ssh-agent.service.ts#L127-L128

Added lines #L127 - L128 were not covered by tests
name: cipher.name,
privateKey: cipher.sshKey.privateKey,
cipherId: cipher.id,
};
});
await ipc.platform.sshAgent.setKeys(keys);
await ipc.platform.sshAgent.signRequestResponse(requestId, Boolean(result));

Check warning on line 135 in apps/desktop/src/platform/services/ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/services/ssh-agent.service.ts#L134-L135

Added lines #L134 - L135 were not covered by tests
}),
);
}

if (ciphers === undefined) {
return of(false).pipe(
switchMap((result) =>
ipc.platform.sshAgent.signRequestResponse(requestId, Boolean(result)),
),
);
}

const cipher = decryptedCiphers.find((cipher) => cipher.id == cipherId);
const cipher = ciphers.find((cipher) => cipher.id == cipherId);

Check warning on line 148 in apps/desktop/src/platform/services/ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/services/ssh-agent.service.ts#L148

Added line #L148 was not covered by tests

ipc.platform.focusWindow();
const dialogRef = ApproveSshRequestComponent.open(
Expand All @@ -141,14 +164,23 @@
)
.subscribe();

this.accountService.activeAccount$

Check warning on line 167 in apps/desktop/src/platform/services/ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/services/ssh-agent.service.ts#L167

Added line #L167 was not covered by tests
.pipe(skip(1), takeUntil(this.destroy$))
.subscribe((account) => {
this.logService.info("Active account changed, clearing SSH keys");
ipc.platform.sshAgent

Check warning on line 171 in apps/desktop/src/platform/services/ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/services/ssh-agent.service.ts#L170-L171

Added lines #L170 - L171 were not covered by tests
.clearKeys()
.catch((e) => this.logService.error("Failed to clear SSH keys", e));

Check warning on line 173 in apps/desktop/src/platform/services/ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/services/ssh-agent.service.ts#L173

Added line #L173 was not covered by tests
});
coroiu marked this conversation as resolved.
Show resolved Hide resolved

combineLatest([
timer(0, this.SSH_REFRESH_INTERVAL),
this.desktopSettingsService.sshAgentEnabled$,
])
.pipe(
concatMap(async ([, enabled]) => {
if (!enabled) {
await ipc.platform.sshAgent.setKeys([]);
await ipc.platform.sshAgent.clearKeys();

Check warning on line 183 in apps/desktop/src/platform/services/ssh-agent.service.ts

View check run for this annotation

Codecov / codecov/patch

apps/desktop/src/platform/services/ssh-agent.service.ts#L183

Added line #L183 was not covered by tests
return;
}

Expand Down
Loading