Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Content-hashed external URI parsing support #2387

Open
wants to merge 4 commits into
base: main
Choose a base branch
from

Conversation

gnunicorn
Copy link
Contributor

This PR brings support for providing non matrix-to https links with content hashing support for additional security. These work analogous to the previous acter:-style-links but with that entire path put into the #-fragment (query stays the same, but the order might change) and replacing the path with a final hash of the entire URI in this style without that hash as the final parameter). This allows any server to provide that feature under any domain + path as long as the last path-item is the hash itself.

Example: acter:roomid/room:acter.global/e/someEvent?via=acter.global&via=example.org becomes https://app.acter.global/p/$HASH?via=acter.global&via=example.org#roomid/room:acter.global/e/someEvent (where $HASH = SHA1.of(https://app.acter.global/p/?via=acter.global&via=example.org#roomid/room:acter.global/e/someEvent)

This ensures that we can a) still parse most information from the URI itself without querying the server and b) ensure that the data wasn't messed with in transit or by any third party compared to the data that the server would be showing under that hash.

To the reviewer:

  1. I renamed the files and functions for better lookup
  2. I have refactored the code to be reusable for both tests of the acter: and new https-links, that's why the test file looks different
  3. I decided to update the super-invite-uris to use the same pattern (rather than a host-url-one) to make it easier to reuse the code (which required updating our current qr-code generator)
  4. links with a hash are checked for containing a user-id - if not the result is thrown in a particular error (allowing the UI to still use it, but make clear that this isn't the usual way it is meant to be)

@@ -32,14 +40,41 @@ UriParseResult _parseActerUri(Uri uri) {
};
}

UriParseResult _parseHttpsUri(Uri uri) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the core of the new scheme

Copy link
Contributor

Hey there 👋,
and thanks for the contribution. But it seems like you forgot to

  • 📰 Add a markdown file in .changes/ explaining what changed

@@ -32,14 +40,41 @@ UriParseResult _parseActerUri(Uri uri) {
};
}

UriParseResult _parseHttpsUri(Uri uri) {
if (uri.host == 'matrix.to') {
return _parseMatrixHttpsUri(uri);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

matrix.to is a special case with a special format for legacy links

Comment on lines +47 to +58
final hash = uri.pathSegments.lastOrNull;
if (hash == null || hash.isEmpty) {
throw IncorrectHashError();
}

final pathWithoutHash = uri.path.substring(0, uri.path.length - hash.length -1);
final strippedUri = uri.replace(path: pathWithoutHash);
final hashableUri = strippedUri.toString();
final calculatedHash = sha1.convert(utf8.encode(hashableUri)).toString();
if (calculatedHash != hash) {
throw IncorrectHashError();
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. look for the hash
  2. remove the hash
  3. compare the hash

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Example: acter:roomid/room:acter.global/e/someEvent?via=acter.global&via=example.org becomes https://app.acter.global/p/$HASH?via=acter.global&via=example.org#roomid/room:acter.global/e/someEvent (where $HASH = SHA1.of(https://app.acter.global/p/?via=acter.global&via=example.org#roomid/room:acter.global/e/someEvent)


Not quite understand link structure and logic. Sorry but looks complex to me to understand.

Comment on lines +61 to +62
final extractableUri = strippedUri.replace(path: strippedUri.fragment, fragment: null);
final result = _parseActerUri(extractableUri);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract the fragment as the path to just use the existing parsing code

final extractableUri = strippedUri.replace(path: strippedUri.fragment, fragment: null);
final result = _parseActerUri(extractableUri);
if (result.preview.userId == null || result.preview.userId?.isEmpty == true) {
throw MissingUserError(result: result);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fail with the result if no userId was found.


// re-usable test cases

void acterObjectLinksTests(UriMaker makeUri) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these are the same as before, just that we have new UriMaker so we can resuse the same tests suites for multiple "schemes"

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

Copy link

codecov bot commented Nov 26, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 27.89%. Comparing base (b1eb5c5) to head (8bcb398).
Report is 1 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2387      +/-   ##
==========================================
+ Coverage   27.86%   27.89%   +0.02%     
==========================================
  Files         617      617              
  Lines       42261    42279      +18     
==========================================
+ Hits        11778    11793      +15     
- Misses      30483    30486       +3     
Flag Coverage Δ
integration-test 36.88% <ø> (-0.02%) ⬇️
unittest 20.11% <ø> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

Copy link
Contributor

@kumarpalsinh25 kumarpalsinh25 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deep-link structure looks complex to me, not sure if I am not getting it well or it is actually complex itself.


// re-usable test cases

void acterObjectLinksTests(UriMaker makeUri) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👏

Comment on lines +220 to +242
group('Testing fallback https://app.acter.global/:-links', () =>
newSpecLinksTests(makeUriMakerForPublicPrefix('https://app.acter.global/p/')),
);

group('Testing https://app.acter.global/ object-links', () =>
acterObjectLinksTests(makeUriMakerForPublicPrefix('https://app.acter.global/p/')),
);

group('Testing https://app.acter.global/ invite-links', () =>
acterInviteLinkTests(makeUriMakerForPublicPrefix('https://app.acter.global/p/')),
);

group('Testing https://app.acter.global/ preview data', () =>
acterObjectPreviewTests(makeUriMakerForPublicPrefix('https://app.acter.global/p/')),
);

group('Testing broken https://app.acter.global/ ', () {
test('faulty hash fails', () async {
expect(() => parseActerUri(
Uri.parse('http://acter.global/p/faultyHash?via=acter.global&via=example.org#roomid/room:acter.global/e/someEvent'),
),
throwsA(TypeMatcher<IncorrectHashError>()),
);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Getting bit difficulty to grasp entire logic.

Comment on lines +47 to +58
final hash = uri.pathSegments.lastOrNull;
if (hash == null || hash.isEmpty) {
throw IncorrectHashError();
}

final pathWithoutHash = uri.path.substring(0, uri.path.length - hash.length -1);
final strippedUri = uri.replace(path: pathWithoutHash);
final hashableUri = strippedUri.toString();
final calculatedHash = sha1.convert(utf8.encode(hashableUri)).toString();
if (calculatedHash != hash) {
throw IncorrectHashError();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.


Example: acter:roomid/room:acter.global/e/someEvent?via=acter.global&via=example.org becomes https://app.acter.global/p/$HASH?via=acter.global&via=example.org#roomid/room:acter.global/e/someEvent (where $HASH = SHA1.of(https://app.acter.global/p/?via=acter.global&via=example.org#roomid/room:acter.global/e/someEvent)


Not quite understand link structure and logic. Sorry but looks complex to me to understand.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: Backlog
Development

Successfully merging this pull request may close these issues.

2 participants