Skip to main content

git_xcrypt/commands/
unlock.rs

1//! `git-xcrypt unlock` — make a cloned repository readable again.
2//!
3//! This is the command PRD US-01 is about: the code is on the new machine, the
4//! secrets are not, and one key file has to turn ciphertext in the working tree
5//! back into the bytes that were committed.
6//!
7//! Three properties shape the implementation.
8//!
9//! **The registration comes before the decryption.** `.git/config` is not
10//! versioned, so a clone has no driver; and the `* filter=git-xcrypt` line in
11//! `.gitattributes` is only there if whoever set the repository up committed
12//! that file. Both are repaired here, because git treats a missing attribute and
13//! an undefined driver identically — as no filter. Decrypting first would leave
14//! a window in which the working tree holds plaintext and git has no filter,
15//! where `git status` reports every secret as modified and the next `git add`
16//! stores it in the clear.
17//!
18//! **A wrong key changes nothing at all.** Every encrypted file is inspected —
19//! 38 bytes each, no decryption — before a single byte is written, and before
20//! the key is even installed. Discovering the mismatch on the fourth file out of
21//! ten would leave a working tree that is half readable and a repository holding
22//! a key that does not belong to it. The limit of that promise is worth naming:
23//! the check can only object to a key it has evidence against, so a working tree
24//! with no encrypted file in it accepts any key. That case gets a warning rather
25//! than a refusal, because proving it would mean scanning history, which is
26//! `status`'s job in S-06.
27//!
28//! **Interrupting it is survivable.** The files are converted in place, one at a
29//! time, so a run cut short leaves some plain and some not. That is recoverable
30//! only because each file says what it is in its own header: a second `unlock`
31//! skips what is already plain and finishes the rest. Working from the object
32//! database instead would have been no safer and would have missed every file
33//! that is not committed yet.
34//!
35//! Which files get decrypted is decided by the **header**, not by `.git-xcrypt`
36//! — the same rule the smudge path follows, and for the same reason. It is also
37//! what makes the result byte-identical to a checkout, which is what `git
38//! status` being clean afterwards actually proves.
39
40use std::fs;
41use std::io::Read as _;
42use std::path::{Path, PathBuf};
43
44use zeroize::Zeroizing;
45
46use crate::crypto::format::{self, Header, KEY_ID_LEN, OVERHEAD};
47use crate::crypto::keyfile;
48use crate::git::config as gitconfig;
49use crate::git::repo::{Repo, git_spelling};
50use crate::rules::decide;
51use crate::rules::declaration::Config;
52use crate::{Error, Result};
53
54/// What `unlock` did.
55#[derive(Debug)]
56pub struct Report {
57    /// Fingerprint of the key the repository now holds.
58    pub key_id: [u8; KEY_ID_LEN],
59    /// A key file was written. False when the key was already in place.
60    pub key_imported: bool,
61    /// The filter registration was written or repaired.
62    pub config_written: bool,
63    /// The managed `.gitattributes` section was written or repaired.
64    pub attributes_written: bool,
65    /// Paths, relative to the working tree, that were converted.
66    pub decrypted: Vec<PathBuf>,
67    /// Paths that could not be read, so may still be encrypted.
68    ///
69    /// Separate from [`Report::warnings`] because the count belongs in the
70    /// closing line: "decrypted 3 files" and "decrypted 3 files, 1 could not be
71    /// read" are different outcomes and must not look the same.
72    pub unreadable: Vec<PathBuf>,
73    /// Anything worth saying once, carried out so the binary owns the messages.
74    pub warnings: Vec<String>,
75}
76
77/// Where the key comes from, when one is offered at all.
78///
79/// A type rather than two `Option`s so the two cannot both be set, and so the
80/// call site reads as the choice it is. Both go through the same parser and the
81/// same refusals; only the reading differs.
82#[derive(Debug, Clone, Copy)]
83pub enum KeySource<'a> {
84    /// A file written by `export-key`.
85    File(&'a Path),
86    /// The text of such a file, handed over directly.
87    ///
88    /// **Visible in the process list for as long as the command runs, and kept
89    /// for ever in the shell's history** — measured on macOS: `ps -ww -o command
90    /// -p <pid>` prints the material verbatim. That is the price of the one
91    /// thing a file cannot do, which is arrive from a CI secret without ever
92    /// being written to disk, and it is the caller's to pay knowingly. The
93    /// binary says so on `stderr` every time.
94    Material(&'a str),
95}
96
97/// Unlocks `repo`, optionally installing the key at `key_source` first.
98///
99/// With `key_only` the working tree is left exactly as it is: the key goes in,
100/// the filter and the managed section are repaired, and nothing is decrypted.
101/// That was a command of its own until 2026-08-06 — `import-key` — and it is a
102/// flag now because the two differed by this one step and by nothing else,
103/// while `unlock <key-file>` was already the path every message pointed at.
104/// The evidence check still runs, so a key the working tree's own headers
105/// contradict is refused here exactly as it is on the full path.
106///
107/// # Errors
108///
109/// [`Error::NoKey`] when no key is given and none is present. [`Error::Config`]
110/// when `.git-xcrypt` cannot be understood, or when the repository already holds
111/// a key other than the one offered — note that this second case is code `2`
112/// rather than the `4` a file-level mismatch reports, because the refusal comes
113/// from the repository's own key file and not from anything a header said.
114/// [`Error::Format`] when a file in the working tree belongs to another key.
115/// [`Error::Io`] on a read or write failure.
116pub fn run(repo: &Repo, key_source: Option<KeySource<'_>>, key_only: bool) -> Result<Report> {
117    let key = match key_source {
118        Some(source) => {
119            let key = match source {
120                KeySource::File(path) => keyfile::read_portable(path)?,
121                // The same parser, so the header still verifies the material
122                // behind it: a key truncated on its way through a clipboard or
123                // a CI variable is refused rather than installed.
124                KeySource::Material(text) => keyfile::decode_portable(text)?,
125            };
126            // Asked before anything is written: a refusal that has already
127            // installed a key has not refused.
128            refuse_on_conflict(repo, &key)?;
129            key
130        }
131        None => repo.load_key()?,
132    };
133    let key_id = key.key_id();
134
135    // Everything that must be readable before anything is written. `.git-xcrypt`
136    // is loaded here rather than after the key is installed, so a typo in it
137    // cannot leave a key behind on its way out.
138    let config = Config::load(&repo.xcrypt_config_path())?;
139    let git_config = gitconfig::open_full(repo.git_dir(), repo.common_dir())?;
140    let autocrlf = gitconfig::get(&git_config, "core.autocrlf");
141    let core_eol = gitconfig::get(&git_config, "core.eol");
142
143    // Everything carrying our magic, and the key each one asks for. Gathered
144    // before the first write, so a mismatch costs nothing.
145    let mut walk = Walk::default();
146    let encrypted = collect_encrypted(repo, &mut walk)?;
147    refuse_foreign_keys(repo, &encrypted, &key_id)?;
148
149    let key_imported = install(repo, &key)?;
150    // Both before the decryption, never after — see the module comment. The
151    // attributes section matters as much as the registration: a driver with no
152    // `* filter=git-xcrypt` above it is never invoked, so git would store the
153    // plaintext this command is about to put in the working tree, with exit
154    // code 0 and no signal. Measured on git 2.55 in a clone whose origin never
155    // committed `.gitattributes`.
156    let config_written = super::init::register_driver(repo)?;
157    let attributes_written = crate::git::attributes::write_section(
158        &repo.attributes_path(),
159        // Whichever spelling is already there: repairing this section must not
160        // silently undo a `sync --ignorecase`.
161        &crate::git::attributes::render_lines_as_written(&repo.attributes_path(), &config),
162    )?;
163
164    let mut report = Report {
165        key_id,
166        key_imported,
167        config_written,
168        attributes_written,
169        decrypted: Vec::new(),
170        unreadable: walk.unreadable,
171        warnings: config.pointless_eol.clone(),
172    };
173    report.warnings.append(&mut walk.warnings);
174
175    if config.missing {
176        // Not an error here — the headers say everything decryption needs — but
177        // the check-in path treats the same state as fatal, so without this the
178        // command would report success and leave a tree in which every `git add`
179        // aborts.
180        report.warnings.push(format!(
181            "{} is missing, so every `git add` in this repository will refuse \
182             until it is restored; run `git-xcrypt init` to create one",
183            crate::git::repo::CONFIG_FILE
184        ));
185    }
186
187    if key_imported && encrypted.is_empty() {
188        // The check above can only object to a key it has evidence against, and
189        // an empty working tree offers none. Saying so is the honest version of
190        // "a wrong key changes nothing": nothing was changed, but nothing
191        // confirmed the key either, and committing under the wrong one would
192        // split the repository's history across two keys.
193        report.warnings.push(format!(
194            "no encrypted file was found here, so nothing confirmed that key {} \
195             is this repository's. Run `git-xcrypt status` once the secrets are \
196             checked out.",
197            crate::format_key_id(&key_id)
198        ));
199    }
200    if key_only {
201        // Everything above is "put this repository in a state where git filters
202        // it"; everything below is "and now write the plain text out". Stopping
203        // here is the whole difference, and it is deliberately *after* the
204        // evidence check and both repairs: a key handed to a repository whose
205        // filter is not registered is not a safe place to leave anyone, whether
206        // or not the tree was decrypted on the way.
207        return Ok(report);
208    }
209
210    // The same paths, spelled the way the index stores them.
211    let mut rewritten: Vec<Vec<u8>> = Vec::new();
212
213    // **The loop stops at the first failure, but does not return from here.**
214    // Every step below used to be a bare `?`, which dropped the whole report
215    // together with the list of files already decrypted — and with it the stat
216    // refresh underneath. Measured, on a clone whose second declared file sat in
217    // a directory the user could not write: the first file was decrypted, the
218    // message was `i/o failure: Permission denied (os error 13)` naming nothing,
219    // and `git status` reported the decrypted file as modified for good, because
220    // a later run finds it already in the clear and so never refreshes it.
221    let mut stopped = None;
222    for file in &encrypted {
223        let relative = relative_to(repo, &file.path);
224        let name = repo_relative_bytes(&relative);
225        let content = match fs::read(&file.path) {
226            Ok(content) => content,
227            Err(err) => {
228                stopped = Some(named_io(&relative, "read", &err));
229                break;
230            }
231        };
232        let decision = config.decide(&name);
233
234        // The very function the smudge path calls, on purpose: anything else
235        // here would be a second implementation of line-ending handling, and the
236        // two would drift into a working tree git reports as modified.
237        let outcome = match decide::smudge(
238            Some(&key),
239            &name,
240            &content,
241            decision.encrypt,
242            decision.eol,
243            autocrlf.as_deref(),
244            core_eol.as_deref(),
245        ) {
246            Ok(outcome) => outcome,
247            Err(err) => {
248                stopped = Some(Error::Format(format!("{}: {err}", git_spelling(&relative))));
249                break;
250            }
251        };
252
253        if let Some(warning) = outcome.warning {
254            report.warnings.push(warning);
255        }
256
257        // Zeroizing: this is the secret, now in the clear on the heap.
258        let plaintext = Zeroizing::new(outcome.content);
259        if *plaintext == content {
260            // Unreachable for anything `collect_encrypted` yields — ciphertext
261            // is 38 bytes longer than its plaintext, so the two can never be
262            // equal. Skipping what is already plain happens one level up, in the
263            // walk; this is only here so a write can never be a no-op.
264            continue;
265        }
266        // Atomic, and inheriting the file's own mode, so an interruption cannot
267        // leave a half-written secret and an executable stays executable.
268        match crate::util::atomic::write(&file.path, &plaintext) {
269            Ok(()) => {}
270            Err(Error::Io(err)) => {
271                stopped = Some(named_io(&relative, "replace", &err));
272                break;
273            }
274            Err(err) => {
275                stopped = Some(err);
276                break;
277            }
278        }
279        rewritten.push(name);
280        report.decrypted.push(relative);
281    }
282
283    // Last, and not optional: without it git compares the new size against the
284    // one it cached for the ciphertext, concludes the file changed and never
285    // runs the filter to find out otherwise. `git status` would then report
286    // every unlocked secret as modified, for good. See `crate::git::index`.
287    //
288    // Run even when the loop stopped, and that is the point: the files already
289    // rewritten are the ones whose cached size is now wrong, and no later run
290    // will come back for them — they are plain text by then, so the walk does
291    // not select them at all.
292    let refreshed = crate::git::index::forget_stat(
293        &repo.git_dir().join("index"),
294        crate::git::index::object_hash(
295            gitconfig::get(&git_config, "extensions.objectformat").as_deref(),
296        ),
297        &rewritten,
298    );
299    match refreshed {
300        Ok(crate::git::index::Outcome::Cleared(_)) => {}
301        Ok(crate::git::index::Outcome::Skipped(why)) => report.warnings.push(why),
302        // A warning, not a return: the decryption already happened, and a bare
303        // `Err` here threw the whole report away — the user was never told that
304        // N files now sit in the clear, and a second run cannot say it either,
305        // because the files are plain by then and the walk no longer selects
306        // them. `Skipped` (a held lock, a split index) already answers the
307        // identical situation with a warning carrying the remedy; a failed read
308        // or write differs only in the errno. The report's own decrypted list
309        // is the load-bearing half — what changed on disk must reach the user
310        // whatever the stat cache did.
311        Err(err) if stopped.is_none() => report.warnings.push(format!(
312            "the index's stat cache could not be refreshed ({err}). The files \
313             are decrypted correctly; if `git status` shows them as modified, \
314             `git add --renormalize .` settles it."
315        )),
316        // A second failure on top of the one that stopped the loop. The first is
317        // what the user has to act on; this one goes with it rather than
318        // replacing it.
319        Err(err) => report.warnings.push(err.to_string()),
320    }
321
322    if let Some(err) = stopped {
323        return Err(interrupted(&report, &encrypted, err));
324    }
325
326    Ok(report)
327}
328
329/// Refuses when the repository already holds a key that is not this one.
330///
331/// Separate from [`install`] because this question has to be asked before
332/// anything at all is written: a refusal that has already installed a key has
333/// not refused.
334///
335/// # Errors
336///
337/// [`Error::Config`] for a different key, [`Error::Format`] when the key file
338/// already in the repository cannot be read.
339fn refuse_on_conflict(repo: &Repo, key: &crate::crypto::key::MasterKey) -> Result<()> {
340    match repo.load_key() {
341        Ok(existing) if existing.key_id() == key.key_id() => Ok(()),
342        Ok(existing) => Err(Error::Config(format!(
343            "this repository already holds key {}, and that file offers key {}.\n\
344             Replacing it would make every file encrypted so far impossible to read, for good.\n\
345             If you really mean to change keys, remove {} deliberately first.",
346            crate::format_key_id(&existing.key_id()),
347            crate::format_key_id(&key.key_id()),
348            repo.key_path().display()
349        ))),
350        Err(Error::NoKey) => Ok(()),
351        // A key file we cannot parse is not evidence of absence. Naming it is
352        // the whole repair the user needs.
353        Err(Error::Format(message)) => Err(Error::Format(format!(
354            "{}: {message}",
355            repo.key_path().display()
356        ))),
357        Err(other) => Err(other),
358    }
359}
360
361/// Writes `key` into the repository, reporting whether it had to.
362///
363/// Only correct after [`refuse_on_conflict`] has passed: on its own it would
364/// treat a *different* key already in place as "nothing to do".
365///
366/// # Errors
367///
368/// [`Error::Io`] when the key file cannot be written.
369fn install(repo: &Repo, key: &crate::crypto::key::MasterKey) -> Result<bool> {
370    if repo.has_key() {
371        return Ok(false);
372    }
373    keyfile::write(&repo.key_path(), key)?;
374    Ok(true)
375}
376
377/// Puts a path and the operation in front of a bare I/O failure.
378///
379/// `Permission denied (os error 13)` names neither the file nor what was being
380/// done to it, which for a command part way through rewriting a working tree is
381/// the least useful message it could produce. Measured before this: a `unlock`
382/// stopped by one unwritable directory said exactly that and nothing else.
383fn named_io(relative: &Path, action: &str, err: &std::io::Error) -> Error {
384    Error::Io(std::io::Error::other(format!(
385        "{}: could not {action} it ({err})",
386        git_spelling(relative)
387    )))
388}
389
390/// Adds what was already done to an error that stopped the decryption pass.
391///
392/// The bare error drops the report, and with it the only record that part of the
393/// working tree is now in the clear and part of it is not. The same shape `lock`
394/// uses for the same reason — and, unlike `lock`, this one has to say that a
395/// second run will *not* revisit what already succeeded, because a file in the
396/// clear no longer carries the magic the walk selects on.
397fn interrupted(report: &Report, encrypted: &[Encrypted], err: Error) -> Error {
398    let done = report.decrypted.len();
399    let left = encrypted.len().saturating_sub(done);
400    let context = format!(
401        "\nunlock stopped part way: {done} file(s) are now in the clear and {left} \
402         are still encrypted. The key is in place, so running unlock again picks up \
403         the rest once the cause above is fixed."
404    );
405    match err {
406        Error::Format(message) => Error::Format(message + &context),
407        Error::Crypto(message) => Error::Crypto(message + &context),
408        Error::Config(message) => Error::Config(message + &context),
409        Error::Io(err) => Error::Io(std::io::Error::other(format!("{err}{context}"))),
410        other => other,
411    }
412}
413
414/// A working-tree file that carries our magic, and the header it carries.
415#[derive(Debug)]
416pub(super) struct Encrypted {
417    path: PathBuf,
418    header: Header,
419}
420
421/// Every encrypted file in the working tree, in a stable order.
422///
423/// Only the first 38 bytes of each file are read, so the cost is one open per
424/// file rather than one full read — the same reasoning that lets `status` scan a
425/// whole history cheaply. The walk is otherwise exhaustive: it has no notion of
426/// `.gitignore`, so it does descend `target/` and `node_modules/`. That is the
427/// price of deciding by header, and it buys the case that matters — an encrypted
428/// file that no current pattern selects still gets decrypted, exactly as a
429/// checkout would decrypt it.
430///
431/// Untracked files are included for the same reason, and the bootstrap
432/// exclusions (`.gitattributes`, `.git-xcrypt`) are not consulted: a file
433/// carrying our magic is one of ours whatever its name, and leaving it as
434/// ciphertext would be the surprise.
435///
436/// A path that cannot be read becomes a warning rather than a failure. One
437/// root-owned build artefact must not be able to stop a user recovering their
438/// secrets, and skipping a file only ever means leaving it encrypted — but the
439/// skipped paths are counted and reported, because "decrypted everything" and
440/// "decrypted what it could" must not read the same.
441///
442/// Symbolic links are left alone: following one would write outside the
443/// repository, and replacing it would destroy the link.
444///
445/// **A directory holding a `.git` entry is another repository and is not
446/// entered.** Skipping the entry named `.git` is not enough — that leaves the
447/// submodule's *working tree* in the walk, and a submodule encrypted with its
448/// own key then makes the parent's `unlock` fail with a key mismatch it cannot
449/// be talked out of, having decrypted nothing. Measured. A submodule has its own
450/// configuration, its own key and its own index; it needs its own `unlock`.
451pub(super) fn collect_encrypted(repo: &Repo, walk: &mut Walk) -> Result<Vec<Encrypted>> {
452    let mut found = Vec::new();
453    let mut pending = vec![repo.work_tree().to_path_buf()];
454
455    while let Some(directory) = pending.pop() {
456        let entries = match fs::read_dir(&directory) {
457            Ok(entries) => entries,
458            Err(err) => {
459                walk.warnings
460                    .push(format!("{}: not searched ({err})", directory.display()));
461                continue;
462            }
463        };
464
465        for entry in entries {
466            let entry = match entry {
467                Ok(entry) => entry,
468                Err(err) => {
469                    walk.warnings
470                        .push(format!("{}: not searched ({err})", directory.display()));
471                    continue;
472                }
473            };
474            if entry.file_name() == ".git" {
475                continue;
476            }
477
478            let path = entry.path();
479            let Ok(metadata) = fs::symlink_metadata(&path) else {
480                walk.warnings
481                    .push(format!("{}: skipped, it could not be read", path.display()));
482                walk.unreadable.push(relative_to(repo, &path));
483                continue;
484            };
485            if metadata.is_symlink() {
486                continue;
487            }
488            if metadata.is_dir() {
489                if path.join(".git").exists() {
490                    walk.warnings.push(format!(
491                        "{}: a repository of its own, left to its own `git-xcrypt unlock`",
492                        git_spelling(&relative_to(repo, &path))
493                    ));
494                } else {
495                    pending.push(path);
496                }
497                continue;
498            }
499            if !metadata.is_file() {
500                continue;
501            }
502
503            match peek_header(&path) {
504                // A file whose header will not parse is one of ours and broken;
505                // that has to stop the run, unlike a file we simply cannot open.
506                Ok(Some(header)) => found.push(Encrypted { path, header }),
507                Ok(None) => {}
508                Err(Error::Io(err)) => {
509                    walk.warnings
510                        .push(format!("{}: skipped ({err})", path.display()));
511                    walk.unreadable.push(relative_to(repo, &path));
512                }
513                Err(err) => return Err(err),
514            }
515        }
516    }
517
518    found.sort_by(|left, right| left.path.cmp(&right.path));
519    Ok(found)
520}
521
522/// What the walk noticed on its way through, besides the files it found.
523#[derive(Debug, Default)]
524pub(super) struct Walk {
525    /// Paths that could not be read, so may still hold ciphertext.
526    unreadable: Vec<PathBuf>,
527    /// Messages for the user, one per thing skipped.
528    pub(super) warnings: Vec<String>,
529}
530
531/// A path relative to the working tree, or the path itself if it is outside.
532fn relative_to(repo: &Repo, path: &Path) -> PathBuf {
533    repo.relative(path)
534        .map_or_else(|| path.to_path_buf(), Path::to_path_buf)
535}
536
537/// Reads the header of `path`, or `None` when the file is not one of ours.
538///
539/// A file that starts with our magic but is too short to hold a header is an
540/// error rather than a shrug: it is a truncated encrypted file, and carrying on
541/// would mean deciding it is plaintext.
542fn peek_header(path: &Path) -> Result<Option<Header>> {
543    let mut file = fs::File::open(path)?;
544    let mut prefix = [0u8; OVERHEAD];
545    let read = fill(&mut file, &mut prefix)?;
546    let prefix = &prefix[..read];
547
548    if !format::looks_encrypted(prefix) {
549        return Ok(None);
550    }
551
552    Header::parse(prefix)
553        .map(Some)
554        .map_err(|err| Error::Format(format!("{}: {err}", path.display())))
555}
556
557/// Reads until `buffer` is full or the file ends, returning how much arrived.
558///
559/// `Interrupted` is retried rather than reported, the way `std`'s own readers
560/// do: a signal arriving during a 38-byte read is not a reason to abandon a
561/// user's repository half unlocked.
562fn fill(file: &mut fs::File, buffer: &mut [u8]) -> std::io::Result<usize> {
563    let mut filled = 0;
564    while filled < buffer.len() {
565        match file.read(&mut buffer[filled..]) {
566            Ok(0) => break,
567            Ok(read) => filled += read,
568            Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {}
569            Err(err) => return Err(err),
570        }
571    }
572    Ok(filled)
573}
574
575/// Refuses when any file belongs to a key other than the one offered.
576///
577/// Deliberately an [`Error::Format`] rather than [`Error::KeyMismatch`]: both
578/// report exit code 4, and this one can name the file, which is what turns
579/// "authentication failed" into an instruction.
580pub(super) fn refuse_foreign_keys(
581    repo: &Repo,
582    encrypted: &[Encrypted],
583    key_id: &[u8; KEY_ID_LEN],
584) -> Result<()> {
585    for file in encrypted {
586        if file.header.key_id == *key_id {
587            continue;
588        }
589
590        let relative = relative_to(repo, &file.path);
591        return Err(Error::Format(format!(
592            "{} was encrypted with key {}, but the key offered here is {}.\n\
593             Nothing has been changed. Unlock this repository with the key whose id is {}.",
594            git_spelling(&relative),
595            crate::format_key_id(&file.header.key_id),
596            crate::format_key_id(key_id),
597            crate::format_key_id(&file.header.key_id)
598        )));
599    }
600    Ok(())
601}
602
603/// A repository-relative path as the pattern matcher expects it.
604///
605/// Bytes rather than text, and forward slashes: on Unix a path is an arbitrary
606/// byte string, and decoding it lossily would match a file under a name it does
607/// not have.
608fn repo_relative_bytes(relative: &Path) -> Vec<u8> {
609    #[cfg(unix)]
610    {
611        use std::os::unix::ffi::OsStrExt as _;
612        relative.as_os_str().as_bytes().to_vec()
613    }
614    #[cfg(not(unix))]
615    {
616        relative.to_string_lossy().replace('\\', "/").into_bytes()
617    }
618}