diff --git a/crates/rrg-proto/build.rs b/crates/rrg-proto/build.rs index 4910b2cd..d4ca74a5 100644 --- a/crates/rrg-proto/build.rs +++ b/crates/rrg-proto/build.rs @@ -16,7 +16,7 @@ const PROTOS: &'static [&'static str] = &[ "../../proto/rrg/winreg.proto", "../../proto/rrg/action/execute_signed_command.proto", "../../proto/rrg/action/get_file_contents.proto", - "../../proto/rrg/action/get_file_hash.proto", + "../../proto/rrg/action/get_file_sha256.proto", "../../proto/rrg/action/get_file_metadata.proto", "../../proto/rrg/action/get_filesystem_timeline.proto", "../../proto/rrg/action/get_filesystem_timeline_tsk.proto", diff --git a/crates/rrg/Cargo.toml b/crates/rrg/Cargo.toml index d1ac1b27..a2f0cb24 100644 --- a/crates/rrg/Cargo.toml +++ b/crates/rrg/Cargo.toml @@ -13,6 +13,7 @@ default = [ "action-get_file_metadata-sha1", "action-get_file_metadata-sha256", "action-get_file_contents", + "action-get_file_sha256", "action-grep_file_contents", "action-get_filesystem_timeline", "action-get_tcp_response", @@ -34,6 +35,7 @@ action-get_file_metadata-md5 = ["action-get_file_metadata", "dep:md-5"] action-get_file_metadata-sha1 = ["action-get_file_metadata", "dep:sha1"] action-get_file_metadata-sha256 = ["action-get_file_metadata", "dep:sha2"] action-get_file_contents = ["dep:sha2"] +action-get_file_sha256 = ["dep:sha2"] action-grep_file_contents = [] action-get_filesystem_timeline = ["dep:flate2", "dep:sha2"] action-get_filesystem_timeline_tsk = ["action-get_filesystem_timeline", "dep:tsk"] diff --git a/crates/rrg/src/action.rs b/crates/rrg/src/action.rs index ef380601..6c44072d 100644 --- a/crates/rrg/src/action.rs +++ b/crates/rrg/src/action.rs @@ -24,6 +24,9 @@ pub mod get_file_metadata; #[cfg(feature = "action-get_file_contents")] pub mod get_file_contents; +#[cfg(feature = "action-get_file_sha256")] +pub mod get_file_sha256; + #[cfg(feature = "action-grep_file_contents")] pub mod grep_file_contents; @@ -104,6 +107,10 @@ where GetFileContents => { handle(session, request, self::get_file_contents::handle) } + #[cfg(feature = "action-get_file_sha256")] + GetFileSha256 => { + handle(session, request, self::get_file_sha256::handle) + } #[cfg(feature = "action-grep_file_contents")] GrepFileContents => { handle(session, request, self::grep_file_contents::handle) diff --git a/crates/rrg/src/action/get_file_sha256.rs b/crates/rrg/src/action/get_file_sha256.rs new file mode 100644 index 00000000..bf60dc64 --- /dev/null +++ b/crates/rrg/src/action/get_file_sha256.rs @@ -0,0 +1,245 @@ +// Copyright 2025 Google LLC +// +// Use of this source code is governed by an MIT-style license that can be found +// in the LICENSE file or at https://opensource.org/licenses/MIT. +use std::path::PathBuf; + +/// Arguments of the `get_file_sha256` action. +pub struct Args { + /// Absolute path to the file to get the SHA-256 hash of. + path: PathBuf, + /// Byte offset from which the content should be hashed. + offset: u64, + /// Number of bytes to hash (from the start offset). + len: Option>, +} + +/// Result of the `get_file_sha256` action. +struct Item { + /// Absolute path of the file this result corresponds to. + path: PathBuf, + /// Byte offset from which the file content was hashed. + offset: u64, + /// Number of bytes of the file used to produce the hash. + len: u64, + /// SHA-256 hash digest of the file content. + sha256: [u8; 32], +} + +/// Handle invocations of the `get_file_sha256` action. +pub fn handle(session: &mut S, args: Args) -> crate::session::Result<()> +where + S: crate::session::Session, +{ + use std::io::{BufRead as _, Read as _, Seek as _}; + + let file = std::fs::File::open(&args.path) + .map_err(crate::session::Error::action)?; + let mut file = std::io::BufReader::new(file); + + file.seek(std::io::SeekFrom::Start(args.offset)) + .map_err(crate::session::Error::action)?; + + let mut file = file.take(match args.len { + Some(len) => u64::from(len), + None => u64::MAX, + }); + + use sha2::Digest as _; + let mut sha256 = sha2::Sha256::new(); + loop { + let buf = match file.fill_buf() { + Ok(buf) if buf.is_empty() => break, + Ok(buf) => buf, + Err(error) => return Err(crate::session::Error::action(error)), + }; + sha256.update(&buf[..]); + + let buf_len = buf.len(); + file.consume(buf_len); + } + let sha256 = <[u8; 32]>::from(sha256.finalize()); + + let len = file.stream_position() + .map_err(crate::session::Error::action)?; + + session.reply(Item { + path: args.path, + offset: args.offset, + len, + sha256, + })?; + + Ok(()) +} + +impl crate::request::Args for Args { + + type Proto = rrg_proto::get_file_sha256::Args; + + fn from_proto(mut proto: Self::Proto) -> Result { + use crate::request::ParseArgsError; + + let path = PathBuf::try_from(proto.take_path()) + .map_err(|error| ParseArgsError::invalid_field("path", error))?; + + Ok(Args { + path, + offset: proto.offset(), + len: std::num::NonZero::new(proto.length()), + }) + } +} + +impl crate::response::Item for Item { + + type Proto = rrg_proto::get_file_sha256::Result; + + fn into_proto(self) -> Self::Proto { + let mut proto = rrg_proto::get_file_sha256::Result::new(); + proto.set_path(self.path.into()); + proto.set_offset(self.offset); + proto.set_length(self.len); + proto.set_sha256(self.sha256.to_vec()); + + proto + } +} + +#[cfg(test)] +mod tests { + + use super::*; + + #[test] + fn handle_default() { + let mut tempfile = tempfile::NamedTempFile::new() + .unwrap(); + + use std::io::Write as _; + tempfile.as_file_mut().write_all(b"hello\n") + .unwrap(); + + let args = Args { + path: tempfile.path().to_path_buf(), + offset: 0, + len: None, + }; + + let mut session = crate::session::FakeSession::new(); + assert!(handle(&mut session, args).is_ok()); + + assert_eq!(session.reply_count(), 1); + + let item = session.reply::(0); + assert_eq!(item.path, tempfile.path()); + assert_eq!(item.offset, 0); + assert_eq!(item.len, u64::try_from(b"hello\n".len()).unwrap()); + assert_eq!(item.sha256, [ + // Pre-computed by the `sha256sum` tool. + 0x58, 0x91, 0xb5, 0xb5, 0x22, 0xd5, 0xdf, 0x08, + 0x6d, 0x0f, 0xf0, 0xb1, 0x10, 0xfb, 0xd9, 0xd2, + 0x1b, 0xb4, 0xfc, 0x71, 0x63, 0xaf, 0x34, 0xd0, + 0x82, 0x86, 0xa2, 0xe8, 0x46, 0xf6, 0xbe, 0x03, + ]); + } + + #[test] + fn handle_offset() { + let mut tempfile = tempfile::NamedTempFile::new() + .unwrap(); + + use std::io::Write as _; + tempfile.as_file_mut().write_all(b"hello\n") + .unwrap(); + + let args = Args { + path: tempfile.path().to_path_buf(), + offset: u64::try_from("".len()).unwrap(), + len: None, + }; + + let mut session = crate::session::FakeSession::new(); + assert!(handle(&mut session, args).is_ok()); + + assert_eq!(session.reply_count(), 1); + + let item = session.reply::(0); + assert_eq!(item.path, tempfile.path()); + assert_eq!(item.offset, u64::try_from(b"".len()).unwrap()); + assert_eq!(item.len, u64::try_from(b"hello\n".len()).unwrap()); + assert_eq!(item.sha256, [ + // Pre-computed by the `sha256sum` tool. + 0x58, 0x91, 0xb5, 0xb5, 0x22, 0xd5, 0xdf, 0x08, + 0x6d, 0x0f, 0xf0, 0xb1, 0x10, 0xfb, 0xd9, 0xd2, + 0x1b, 0xb4, 0xfc, 0x71, 0x63, 0xaf, 0x34, 0xd0, + 0x82, 0x86, 0xa2, 0xe8, 0x46, 0xf6, 0xbe, 0x03, + ]); + } + + #[test] + fn handle_len() { + let mut tempfile = tempfile::NamedTempFile::new() + .unwrap(); + + use std::io::Write as _; + tempfile.as_file_mut().write_all(b"hello\n") + .unwrap(); + + let args = Args { + path: tempfile.path().to_path_buf(), + offset: 0, + len: std::num::NonZero::new(b"hello\n".len().try_into().unwrap()), + }; + + let mut session = crate::session::FakeSession::new(); + assert!(handle(&mut session, args).is_ok()); + + assert_eq!(session.reply_count(), 1); + + let item = session.reply::(0); + assert_eq!(item.path, tempfile.path()); + assert_eq!(item.offset, 0); + assert_eq!(item.len, u64::try_from(b"hello\n".len()).unwrap()); + assert_eq!(item.sha256, [ + // Pre-computed by the `sha256sum` tool. + 0x58, 0x91, 0xb5, 0xb5, 0x22, 0xd5, 0xdf, 0x08, + 0x6d, 0x0f, 0xf0, 0xb1, 0x10, 0xfb, 0xd9, 0xd2, + 0x1b, 0xb4, 0xfc, 0x71, 0x63, 0xaf, 0x34, 0xd0, + 0x82, 0x86, 0xa2, 0xe8, 0x46, 0xf6, 0xbe, 0x03, + ]); + } + + #[test] + fn handle_large() { + let mut tempfile = tempfile::NamedTempFile::new() + .unwrap(); + + use std::io::Read as _; + std::io::copy(&mut std::io::repeat(0).take(13371337), &mut tempfile) + .unwrap(); + + let args = Args { + path: tempfile.path().to_path_buf(), + offset: 0, + len: None, + }; + + let mut session = crate::session::FakeSession::new(); + assert!(handle(&mut session, args).is_ok()); + + assert_eq!(session.reply_count(), 1); + + let item = session.reply::(0); + assert_eq!(item.path, tempfile.path()); + assert_eq!(item.offset, 0); + assert_eq!(item.len, 13371337); + assert_eq!(item.sha256, [ + // Pre-computed by `head --bytes=13371337 < /dev/zero | sha256sum`. + 0xda, 0xa6, 0x04, 0x11, 0x35, 0x03, 0xdb, 0x38, + 0xe3, 0x62, 0xfe, 0xff, 0x8f, 0x73, 0xc1, 0xf9, + 0xb2, 0x6f, 0x02, 0x85, 0x3d, 0x2f, 0x47, 0x8d, + 0x52, 0x16, 0xc5, 0x70, 0x32, 0x54, 0x1c, 0xf8, + ]); + } +} diff --git a/crates/rrg/src/request.rs b/crates/rrg/src/request.rs index f1e7a9a7..9c8f3824 100644 --- a/crates/rrg/src/request.rs +++ b/crates/rrg/src/request.rs @@ -19,8 +19,8 @@ pub enum Action { GetFileMetadata, /// Get contents of the specified file. GetFileContents, - /// Get hash of the specified file. - GetFileHash, + /// Get SHA-256 hash of the specified file. + GetFileSha256, /// Grep the specified file for a pattern. GrepFileContents, /// List contents of a directory. @@ -64,7 +64,7 @@ impl std::fmt::Display for Action { Action::GetSystemMetadata => write!(fmt, "get_system_metadata"), Action::GetFileMetadata => write!(fmt, "get_file_metadata"), Action::GetFileContents => write!(fmt, "get_file_contents"), - Action::GetFileHash => write!(fmt, "get_file_hash"), + Action::GetFileSha256 => write!(fmt, "get_file_sha256"), Action::GrepFileContents => write!(fmt, "grep_file_contents"), Action::ListDirectory => write!(fmt, "list_directory"), Action::ListProcesses => write!(fmt, "list_processes"), @@ -117,7 +117,7 @@ impl TryFrom for Action { GET_SYSTEM_METADATA => Ok(Action::GetSystemMetadata), GET_FILE_METADATA => Ok(Action::GetFileMetadata), GET_FILE_CONTENTS => Ok(Action::GetFileContents), - GET_FILE_HASH => Ok(Action::GetFileHash), + GET_FILE_SHA256 => Ok(Action::GetFileSha256), GREP_FILE_CONTENTS => Ok(Action::GrepFileContents), LIST_DIRECTORY => Ok(Action::ListDirectory), LIST_PROCESSES => Ok(Action::ListProcesses), diff --git a/proto/rrg.proto b/proto/rrg.proto index 4a969035..ad10ba43 100644 --- a/proto/rrg.proto +++ b/proto/rrg.proto @@ -20,8 +20,8 @@ enum Action { GET_FILE_METADATA = 2; // Get contents of the specified file. GET_FILE_CONTENTS = 3; - // Get hash of the specified file. - GET_FILE_HASH = 4; + // Get SHA-256 hash of the specified file. + GET_FILE_SHA256 = 4; // List contents of a directory. LIST_DIRECTORY = 5; // List processes available on the system. diff --git a/proto/rrg/action/get_file_hash.proto b/proto/rrg/action/get_file_hash.proto deleted file mode 100644 index a4bb710d..00000000 --- a/proto/rrg/action/get_file_hash.proto +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2023 Google LLC -// -// Use of this source code is governed by an MIT-style license that can be found -// in the LICENSE file or at https://opensource.org/licenses/MIT. -syntax = "proto3"; - -package rrg.action.get_file_hash; - -import "rrg/fs.proto"; - -message Args { - // Absolute path to the file to get the hash of. - rrg.fs.Path path = 1; - - // Byte offset from which the content should be hashed. - // - // If unset, hashes from the beginning of the file. - uint64 offset = 2; - - // Number of bytes to hash (from the start offset). - // - // If unset, hashes until the end of the file. - uint64 length = 3; - - // Whether to collect an MD5 [1] hash digest of the file content. - // - // [1]: https://en.wikipedia.org/wiki/MD5 - bool md5 = 4; - - // Whether to collect a SHA-1 [1] hash digest of the file content. - // - // [1]: https://en.wikipedia.org/wiki/SHA-1 - bool sha1 = 5; - - // Whether to collect a SHA-256 [1] hash digest of the file content. - // - // [1]: https://en.wikipedia.org/wiki/SHA-2 - bool sha256 = 6; -} - -message Result { - // Canonical path of the file. - rrg.fs.Path path = 1; - - // Byte offset from which the file content was hashed. - uint64 offset = 2; - - // Number of bytes of the file used to produce the hash. - uint64 length = 3; - - // MD5 [1] hash digest of the file content. - // - // This field is set only if MD5 collection was requested and is supported by - // the agent. - // - // [1]: https://en.wikipedia.org/wiki/MD5 - bytes md5 = 4; - - // SHA-1 [1] hash digest of the file content. - // - // This field is set only if SHA-1 collection was requested and is supported - // by the agent. - // - // [1]: https://en.wikipedia.org/wiki/SHA-1 - bytes sha1 = 5; - - // SHA-256 [1] hash digest of the file content. - // - // This field is set only if SHA-256 collection was requested and is supported - // by the agent. - // - // [1]: https://en.wikipedia.org/wiki/SHA-2 - bytes sha256 = 6; -} diff --git a/proto/rrg/action/get_file_sha256.proto b/proto/rrg/action/get_file_sha256.proto new file mode 100644 index 00000000..9af870c0 --- /dev/null +++ b/proto/rrg/action/get_file_sha256.proto @@ -0,0 +1,43 @@ +// Copyright 2023-2025 Google LLC +// +// Use of this source code is governed by an MIT-style license that can be found +// in the LICENSE file or at https://opensource.org/licenses/MIT. +syntax = "proto3"; + +package rrg.action.get_file_sha256; + +import "rrg/fs.proto"; + +message Args { + // Absolute path to the file to get the SHA-256 hash of. + rrg.fs.Path path = 1; + + // Byte offset from which the content should be hashed. + // + // If unset, hashes from the beginning of the file. + uint64 offset = 2; + + // Number of bytes to hash (from the start offset). + // + // This is value serves as an upper bound: if the file is shorter than the + // specified length, only bytes that actually exist are going to be hashed. + // + // If unset, hashes until the end of the file. + uint64 length = 3; +} + +message Result { + // Absolute path of the file this result corresponds to. + rrg.fs.Path path = 1; + + // Byte offset from which the file content was hashed. + uint64 offset = 2; + + // Number of bytes of the file used to produce the hash. + uint64 length = 3; + + // SHA-256 [1] hash digest of the file content. + // + // [1]: https://en.wikipedia.org/wiki/SHA-2 + bytes sha256 = 4; +}