1use crate::{
7 constants::{get_default_user_agent, APPLICATION_NAME, IS_INTERACTIVE_CLI, PRODUCT_NAME_LONG},
8 debug, error, info, log,
9 state::{LauncherPaths, PersistedState},
10 trace,
11 util::{
12 errors::{
13 wrap, AnyError, CodeError, OAuthError, RefreshTokenNotAvailableError, StatusError,
14 WrappedError,
15 },
16 input::prompt_options,
17 },
18 warning,
19};
20use async_trait::async_trait;
21use chrono::{DateTime, Utc};
22use gethostname::gethostname;
23use serde::{de::DeserializeOwned, Deserialize, Serialize};
24use std::{cell::Cell, fmt::Display, path::PathBuf, sync::Arc, thread};
25use tokio::time::sleep;
26use tunnels::{
27 contracts::PROD_FIRST_PARTY_APP_ID,
28 management::{Authorization, AuthorizationProvider, HttpError},
29};
30
31#[derive(Deserialize)]
32struct DeviceCodeResponse {
33 device_code: String,
34 user_code: String,
35 message: Option<String>,
36 verification_uri: String,
37 expires_in: i64,
38}
39
40#[derive(Deserialize, Debug)]
41struct AuthenticationResponse {
42 access_token: String,
43 refresh_token: Option<String>,
44 expires_in: Option<i64>,
45}
46
47#[derive(Deserialize)]
48struct AuthenticationError {
49 error: String,
50 error_description: Option<String>,
51}
52
53#[derive(clap::ValueEnum, Serialize, Deserialize, Debug, Clone, Copy)]
54pub enum AuthProvider {
55 Microsoft,
56 Github,
57}
58
59impl Display for AuthProvider {
60 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
61 match self {
62 AuthProvider::Microsoft => write!(f, "Microsoft Account"),
63 AuthProvider::Github => write!(f, "Github Account"),
64 }
65 }
66}
67
68impl AuthProvider {
69 pub fn client_id(&self) -> &'static str {
70 match self {
71 AuthProvider::Microsoft => "aebc6443-996d-45c2-90f0-388ff96faa56",
72 AuthProvider::Github => "01ab8ac9400c4e429b23",
73 }
74 }
75
76 pub fn code_uri(&self) -> &'static str {
77 match self {
78 AuthProvider::Microsoft => {
79 "https://login.microsoftonline.com/organizations/oauth2/v2.0/devicecode"
80 }
81 AuthProvider::Github => "https://github.com/login/device/code",
82 }
83 }
84
85 pub fn grant_uri(&self) -> &'static str {
86 match self {
87 AuthProvider::Microsoft => {
88 "https://login.microsoftonline.com/organizations/oauth2/v2.0/token"
89 }
90 AuthProvider::Github => "https://github.com/login/oauth/access_token",
91 }
92 }
93
94 pub fn get_default_scopes(&self) -> String {
95 match self {
96 AuthProvider::Microsoft => format!(
97 "{}/.default+offline_access+profile+openid",
98 PROD_FIRST_PARTY_APP_ID
99 ),
100 AuthProvider::Github => "read:user+read:org".to_string(),
101 }
102 }
103}
104
105#[derive(Serialize, Deserialize, Debug, Clone)]
106pub struct StoredCredential {
107 #[serde(rename = "p")]
108 provider: AuthProvider,
109 #[serde(rename = "a")]
110 access_token: String,
111 #[serde(rename = "r")]
112 refresh_token: Option<String>,
113 #[serde(rename = "e")]
114 expires_at: Option<DateTime<Utc>>,
115}
116
117const GH_USER_ENDPOINT: &str = "https://api.github.com/user";
118
119async fn get_github_user(
120 client: &reqwest::Client,
121 access_token: &str,
122) -> Result<reqwest::Response, reqwest::Error> {
123 client
124 .get(GH_USER_ENDPOINT)
125 .header("Authorization", format!("token {}", access_token))
126 .header("User-Agent", get_default_user_agent())
127 .send()
128 .await
129}
130
131impl StoredCredential {
132 pub async fn is_expired(&self, log: &log::Logger, client: &reqwest::Client) -> bool {
133 match self.provider {
134 AuthProvider::Microsoft => self
135 .expires_at
136 .map(|e| Utc::now() + chrono::Duration::minutes(5) > e)
137 .unwrap_or(false),
138
139 AuthProvider::Github => {
143 let res = get_github_user(client, &self.access_token).await;
144 let res = match res {
145 Ok(r) => r,
146 Err(e) => {
147 warning!(log, "failed to check Github token: {}", e);
148 return false;
149 }
150 };
151
152 if res.status().is_success() {
153 return false;
154 }
155
156 let err = StatusError::from_res(res).await;
157 debug!(log, "github token looks expired: {:?}", err);
158 true
159 }
160 }
161 }
162
163 fn from_response(auth: AuthenticationResponse, provider: AuthProvider) -> Self {
164 StoredCredential {
165 provider,
166 access_token: auth.access_token,
167 refresh_token: auth.refresh_token,
168 expires_at: auth
169 .expires_in
170 .map(|e| Utc::now() + chrono::Duration::seconds(e)),
171 }
172 }
173}
174
175struct StorageWithLastRead {
176 storage: Box<dyn StorageImplementation>,
177 fallback_storage: Option<FileStorage>,
178 last_read: Cell<Result<Option<StoredCredential>, WrappedError>>,
179}
180
181#[derive(Clone)]
182pub struct Auth {
183 client: reqwest::Client,
184 log: log::Logger,
185 file_storage_path: PathBuf,
186 storage: Arc<std::sync::Mutex<Option<StorageWithLastRead>>>,
187}
188
189trait StorageImplementation: Send + Sync {
190 fn read(&mut self) -> Result<Option<StoredCredential>, AnyError>;
191 fn store(&mut self, value: StoredCredential) -> Result<(), AnyError>;
192 fn clear(&mut self) -> Result<(), AnyError>;
193}
194
195fn seal<T>(value: &T) -> String
197where
198 T: Serialize + ?Sized,
199{
200 let dec = serde_json::to_string(value).expect("expected to serialize");
201 if std::env::var("VSCODE_CLI_DISABLE_KEYCHAIN_ENCRYPT").is_ok() {
202 return dec;
203 }
204 encrypt(&dec)
205}
206
207fn unseal<T>(value: &str) -> Option<T>
209where
210 T: DeserializeOwned,
211{
212 if let Ok(v) = serde_json::from_str::<T>(value) {
214 return Some(v);
215 }
216
217 let dec = decrypt(value)?;
218 serde_json::from_str::<T>(&dec).ok()
219}
220
221#[cfg(target_os = "windows")]
222const KEYCHAIN_ENTRY_LIMIT: usize = 1024;
223#[cfg(not(target_os = "windows"))]
224const KEYCHAIN_ENTRY_LIMIT: usize = 128 * 1024;
225
226const CONTINUE_MARKER: &str = "<MORE>";
227
228struct ThreadKeyringStorage {
231 s: Option<KeyringStorage>,
232}
233
234impl ThreadKeyringStorage {
235 fn thread_op<R, Fn>(&mut self, f: Fn) -> Result<R, AnyError>
236 where
237 Fn: 'static + Send + FnOnce(&mut KeyringStorage) -> Result<R, AnyError>,
238 R: 'static + Send,
239 {
240 let mut s = match self.s.take() {
241 Some(s) => s,
242 None => return Err(CodeError::KeyringTimeout.into()),
243 };
244
245 let (sender, receiver) = std::sync::mpsc::channel();
248 let tsender = sender.clone();
249
250 thread::spawn(move || sender.send(Some((f(&mut s), s))));
251 thread::spawn(move || {
252 thread::sleep(std::time::Duration::from_secs(5));
253 let _ = tsender.send(None);
254 });
255
256 match receiver.recv().unwrap() {
257 Some((r, s)) => {
258 self.s = Some(s);
259 r
260 }
261 None => Err(CodeError::KeyringTimeout.into()),
262 }
263 }
264}
265
266impl Default for ThreadKeyringStorage {
267 fn default() -> Self {
268 Self {
269 s: Some(KeyringStorage::default()),
270 }
271 }
272}
273
274impl StorageImplementation for ThreadKeyringStorage {
275 fn read(&mut self) -> Result<Option<StoredCredential>, AnyError> {
276 self.thread_op(|s| s.read())
277 }
278
279 fn store(&mut self, value: StoredCredential) -> Result<(), AnyError> {
280 self.thread_op(move |s| s.store(value))
281 }
282
283 fn clear(&mut self) -> Result<(), AnyError> {
284 self.thread_op(|s| s.clear())
285 }
286}
287
288#[derive(Default)]
289struct KeyringStorage {
290 entries: Vec<keyring::Entry>,
293}
294
295macro_rules! get_next_entry {
296 ($self: expr, $i: expr) => {
297 match $self.entries.get($i) {
298 Some(e) => e,
299 None => {
300 let e = keyring::Entry::new("vscode-cli", &format!("vscode-cli-{}", $i)).unwrap();
301 $self.entries.push(e);
302 $self.entries.last().unwrap()
303 }
304 }
305 };
306}
307
308impl StorageImplementation for KeyringStorage {
309 fn read(&mut self) -> Result<Option<StoredCredential>, AnyError> {
310 let mut str = String::new();
311
312 for i in 0.. {
313 let entry = get_next_entry!(self, i);
314 let next_chunk = match entry.get_password() {
315 Ok(value) => value,
316 Err(keyring::Error::NoEntry) => return Ok(None), Err(e) => return Err(wrap(e, "error reading keyring").into()),
318 };
319
320 if next_chunk.ends_with(CONTINUE_MARKER) {
321 str.push_str(&next_chunk[..next_chunk.len() - CONTINUE_MARKER.len()]);
322 } else {
323 str.push_str(&next_chunk);
324 break;
325 }
326 }
327
328 Ok(unseal(&str))
329 }
330
331 fn store(&mut self, value: StoredCredential) -> Result<(), AnyError> {
332 let sealed = seal(&value);
333 let step_size = KEYCHAIN_ENTRY_LIMIT - CONTINUE_MARKER.len();
334
335 for i in (0..sealed.len()).step_by(step_size) {
336 let entry = get_next_entry!(self, i / step_size);
337
338 let cutoff = i + step_size;
339 let stored = if cutoff <= sealed.len() {
340 let mut part = sealed[i..cutoff].to_string();
341 part.push_str(CONTINUE_MARKER);
342 entry.set_password(&part)
343 } else {
344 entry.set_password(&sealed[i..])
345 };
346
347 if let Err(e) = stored {
348 return Err(wrap(e, "error updating keyring").into());
349 }
350 }
351
352 Ok(())
353 }
354
355 fn clear(&mut self) -> Result<(), AnyError> {
356 self.read().ok(); for entry in self.entries.iter() {
358 entry
359 .delete_password()
360 .map_err(|e| wrap(e, "error updating keyring"))?;
361 }
362 self.entries.clear();
363
364 Ok(())
365 }
366}
367
368struct FileStorage(PersistedState<Option<String>>);
369
370impl StorageImplementation for FileStorage {
371 fn read(&mut self) -> Result<Option<StoredCredential>, AnyError> {
372 Ok(self.0.load().and_then(|s| unseal(&s)))
373 }
374
375 fn store(&mut self, value: StoredCredential) -> Result<(), AnyError> {
376 self.0.save(Some(seal(&value))).map_err(|e| e.into())
377 }
378
379 fn clear(&mut self) -> Result<(), AnyError> {
380 self.0.save(None).map_err(|e| e.into())
381 }
382}
383
384impl Auth {
385 pub fn new(paths: &LauncherPaths, log: log::Logger) -> Auth {
386 Auth {
387 log,
388 client: reqwest::Client::new(),
389 file_storage_path: paths.root().join("token.json"),
390 storage: Arc::new(std::sync::Mutex::new(None)),
391 }
392 }
393
394 fn with_storage<T, F>(&self, op: F) -> T
395 where
396 F: FnOnce(&mut StorageWithLastRead) -> T,
397 {
398 let mut opt = self.storage.lock().unwrap();
399 if let Some(s) = opt.as_mut() {
400 return op(s);
401 }
402
403 #[cfg(not(target_os = "linux"))]
404 let mut keyring_storage = KeyringStorage::default();
405 #[cfg(target_os = "linux")]
406 let mut keyring_storage = ThreadKeyringStorage::default();
407 let mut file_storage = FileStorage(PersistedState::new_with_mode(
408 self.file_storage_path.clone(),
409 0o600,
410 ));
411
412 let native_storage_result = if std::env::var("VSCODE_CLI_USE_FILE_KEYCHAIN").is_ok()
413 || self.file_storage_path.exists()
414 {
415 Err(wrap("", "user prefers file storage").into())
416 } else {
417 keyring_storage.read()
418 };
419
420 let mut storage = match native_storage_result {
421 Ok(v) => StorageWithLastRead {
422 last_read: Cell::new(Ok(v)),
423 fallback_storage: Some(file_storage),
424 storage: Box::new(keyring_storage),
425 },
426 Err(e) => {
427 debug!(self.log, "Using file keychain storage due to: {}", e);
428 StorageWithLastRead {
429 last_read: Cell::new(
430 file_storage
431 .read()
432 .map_err(|e| wrap(e, "could not read from file storage")),
433 ),
434 fallback_storage: None,
435 storage: Box::new(file_storage),
436 }
437 }
438 };
439
440 let out = op(&mut storage);
441 *opt = Some(storage);
442 out
443 }
444
445 pub async fn get_tunnel_authentication(&self) -> Result<Authorization, AnyError> {
447 let cred = self.get_credential().await?;
448 let auth = match cred.provider {
449 AuthProvider::Microsoft => Authorization::Bearer(cred.access_token),
450 AuthProvider::Github => Authorization::Github(format!(
451 "client_id={} {}",
452 cred.provider.client_id(),
453 cred.access_token
454 )),
455 };
456
457 Ok(auth)
458 }
459
460 pub fn get_current_credential(&self) -> Result<Option<StoredCredential>, WrappedError> {
462 self.with_storage(|storage| {
463 let value = storage.last_read.replace(Ok(None));
464 storage.last_read.set(value.clone());
465 value
466 })
467 }
468
469 pub fn clear_credentials(&self) -> Result<(), AnyError> {
471 self.with_storage(|storage| {
472 storage.storage.clear()?;
473 storage.last_read.set(Ok(None));
474 Ok(())
475 })
476 }
477
478 pub async fn login(
480 &self,
481 provider: Option<AuthProvider>,
482 access_token: Option<String>,
483 ) -> Result<StoredCredential, AnyError> {
484 let provider = match provider {
485 Some(p) => p,
486 None => self.prompt_for_provider().await?,
487 };
488
489 let credentials = match access_token {
490 Some(t) => StoredCredential {
491 provider,
492 access_token: t,
493 refresh_token: None,
494 expires_at: None,
495 },
496 None => self.do_device_code_flow_with_provider(provider).await?,
497 };
498
499 self.store_credentials(credentials.clone());
500 Ok(credentials)
501 }
502
503 pub async fn get_credential(&self) -> Result<StoredCredential, AnyError> {
505 let entry = match self.get_current_credential() {
506 Ok(Some(old_creds)) => {
507 trace!(self.log, "Found token in keyring");
508 match self.maybe_refresh_token(&old_creds).await {
509 Ok(Some(new_creds)) => {
510 self.store_credentials(new_creds.clone());
511 new_creds
512 }
513 Ok(None) => old_creds,
514 Err(e) => {
515 info!(self.log, "error refreshing token: {}", e);
516 let new_creds = self
517 .do_device_code_flow_with_provider(old_creds.provider)
518 .await?;
519 self.store_credentials(new_creds.clone());
520 new_creds
521 }
522 }
523 }
524
525 Ok(None) => {
526 trace!(self.log, "No token in keyring, getting a new one");
527 let creds = self.do_device_code_flow().await?;
528 self.store_credentials(creds.clone());
529 creds
530 }
531
532 Err(e) => {
533 warning!(
534 self.log,
535 "Error reading token from keyring, getting a new one: {}",
536 e
537 );
538 let creds = self.do_device_code_flow().await?;
539 self.store_credentials(creds.clone());
540 creds
541 }
542 };
543
544 Ok(entry)
545 }
546
547 fn store_credentials(&self, creds: StoredCredential) {
549 self.with_storage(|storage| {
550 if let Err(e) = storage.storage.store(creds.clone()) {
551 warning!(
552 self.log,
553 "Failed to update keyring with new credentials: {}",
554 e
555 );
556
557 if let Some(fb) = storage.fallback_storage.take() {
558 storage.storage = Box::new(fb);
559 match storage.storage.store(creds.clone()) {
560 Err(e) => {
561 warning!(self.log, "Also failed to update fallback storage: {}", e)
562 }
563 Ok(_) => debug!(self.log, "Updated fallback storage successfully"),
564 }
565 }
566 }
567
568 storage.last_read.set(Ok(Some(creds)));
569 })
570 }
571
572 async fn maybe_refresh_token(
575 &self,
576 creds: &StoredCredential,
577 ) -> Result<Option<StoredCredential>, AnyError> {
578 if !creds.is_expired(&self.log, &self.client).await {
579 return Ok(None);
580 }
581
582 self.do_refresh_token(creds).await
583 }
584
585 async fn do_refresh_token(
588 &self,
589 creds: &StoredCredential,
590 ) -> Result<Option<StoredCredential>, AnyError> {
591 match &creds.refresh_token {
592 Some(t) => self
593 .do_grant(
594 creds.provider,
595 format!(
596 "client_id={}&grant_type=refresh_token&refresh_token={}",
597 creds.provider.client_id(),
598 t
599 ),
600 )
601 .await
602 .map(Some),
603 None => match creds.provider {
604 AuthProvider::Github => self.touch_github_token(creds).await.map(|_| None),
605 _ => Err(RefreshTokenNotAvailableError().into()),
606 },
607 }
608 }
609
610 async fn do_grant(
612 &self,
613 provider: AuthProvider,
614 body: String,
615 ) -> Result<StoredCredential, AnyError> {
616 let response = self
617 .client
618 .post(provider.grant_uri())
619 .body(body)
620 .header("Accept", "application/json")
621 .send()
622 .await?;
623
624 let status_code = response.status().as_u16();
625 let body = response.bytes().await?;
626 if let Ok(body) = serde_json::from_slice::<AuthenticationResponse>(&body) {
627 return Ok(StoredCredential::from_response(body, provider));
628 }
629
630 Err(Auth::handle_grant_error(
631 provider.grant_uri(),
632 status_code,
633 body,
634 ))
635 }
636
637 async fn touch_github_token(&self, credential: &StoredCredential) -> Result<(), AnyError> {
641 let response = get_github_user(&self.client, &credential.access_token).await?;
642 if response.status().is_success() {
643 return Ok(());
644 }
645
646 let status_code = response.status().as_u16();
647 let body = response.bytes().await?;
648 Err(Auth::handle_grant_error(
649 GH_USER_ENDPOINT,
650 status_code,
651 body,
652 ))
653 }
654
655 fn handle_grant_error(url: &str, status_code: u16, body: bytes::Bytes) -> AnyError {
656 if let Ok(res) = serde_json::from_slice::<AuthenticationError>(&body) {
657 return OAuthError {
658 error: res.error,
659 error_description: res.error_description,
660 }
661 .into();
662 }
663
664 return StatusError {
665 body: String::from_utf8_lossy(&body).to_string(),
666 status_code,
667 url: url.to_string(),
668 }
669 .into();
670 }
671 async fn do_device_code_flow(&self) -> Result<StoredCredential, AnyError> {
673 let provider = self.prompt_for_provider().await?;
674 self.do_device_code_flow_with_provider(provider).await
675 }
676
677 async fn prompt_for_provider(&self) -> Result<AuthProvider, AnyError> {
678 if !*IS_INTERACTIVE_CLI {
679 info!(
680 self.log,
681 "Using Github for authentication, run `{} tunnel user login --provider <provider>` option to change this.",
682 APPLICATION_NAME
683 );
684 return Ok(AuthProvider::Github);
685 }
686
687 let provider = prompt_options(
688 format!("How would you like to log in to {}?", PRODUCT_NAME_LONG),
689 &[AuthProvider::Microsoft, AuthProvider::Github],
690 )?;
691
692 Ok(provider)
693 }
694
695 async fn do_device_code_flow_with_provider(
696 &self,
697 provider: AuthProvider,
698 ) -> Result<StoredCredential, AnyError> {
699 loop {
700 let init_code = self
701 .client
702 .post(provider.code_uri())
703 .header("Accept", "application/json")
704 .body(format!(
705 "client_id={}&scope={}",
706 provider.client_id(),
707 provider.get_default_scopes(),
708 ))
709 .send()
710 .await?;
711
712 if !init_code.status().is_success() {
713 return Err(StatusError::from_res(init_code).await?.into());
714 }
715
716 let init_code_json = init_code.json::<DeviceCodeResponse>().await?;
717 let expires_at = Utc::now() + chrono::Duration::seconds(init_code_json.expires_in);
718
719 match &init_code_json.message {
720 Some(m) => self.log.result(m),
721 None => self.log.result(&format!(
722 "To grant access to the server, please log into {} and use code {}",
723 init_code_json.verification_uri, init_code_json.user_code
724 )),
725 };
726
727 let body = format!(
728 "client_id={}&grant_type=urn:ietf:params:oauth:grant-type:device_code&device_code={}",
729 provider.client_id(),
730 init_code_json.device_code
731 );
732
733 let mut interval_s = 5;
734 while Utc::now() < expires_at {
735 sleep(std::time::Duration::from_secs(interval_s)).await;
736
737 match self.do_grant(provider, body.clone()).await {
738 Ok(creds) => return Ok(creds),
739 Err(AnyError::OAuthError(e)) if e.error == "slow_down" => {
740 interval_s += 5; trace!(self.log, "refresh poll failed, slowing down");
742 }
743 Err(AnyError::StatusError(e)) if e.status_code == 429 => {
745 interval_s += 5; trace!(self.log, "refresh poll failed, slowing down");
747 }
748 Err(e) => {
749 trace!(self.log, "refresh poll failed, retrying: {}", e);
750 }
751 }
752 }
753 }
754 }
755
756 pub async fn keep_token_alive(self) -> Result<(), AnyError> {
760 let this = self.clone();
761 let default_refresh = std::time::Duration::from_secs(60 * 60);
762 let min_refresh = std::time::Duration::from_secs(10);
763
764 let mut credential = this.get_credential().await?;
765 let mut last_did_error = false;
766 loop {
767 let sleep_time = if last_did_error {
768 min_refresh
769 } else {
770 match credential.expires_at {
771 Some(d) => ((d - Utc::now()) * 2 / 3).to_std().unwrap_or(min_refresh),
772 None => default_refresh,
773 }
774 };
775
776 tokio::time::sleep(sleep_time.max(min_refresh)).await;
778
779 match this.do_refresh_token(&credential).await {
780 Err(AnyError::StatusError(e)) if e.status_code >= 400 && e.status_code < 500 => {
782 error!(this.log, "failed to keep token alive: {:?}", e);
783 return Err(e.into());
784 }
785 Err(AnyError::RefreshTokenNotAvailableError(_)) => {
786 return Ok(());
787 }
788 Err(e) => {
789 warning!(this.log, "error refreshing token: {:?}", e);
790 last_did_error = true;
791 continue;
792 }
793 Ok(c) => {
794 trace!(this.log, "token was successfully refreshed in keepalive");
795 last_did_error = false;
796 if let Some(c) = c {
797 this.store_credentials(c.clone());
798 credential = c;
799 }
800 }
801 }
802 }
803 }
804}
805
806#[async_trait]
807impl AuthorizationProvider for Auth {
808 async fn get_authorization(&self) -> Result<Authorization, HttpError> {
809 self.get_tunnel_authentication()
810 .await
811 .map_err(|e| HttpError::AuthorizationError(e.to_string()))
812 }
813}
814
815lazy_static::lazy_static! {
816 static ref HOSTNAME: Vec<u8> = gethostname().to_string_lossy().bytes().collect();
817}
818
819#[cfg(feature = "vscode-encrypt")]
820fn encrypt(value: &str) -> String {
821 vscode_encrypt::encrypt(&HOSTNAME, value.as_bytes()).expect("expected to encrypt")
822}
823
824#[cfg(feature = "vscode-encrypt")]
825fn decrypt(value: &str) -> Option<String> {
826 let b = vscode_encrypt::decrypt(&HOSTNAME, value).ok()?;
827 String::from_utf8(b).ok()
828}
829
830#[cfg(not(feature = "vscode-encrypt"))]
831fn encrypt(value: &str) -> String {
832 value.to_owned()
833}
834
835#[cfg(not(feature = "vscode-encrypt"))]
836fn decrypt(value: &str) -> Option<String> {
837 Some(value.to_owned())
838}