Skip to content

Commit d65edb2

Browse files
authored
Syncing recent changes. (#1099)
* Added support for lising %SystemDrive%\Users * Removed support for the GREP artifact source. * Removed now unused parsers code. * Removed the accidentally introduced dependency of grr-api-client on grr-response-core. * Migrated more API handlers to not use RDFProtoStructs. * Adjusted the file formatting. * Bumped the version to 3.4.7.5.
1 parent e4fec69 commit d65edb2

File tree

184 files changed

+6574
-6477
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

184 files changed

+6574
-6477
lines changed

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
### Added
99

10+
* Added support for listing `%SystemDrive%\Users` as a supplementary mechanism
11+
for collecting user profiles on Windows (additionally to using data from the
12+
registry).
13+
1014
### Removed
1115

1216
* Removed the `ListFlowApplicableParsers` API method.
1317
* Removed the `ListParsedFlowResults` API method.
18+
* Removed support for the `GREP` artifact source (these were internal to GRR and
19+
not part of the [official specification](https://artifacts.readthedocs.io/en/latest/sources/Format-specification.html).
1420

1521
## [3.4.7.4] - 2024-05-28
1622

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# A Docker image capable of running all GRR components.
22
#
3-
# See https://hub.docker.com/r/grrdocker/grr/
3+
# See https://github.com/google/grr/pkgs/container/grr
44
#
55
# We have configured Github Actions to trigger an image build every
66
# time a new a PUSH happens in the GRR github repository.

api_client/python/grr_api_client/client.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
"""Clients-related part of GRR API client library."""
33

44
from collections import abc
5+
import time
56
from typing import Sequence
67

78
from grr_api_client import flow
89
from grr_api_client import utils
910
from grr_api_client import vfs
10-
from grr_response_core.lib import rdfvalue
1111
from grr_response_proto.api import client_pb2
1212
from grr_response_proto.api import flow_pb2
1313
from grr_response_proto.api import user_pb2
@@ -209,10 +209,9 @@ def CreateApproval(
209209

210210
expiration_time_us = 0
211211
if expiration_duration_days != 0:
212-
expiration_time_us = (
213-
rdfvalue.RDFDatetime.Now()
214-
+ rdfvalue.Duration.From(expiration_duration_days, rdfvalue.DAYS)
215-
).AsMicrosecondsSinceEpoch()
212+
expiration_time_us = int(
213+
(time.time() + expiration_duration_days * 24 * 3600) * 1e6
214+
)
216215

217216
approval = user_pb2.ApiClientApproval(
218217
reason=reason,

api_client/python/grr_api_client/flow.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from grr_api_client import context as api_context
1212
from grr_api_client import errors
1313
from grr_api_client import utils
14-
from grr_response_core.lib.util import aead
1514
from grr_response_proto.api import flow_pb2
1615
from grr_response_proto.api import osquery_pb2
1716
from grr_response_proto.api import timeline_pb2
@@ -268,5 +267,5 @@ def DecryptLargeFile(
268267

269268
with input_context as input_stream:
270269
with output_context as output_stream:
271-
decrypted_stream = aead.Decrypt(input_stream, encryption_key)
270+
decrypted_stream = utils.AEADDecrypt(input_stream, encryption_key)
272271
shutil.copyfileobj(decrypted_stream, output_stream)

api_client/python/grr_api_client/utils.py

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
#!/usr/bin/env python
22
"""Utility functions and classes for GRR API client library."""
33

4+
import io
5+
import itertools
6+
import struct
47
import time
58
from typing import Any
69
from typing import Callable
@@ -11,6 +14,8 @@
1114
from typing import TypeVar
1215
from typing import Union
1316

17+
from cryptography.hazmat.primitives.ciphers import aead
18+
1419
from google.protobuf import any_pb2
1520
from google.protobuf import wrappers_pb2
1621
from google.protobuf import descriptor
@@ -307,6 +312,97 @@ def Xor(bytestr: bytes, key: int) -> bytes:
307312
return bytes([byte ^ key for byte in bytestr])
308313

309314

315+
class _Unchunked(io.RawIOBase, IO[bytes]): # pytype: disable=signature-mismatch # overriding-return-type-checks
316+
"""A raw file-like object that reads chunk stream on demand."""
317+
318+
def __init__(self, chunks: Iterator[bytes]) -> None:
319+
"""Initializes the object."""
320+
super().__init__()
321+
self._chunks = chunks
322+
self._buf = io.BytesIO()
323+
324+
def readable(self) -> bool:
325+
return True
326+
327+
def readall(self) -> bytes:
328+
return b"".join(self._chunks)
329+
330+
def readinto(self, buf: bytearray) -> int:
331+
if self._buf.tell() == len(self._buf.getbuffer()):
332+
self._buf.seek(0, io.SEEK_SET)
333+
self._buf.truncate()
334+
self._buf.write(next(self._chunks, b""))
335+
self._buf.seek(0, io.SEEK_SET)
336+
337+
return self._buf.readinto(buf)
338+
339+
340+
def AEADDecrypt(stream: IO[bytes], key: bytes) -> IO[bytes]:
341+
"""Decrypts given file-like object using AES algorithm in GCM mode.
342+
343+
Refer to the encryption documentation to learn about the details of the format
344+
that this function allows to decode.
345+
346+
Args:
347+
stream: A file-like object to decrypt.
348+
key: A secret key used for decrypting the data.
349+
350+
Returns:
351+
A file-like object with decrypted data.
352+
"""
353+
aesgcm = aead.AESGCM(key)
354+
355+
def Generate() -> Iterator[bytes]:
356+
# Buffered reader should accept `IO[bytes]` but for now it accepts only
357+
# `RawIOBase` (which is a concrete base class for all I/O implementations).
358+
reader = io.BufferedReader(stream) # pytype: disable=wrong-arg-types
359+
360+
# We abort early if there is no data in the stream. Otherwise we would try
361+
# to read nonce and fail.
362+
if not reader.peek():
363+
return
364+
365+
for idx in itertools.count():
366+
nonce = reader.read(_AEAD_NONCE_SIZE)
367+
368+
# As long there is some data in the buffer (and there should be because of
369+
# the initial check) there should be a fixed-size nonce prepended to each
370+
# chunk.
371+
if len(nonce) != _AEAD_NONCE_SIZE:
372+
raise EOFError(f"Incorrect nonce length: {len(nonce)}")
373+
374+
chunk = reader.read(_AEAD_CHUNK_SIZE + 16)
375+
376+
# `BufferedReader#peek` will return non-empty byte string if there is more
377+
# data available in the stream.
378+
is_last = reader.peek() == b"" # pylint: disable=g-explicit-bool-comparison
379+
380+
adata = _AEAD_ADATA_FORMAT.pack(idx, is_last)
381+
382+
yield aesgcm.decrypt(nonce, chunk, adata)
383+
384+
if is_last:
385+
break
386+
387+
return io.BufferedReader(_Unchunked(Generate()))
388+
389+
390+
# We use 12 bytes (96 bits) as it is the recommended IV length by NIST for best
391+
# performance [1]. See AESGCM documentation for more details.
392+
#
393+
# [1]: https://csrc.nist.gov/publications/detail/sp/800-38d/final
394+
_AEAD_NONCE_SIZE = 12
395+
396+
# Because chunk size is crucial to the security of the whole procedure, we don't
397+
# let users pick their own chunk size. Instead, we use a fixed-size chunks of
398+
# 4 mebibytes.
399+
_AEAD_CHUNK_SIZE = 4 * 1024 * 1024
400+
401+
# As associated data for each encrypted chunk we use an integer denoting chunk
402+
# id followed by a byte with information whether this is the last chunk.
403+
_AEAD_ADATA_FORMAT = struct.Struct("!Q?")
404+
405+
310406
def RegisterProtoDescriptors(
311407
db: symbol_database.SymbolDatabase,
312408
*additional_descriptors: descriptor.FileDescriptor,

api_client/python/grr_api_client/utils_test.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
#!/usr/bin/env python
22
import io
3+
import os
34
import struct
45

56
from absl.testing import absltest
7+
from cryptography import exceptions
8+
from cryptography.hazmat.primitives.ciphers import aead
69

710
from google.protobuf import empty_pb2
811
from google.protobuf import timestamp_pb2
@@ -96,5 +99,69 @@ def testDecodeSeveralChunks(self):
9699
self.assertEqual(b"".join(decoded), content)
97100

98101

102+
class AEADDecryptTest(absltest.TestCase):
103+
104+
def testReadExact(self):
105+
key = os.urandom(32)
106+
107+
aesgcm = aead.AESGCM(key)
108+
nonce = os.urandom(utils._AEAD_NONCE_SIZE)
109+
adata = utils._AEAD_ADATA_FORMAT.pack(0, True)
110+
encrypted = io.BytesIO(
111+
nonce + aesgcm.encrypt(nonce, b"foobarbazquxnorf", adata)
112+
)
113+
114+
decrypted = utils.AEADDecrypt(encrypted, key)
115+
self.assertEqual(decrypted.read(3), b"foo")
116+
self.assertEqual(decrypted.read(3), b"bar")
117+
self.assertEqual(decrypted.read(3), b"baz")
118+
self.assertEqual(decrypted.read(3), b"qux")
119+
self.assertEqual(decrypted.read(4), b"norf")
120+
121+
self.assertEqual(decrypted.read(), b"")
122+
123+
def testIncorrectNonceLength(self):
124+
key = os.urandom(32)
125+
126+
buf = io.BytesIO()
127+
128+
nonce = os.urandom(utils._AEAD_NONCE_SIZE - 1)
129+
buf.write(nonce)
130+
buf.seek(0, io.SEEK_SET)
131+
132+
with self.assertRaisesRegex(EOFError, "nonce length"):
133+
utils.AEADDecrypt(buf, key).read()
134+
135+
def testIncorrectTag(self):
136+
key = os.urandom(32)
137+
aesgcm = aead.AESGCM(key)
138+
139+
buf = io.BytesIO()
140+
141+
nonce = os.urandom(utils._AEAD_NONCE_SIZE)
142+
buf.write(nonce)
143+
buf.write(aesgcm.encrypt(nonce, b"foo", b"QUUX"))
144+
buf.seek(0, io.SEEK_SET)
145+
146+
with self.assertRaises(exceptions.InvalidTag):
147+
utils.AEADDecrypt(buf, key).read()
148+
149+
def testIncorrectData(self):
150+
key = os.urandom(32)
151+
aesgcm = aead.AESGCM(key)
152+
153+
buf = io.BytesIO()
154+
155+
nonce = os.urandom(utils._AEAD_NONCE_SIZE)
156+
adata = utils._AEAD_ADATA_FORMAT.pack(0, True)
157+
buf.write(nonce)
158+
buf.write(aesgcm.encrypt(nonce, b"foo", adata))
159+
buf.getbuffer()[-1] ^= 0b10101010 # Corrupt last byte.
160+
buf.seek(0, io.SEEK_SET)
161+
162+
with self.assertRaises(exceptions.InvalidTag):
163+
utils.AEADDecrypt(buf, key).read()
164+
165+
99166
if __name__ == "__main__":
100167
absltest.main()

0 commit comments

Comments
 (0)