diff --git a/core/nectarlink-core/src/lib.rs b/core/nectarlink-core/src/lib.rs index 0002ecbb..f0c0778b 100644 --- a/core/nectarlink-core/src/lib.rs +++ b/core/nectarlink-core/src/lib.rs @@ -39,6 +39,7 @@ mod store; pub mod timeline; pub mod toggles; mod transfer; +pub mod unlock; pub mod usb; pub mod webcam; diff --git a/core/nectarlink-core/src/unlock.rs b/core/nectarlink-core/src/unlock.rs new file mode 100644 index 00000000..13baa0b4 --- /dev/null +++ b/core/nectarlink-core/src/unlock.rs @@ -0,0 +1,379 @@ +// SPDX-License-Identifier: MPL-2.0 +//! Unlock the PC with the phone, the PC's half (docs/adr/0009-unlock-with-phone.md; +//! proposed, not wired up). The user's Windows password is kept only sealed +//! under a key `K` the PC doesn't keep: the phone holds `K`, wrapped by a +//! Keystore key that needs the owner's fingerprint for every use, and returns +//! it for one unlock at a time. +//! +//! This module is the part that can be tested on its own: sealing and opening +//! the password (AES-256-GCM, bound to the PC, the phone and the Windows user), +//! and the rules for unlock requests: single use, a short expiry, only the +//! enrolled phone may answer, and a lockout after repeated failures. + +use std::{ + collections::{HashMap, VecDeque}, + time::{Duration, Instant}, +}; + +use nectarlink_protocol::DeviceId; +use ring::{ + aead::{AES_256_GCM, Aad, LessSafeKey, NONCE_LEN, Nonce, UnboundKey}, + rand::{SecureRandom, SystemRandom}, +}; +use serde::{Deserialize, Serialize}; + +/// Bytes in `K`. +pub const KEY_LEN: usize = 32; +/// How long the phone has to answer a request. +pub const REQUEST_TTL: Duration = Duration::from_secs(30); +/// Failed unlocks allowed within [`FAILURE_WINDOW`] before a lockout. +pub const MAX_FAILURES: usize = 5; +pub const FAILURE_WINDOW: Duration = Duration::from_secs(15 * 60); +pub const LOCKOUT: Duration = Duration::from_secs(15 * 60); +/// Separates this use of the key from any other. +const DOMAIN: &[u8] = b"nectarlink-unlock-v1\0"; + +/// Secret bytes (`K`, or the password): overwritten when dropped, never printed. +pub struct Secret(Vec); + +impl Secret { + pub fn new(bytes: Vec) -> Secret { + Secret(bytes) + } + + pub fn expose(&self) -> &[u8] { + &self.0 + } +} + +impl Drop for Secret { + fn drop(&mut self) { + self.0.fill(0); + // Best effort without unsafe code: black_box keeps the wipe from being + // optimized away as a store nobody reads. + std::hint::black_box(&self.0); + } +} + +impl std::fmt::Debug for Secret { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Secret({} bytes)", self.0.len()) + } +} + +/// What a sealed password belongs to: opening it for any other PC, phone or +/// user fails, even with the right key. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Binding { + pub pc: DeviceId, + pub phone: DeviceId, + /// The Windows user's security identifier (`S-1-5-21-…`). + pub user: String, +} + +impl Binding { + fn aad(&self) -> Vec { + let user = self.user.as_bytes(); + let mut aad = Vec::with_capacity(DOMAIN.len() + 64 + 4 + user.len()); + aad.extend_from_slice(DOMAIN); + aad.extend_from_slice(&self.pc.0); + aad.extend_from_slice(&self.phone.0); + aad.extend_from_slice(&(user.len() as u32).to_be_bytes()); + aad.extend_from_slice(user); + aad + } +} + +/// The password, sealed under `K`: what the PC keeps on disk. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Sealed { + pub nonce: [u8; NONCE_LEN], + pub ciphertext: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)] +pub enum VaultError { + #[error("no secure random numbers")] + Random, + #[error("the key isn't 32 bytes")] + KeyLength, + /// The key is wrong, or the sealed password was changed, or it belongs + /// to another PC, phone or user. + #[error("can't open the sealed password")] + Open, +} + +/// A new random `K`, for enrolling. +pub fn new_key() -> Result { + let mut key = vec![0u8; KEY_LEN]; + SystemRandom::new().fill(&mut key).map_err(|_| VaultError::Random)?; + Ok(Secret(key)) +} + +fn aead_key(key: &Secret) -> Result { + if key.expose().len() != KEY_LEN { + return Err(VaultError::KeyLength); + } + let unbound = UnboundKey::new(&AES_256_GCM, key.expose()).map_err(|_| VaultError::KeyLength)?; + Ok(LessSafeKey::new(unbound)) +} + +/// Seals `password` under `key` for `binding`, with a fresh random nonce. +pub fn seal(password: &Secret, key: &Secret, binding: &Binding) -> Result { + let aead = aead_key(key)?; + let mut nonce = [0u8; NONCE_LEN]; + SystemRandom::new().fill(&mut nonce).map_err(|_| VaultError::Random)?; + let mut buffer = password.expose().to_vec(); + let sealed = aead.seal_in_place_append_tag( + Nonce::assume_unique_for_key(nonce), + Aad::from(binding.aad()), + &mut buffer, + ); + if sealed.is_err() { + // Don't leave the plaintext copy behind. + drop(Secret(buffer)); + return Err(VaultError::Open); + } + Ok(Sealed { nonce, ciphertext: buffer }) +} + +/// Opens `sealed` with `key` for `binding`. +pub fn open(sealed: &Sealed, key: &Secret, binding: &Binding) -> Result { + let aead = aead_key(key)?; + let mut buffer = sealed.ciphertext.clone(); + let opened = + aead.open_in_place(Nonce::assume_unique_for_key(sealed.nonce), Aad::from(binding.aad()), &mut buffer); + match opened { + Ok(plain) => { + let len = plain.len(); + buffer.truncate(len); + Ok(Secret(buffer)) + } + Err(_) => { + drop(Secret(buffer)); + Err(VaultError::Open) + } + } +} + +// ---- Requests ---- + +/// One unlock request's ID: random, so it can't be guessed. +pub type RequestId = [u8; 16]; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Request { + pub phone: DeviceId, + pub user: String, + pub expires: Instant, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Refused { + /// Too many failures; unlocking with the phone works again at `until`. + LockedOut { + until: Instant, + }, + /// Not a request this PC made, or one already answered. + Unknown, + Expired, + /// An answer from a device other than the one asked. + WrongDevice, + Random, +} + +/// The PC's unlock requests and its record of failures. +#[derive(Debug, Default)] +pub struct Requests { + pending: HashMap, + failures: VecDeque, + locked_until: Option, +} + +impl Requests { + pub fn new() -> Requests { + Requests::default() + } + + /// Whether unlocking with the phone is locked out at `now`, and until when. + pub fn locked_out(&self, now: Instant) -> Option { + self.locked_until.filter(|until| now < *until) + } + + /// Starts a request to `phone` for `user`, unless locked out. + pub fn begin(&mut self, phone: DeviceId, user: &str, now: Instant) -> Result { + if let Some(until) = self.locked_out(now) { + return Err(Refused::LockedOut { until }); + } + self.pending.retain(|_, r| now < r.expires); + let mut id = [0u8; 16]; + SystemRandom::new().fill(&mut id).map_err(|_| Refused::Random)?; + self.pending.insert(id, Request { phone, user: user.to_owned(), expires: now + REQUEST_TTL }); + Ok(id) + } + + /// Takes the answer to `id`, from `from`: each request can be answered + /// once, before it expires, and only by the phone it was sent to. What it + /// returns is then opened with the vault; report how that went with + /// [`succeeded`](Self::succeeded) or [`failed`](Self::failed). + pub fn answer(&mut self, id: &RequestId, from: DeviceId, now: Instant) -> Result { + if let Some(until) = self.locked_out(now) { + self.pending.remove(id); + return Err(Refused::LockedOut { until }); + } + let request = self.pending.remove(id).ok_or(Refused::Unknown)?; + if now >= request.expires { + return Err(Refused::Expired); + } + if request.phone != from { + self.failed(now); + return Err(Refused::WrongDevice); + } + Ok(request) + } + + /// An unlock failed (the key didn't open the password, or a wrong device + /// answered). Too many within the window lock it out for a while. + pub fn failed(&mut self, now: Instant) { + while self.failures.front().is_some_and(|at| now.duration_since(*at) >= FAILURE_WINDOW) { + self.failures.pop_front(); + } + self.failures.push_back(now); + if self.failures.len() >= MAX_FAILURES { + self.locked_until = Some(now + LOCKOUT); + self.failures.clear(); + self.pending.clear(); + } + } + + /// An unlock worked: earlier failures no longer count. + pub fn succeeded(&mut self) { + self.failures.clear(); + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn binding() -> Binding { + Binding { pc: DeviceId([1; 32]), phone: DeviceId([2; 32]), user: "S-1-5-21-1-2-3-1001".into() } + } + + fn password() -> Secret { + Secret::new(b"correct horse battery staple".to_vec()) + } + + #[test] + fn a_sealed_password_opens_only_with_its_key_and_binding() { + let key = new_key().unwrap(); + let sealed = seal(&password(), &key, &binding()).unwrap(); + assert_ne!(sealed.ciphertext.as_slice(), password().expose(), "not stored in the clear"); + assert_eq!(open(&sealed, &key, &binding()).unwrap().expose(), password().expose()); + + let other_key = new_key().unwrap(); + assert_eq!(open(&sealed, &other_key, &binding()).unwrap_err(), VaultError::Open); + for other in [ + Binding { pc: DeviceId([9; 32]), ..binding() }, + Binding { phone: DeviceId([9; 32]), ..binding() }, + Binding { user: "S-1-5-21-1-2-3-1002".into(), ..binding() }, + ] { + assert_eq!(open(&sealed, &key, &other).unwrap_err(), VaultError::Open, "{other:?}"); + } + } + + #[test] + fn a_changed_sealed_password_doesnt_open() { + let key = new_key().unwrap(); + let mut sealed = seal(&password(), &key, &binding()).unwrap(); + sealed.ciphertext[0] ^= 1; + assert_eq!(open(&sealed, &key, &binding()).unwrap_err(), VaultError::Open); + let mut sealed = seal(&password(), &key, &binding()).unwrap(); + sealed.nonce[0] ^= 1; + assert_eq!(open(&sealed, &key, &binding()).unwrap_err(), VaultError::Open); + } + + #[test] + fn each_seal_uses_a_new_nonce_and_keys_must_be_32_bytes() { + let key = new_key().unwrap(); + let a = seal(&password(), &key, &binding()).unwrap(); + let b = seal(&password(), &key, &binding()).unwrap(); + assert_ne!(a.nonce, b.nonce); + assert_ne!(a.ciphertext, b.ciphertext); + assert_eq!( + seal(&password(), &Secret::new(vec![0; 16]), &binding()).unwrap_err(), + VaultError::KeyLength + ); + } + + #[test] + fn secrets_are_never_printed() { + assert_eq!(format!("{:?}", password()), "Secret(28 bytes)"); + } + + #[test] + fn a_request_is_answered_once_in_time_by_its_phone() { + let now = Instant::now(); + let phone = DeviceId([2; 32]); + let mut requests = Requests::new(); + let id = requests.begin(phone, "S-1", now).unwrap(); + let request = requests.answer(&id, phone, now + Duration::from_secs(5)).unwrap(); + assert_eq!((request.phone, request.user.as_str()), (phone, "S-1")); + assert_eq!(requests.answer(&id, phone, now).unwrap_err(), Refused::Unknown, "only once"); + + let late = requests.begin(phone, "S-1", now).unwrap(); + assert_eq!(requests.answer(&late, phone, now + REQUEST_TTL).unwrap_err(), Refused::Expired); + + let stolen = requests.begin(phone, "S-1", now).unwrap(); + assert_eq!(requests.answer(&stolen, DeviceId([7; 32]), now).unwrap_err(), Refused::WrongDevice); + assert_eq!( + requests.answer(&stolen, phone, now).unwrap_err(), + Refused::Unknown, + "used up by the wrong device" + ); + + assert_eq!(requests.answer(&[0; 16], phone, now).unwrap_err(), Refused::Unknown); + } + + #[test] + fn repeated_failures_lock_it_out_for_a_while() { + let start = Instant::now(); + let phone = DeviceId([2; 32]); + let mut requests = Requests::new(); + for i in 0..MAX_FAILURES - 1 { + requests.failed(start + Duration::from_secs(i as u64)); + } + assert!(requests.begin(phone, "S-1", start).is_ok(), "not yet"); + let pending = requests.begin(phone, "S-1", start).unwrap(); + requests.failed(start + Duration::from_secs(10)); + let at = start + Duration::from_secs(11); + assert!(matches!(requests.begin(phone, "S-1", at), Err(Refused::LockedOut { .. }))); + assert!( + matches!(requests.answer(&pending, phone, at), Err(Refused::LockedOut { .. })), + "pending requests are dropped too" + ); + let after = start + Duration::from_secs(10) + LOCKOUT; + assert!(requests.begin(phone, "S-1", after).is_ok(), "works again after the lockout"); + } + + #[test] + fn old_failures_and_a_success_dont_count() { + let start = Instant::now(); + let phone = DeviceId([2; 32]); + let mut requests = Requests::new(); + for i in 0..MAX_FAILURES - 1 { + requests.failed(start + Duration::from_secs(i as u64)); + } + // Long after: the earlier ones fell out of the window. + requests.failed(start + FAILURE_WINDOW + Duration::from_secs(60)); + assert!(requests.begin(phone, "S-1", start + FAILURE_WINDOW + Duration::from_secs(61)).is_ok()); + + let mut requests = Requests::new(); + for i in 0..MAX_FAILURES - 1 { + requests.failed(start + Duration::from_secs(i as u64)); + } + requests.succeeded(); + requests.failed(start + Duration::from_secs(10)); + assert!(requests.begin(phone, "S-1", start + Duration::from_secs(11)).is_ok()); + } +} diff --git a/docs/adr/0009-unlock-with-phone.md b/docs/adr/0009-unlock-with-phone.md new file mode 100644 index 00000000..60205b44 --- /dev/null +++ b/docs/adr/0009-unlock-with-phone.md @@ -0,0 +1,127 @@ +# ADR 0009: Unlock the PC with the phone + +- **Status:** Proposed (needs the owner's decision and an external security review before any of it ships) +- **Date:** 2026-10-19 + +## Context + +People want to unlock their locked PC by touching the fingerprint sensor on +their phone, the way Phone Link's "Dynamic Lock" locks it. `docs/PLAN.md` +§3.10 lists it as a dedicated, post-1.0 workstream because it puts code in the +most sensitive place on Windows: the sign-in screen. + +Two questions decide the design: + +1. **What does Windows accept at the lock screen?** Only a credential: a + password, a smart-card certificate, or Windows Hello. A third-party app + can't simply tell Windows "unlock". +2. **Where does that credential live, and who can get it?** Whatever we keep + must be useless to someone who steals the PC's disk, malware running as the + user, and anyone without the phone and its owner's fingerprint. + +### Ways Windows allows it + +| Approach | How | Fit | +|---|---|---| +| Custom credential provider + stored password | An `ICredentialProvider` DLL in LogonUI returns `KERB_INTERACTIVE_UNLOCK_LOGON` with the user's password | Works for local and Microsoft accounts that have a password. Most third-party "unlock with phone" tools do this. We must store the password, so how it's stored decides the security. | +| Windows Hello companion device framework (`SecondaryAuthenticationFactor`) | Microsoft's own API for companion devices | Microsoft lists it as deprecated; to verify on current Windows 11 before ruling it out entirely. Would avoid storing a password. | +| Certificate / virtual smart card logon | A credential provider presenting a certificate | Needs a domain and a certificate authority; not for home PCs. | + +## Decision (proposed) + +A credential provider that returns the user's password, where the password +can only be recovered with the phone and the user's fingerprint, and only +to unlock a session that's already signed in (not the first sign-in after a +restart). + +### Split key + +- **Enrollment** (the user turns it on, signed in, in Settings, and types + their Windows password once): + 1. The PC makes a random 256-bit key `K` and encrypts the password with + AES-256-GCM under `K`. Only that ciphertext is kept on the PC, bound + (as associated data) to this PC, this phone and this Windows user. + 2. The PC sends `K` to the phone over the paired session. The phone wraps it + with an AES-GCM key held in Android Keystore (StrongBox when there is + one) that requires strong biometrics for every use and is invalidated + when a new fingerprint is enrolled. The phone returns the wrapped `K` + and keeps nothing else. + 3. The PC stores the wrapped `K` beside the ciphertext and forgets `K`. +- **Unlock**: the lock screen offers "Unlock with ". The PC sends a + request (single use, 30-second expiry, naming the PC and the user) with the + wrapped `K`. The phone shows a biometric prompt naming the PC and the user; + on success Keystore unwraps `K` and the phone returns it. The PC decrypts + the password (the GCM tag proves `K` is right), hands it to LogonUI, and + wipes `K` and the password from memory. + +What each side holds: + +| Holder | Has | Alone, it gets | +|---|---|---| +| PC disk | password ciphertext, wrapped `K` | nothing: `K` never touches the PC's disk, and the wrapping key never leaves the phone | +| Phone | the non-exportable wrapping key | nothing: it never sees the password | +| Network | an encrypted, mutually authenticated session (ADR 0003) | nothing | + +### Guards + +- **Only on the local network**, never over relays or mobile data, and only + with a live session to the enrolled phone. Optionally, BLE proximity (the + beacon already in `ble_beacon`). +- **Rate limit:** 5 failed attempts in 15 minutes locks the feature for 15 + minutes and notifies the user on both devices. +- **Revocable:** unpairing, turning the feature off, changing the Windows + password (which makes the ciphertext useless; the PC asks to re-enroll), + or a new fingerprint on the phone (the Keystore key is invalidated) all end + it. +- **Unlock only:** the credential provider answers `CPUS_UNLOCK_WORKSTATION`, + not `CPUS_LOGON`. Unlocking needs NectarLink running in the locked session, + which also means a restart always asks for the real password once. +- **Off by default**, behind a clear explanation, and absent from builds until + the review is done. + +### Where the pieces run + +- **Core (`nectarlink-core`, Rust):** the protocol, the vault (encrypting, + decrypting, binding) and the request rules above. Pure code, fully tested. + The first part of this is in `core/nectarlink-core/src/unlock.rs`. +- **Phone (Kotlin):** the Keystore key, the biometric prompt with a + `CryptoObject`, and the protocol handler. +- **Credential provider (`desktop/credprov`, C++):** a small COM DLL in + LogonUI. It knows nothing about phones: it asks the user's NectarLink, over + a named pipe whose ACL admits only that user's NectarLink, for "the + credential for user X", and serializes the answer. Kept small so it's easy + to review. +- **Installation:** registering a credential provider is a machine-wide + change (HKLM, administrator). The installer would do it only when the user + turns the feature on, and remove it when it's off. + +## Threat model + +| Threat | Mitigation | +|---|---| +| Stolen or copied PC disk | Ciphertext without `K`; `K` is only on the phone, wrapped by a non-exportable key | +| Stolen phone | Needs the owner's biometric; a new fingerprint invalidates the key | +| Someone at the locked PC with a stolen, unlocked phone | Still needs the biometric for each unlock | +| Replay of an old response | Single-use request IDs, a 30-second expiry, and the session's own encryption | +| A different paired device answering | Responses only from the enrolled phone's identity, bound into the ciphertext's associated data | +| Malware running as the user on the PC | Out of scope for any unlock feature (it already has the session), but it must not learn the password: `K` and the password exist only in memory, briefly, inside NectarLink; the pipe to the credential provider admits only NectarLink. Residual risk: malware that reads NectarLink's memory at that moment. To be reviewed. | +| Bugs in the credential provider locking the user out | It only adds a tile; the normal password tile stays. Tested in a VM with recovery snapshots before any real machine. | +| Brute force via the lock screen | Rate limit and lockout above | + +## Open questions for the owner + +1. Is storing the password (split as above) acceptable, or should we first + confirm whether Windows Hello's companion device API still works on + Windows 11 24H2 and later? +2. PIN-only and passwordless Microsoft accounts have no usable password; we + would tell those users it isn't available. Acceptable? +3. Who does the external review, and does the credential provider need code + signing first (SignPath)? + +## Consequences + +- A security-critical component that needs its own tests, a VM test plan, and + an external review before release. +- More install surface: a DLL registered machine-wide when enabled. +- The core and protocol parts can be built and tested now, independently of + the credential provider. diff --git a/docs/adr/README.md b/docs/adr/README.md index 6e5b3265..ed3aac73 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -10,3 +10,4 @@ | 0006 | [Kotlin + Jetpack Compose for the Android app](0006-kotlin-compose-android.md) | Accepted | | 0007 | [Licensing](0007-licensing.md) | Accepted | | 0008 | [Platforms and distribution](0008-platforms-and-distribution.md) | Accepted | +| 0009 | [Unlock the PC with the phone](0009-unlock-with-phone.md) | Proposed | diff --git a/docs/protocol/unlock.md b/docs/protocol/unlock.md new file mode 100644 index 00000000..e873bd30 --- /dev/null +++ b/docs/protocol/unlock.md @@ -0,0 +1,70 @@ +# Unlock the PC with the phone (draft) + +> Status: **proposal**, not implemented on the wire. See +> [ADR 0009](../adr/0009-unlock-with-phone.md) for the design, the threat model +> and the open questions. Builds on [protocol v0](v0.md). License: CC BY 4.0. + +A phone lets a paired PC unlock its locked Windows session after the phone's +owner confirms with a strong biometric. The PC keeps the user's Windows +password only sealed under a key `K`; the phone keeps `K` only wrapped by a +non-exportable Keystore key that needs a biometric for each use. + +## 1. Capabilities and toggle + +| Capability | Offered by | Meaning | +|---|---|---| +| `unlock.key` | Phone | Can hold a wrapped key for a PC and release it after a strong biometric | +| `unlock.pc` | PC | Can unlock its locked session with a phone | + +Both devices have an `unlock` per-device toggle, **off by default**. Every +message below is refused with `DENIED` when it's off on the receiving side, and +only accepted on a direct local path (LAN or USB), never over a relay. + +## 2. Messages + +On the control stream. + +``` +t = "unlock.enroll" id = n b = { pc_name: text, user_name: text, user: text, key: bytes(32) } +t = "unlock.wrapped" re = n b = { wrapped: bytes } +t = "unlock.request" id = n b = { request: bytes(16), user_name: text, wrapped: bytes } +t = "unlock.release" re = n b = { request: bytes(16), key: bytes(32) } +t = "unlock.forget" b = { user?: text } +``` + +### 2.1 `unlock.enroll` (PC to phone) + +Sent once the user has turned it on in the PC's Settings and typed their +Windows password there. `user` is the Windows security identifier, `user_name` +what the phone shows. The phone asks its owner ("Let unlock with +your fingerprint for ?"); after a strong biometric it wraps `key` +with its Keystore key and answers `unlock.wrapped`. Errors: `DENIED` when +declined or off, `UNSUPPORTED` without a strong biometric. + +The PC seals the password under `key` (AES-256-GCM, associated data binding the +PC, the phone and `user`), keeps the sealed password and `wrapped`, and +forgets `key`. + +### 2.2 `unlock.request` (PC to phone) + +Sent when the user picks "Unlock with " on the lock screen. `request` is +random and single use; the phone must answer within 30 seconds. The phone +shows its biometric prompt ("Unlock for ?"), unwraps +`wrapped`, and answers `unlock.release` with the same `request` and `key`. +Errors: `DENIED` (declined or off), `NOT_FOUND` (the Keystore key is gone, +for example after a new fingerprint: the PC asks the user to enroll again). + +The PC opens the sealed password with `key`. A key that doesn't open it counts +as a failure; five failures in 15 minutes lock the feature for 15 minutes. + +### 2.3 `unlock.forget` (either way) + +Ends it: the phone deletes its Keystore key for this PC (or for `user`), the PC +deletes the sealed password and the wrapped key. Sent when either side turns +the toggle off or unpairs. + +## 3. Privacy and logging + +`key`, `wrapped` and the password are never logged, shown or written anywhere +but where section 2 says. The phone's prompt always names the PC and the user, +so a request can't be passed off as something else.