Skip to content

Commit

Permalink
Merge pull request #253 from LiamMorrow/verify-inbox-messages
Browse files Browse the repository at this point in the history
  • Loading branch information
LiamMorrow authored Aug 21, 2024
2 parents 9feba5c + 458bb2e commit 2d48fc4
Show file tree
Hide file tree
Showing 19 changed files with 196 additions and 25 deletions.
2 changes: 2 additions & 0 deletions LiftLog.Api/LiftLog.Api.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
<InvariantGlobalization>true</InvariantGlobalization>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ContainerRepository>liftlog-api</ContainerRepository>
<!-- Enums not handled exhaustively with numbers -->
<NoWarn>CS8524</NoWarn>
</PropertyGroup>

<ItemGroup>
Expand Down
1 change: 0 additions & 1 deletion LiftLog.Api/Service/PurchaseVerificationService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ public Task<bool> IsValidPurchaseToken(AppStore appStore, string proToken)
proToken
),
AppStore.Web => webAuthPurchaseVerificationService.IsValidPurchaseToken(proToken),
_ => throw new NotSupportedException(),
};
}
}
8 changes: 8 additions & 0 deletions LiftLog.Lib/Services/IEncryptionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ public Task<AesEncryptedAndRsaSignedData> SignRsa256PssAndEncryptAesCbcAsync(
public Task<RsaEncryptedData> EncryptRsaOaepSha256Async(byte[] data, RsaPublicKey publicKey);

public ValueTask<RsaKeyPair> GenerateRsaKeysAsync();

public Task<byte[]> SignRsaPssSha256Async(byte[] data, RsaPrivateKey privateKey);

public Task<bool> VerifyRsaPssSha256Async(
byte[] data,
byte[] signature,
RsaPublicKey publicKey
);
}

public record RsaPublicKey(byte[] SpkiPublicKeyBytes);
Expand Down
33 changes: 33 additions & 0 deletions LiftLog.Lib/Services/OsEncryptionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,4 +163,37 @@ public ValueTask<RsaKeyPair> GenerateRsaKeysAsync()
)
);
}

public Task<byte[]> SignRsaPssSha256Async(byte[] data, RsaPrivateKey privateKey)
{
return Task.Run(() =>
{
var rsa = RSA.Create();

rsa.ImportPkcs8PrivateKey(privateKey.Pkcs8PrivateKeyBytes, out _);

var dataHash = SHA256.HashData(data);

return rsa.SignData(dataHash, HashAlgorithmName.SHA256, RSASignaturePadding.Pss);
});
}

public Task<bool> VerifyRsaPssSha256Async(byte[] data, byte[] signature, RsaPublicKey publicKey)
{
return Task.Run(() =>
{
var rsa = RSA.Create();

rsa.ImportSubjectPublicKeyInfo(publicKey.SpkiPublicKeyBytes, out _);

var dataHash = SHA256.HashData(data);

return rsa.VerifyData(
dataHash,
signature,
HashAlgorithmName.SHA256,
RSASignaturePadding.Pss
);
});
}
}
2 changes: 2 additions & 0 deletions LiftLog.Maui/LiftLog.Maui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
<SingleProject>true</SingleProject>
<ImplicitUsings>enable</ImplicitUsings>
<EnableDefaultCssItems>false</EnableDefaultCssItems>
<!-- Enums not handled exhaustively with numbers -->
<NoWarn>CS8524</NoWarn>

<!-- Display name -->
<ApplicationTitle>LiftLog</ApplicationTitle>
Expand Down
1 change: 0 additions & 1 deletion LiftLog.Maui/Services/AppHapticFeedbackService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ public Task PerformAsync(Ui.Services.HapticFeedbackType amount)
{
Ui.Services.HapticFeedbackType.Click => HapticFeedbackType.Click,
Ui.Services.HapticFeedbackType.LongPress => HapticFeedbackType.LongPress,
_ => throw new ArgumentOutOfRangeException(nameof(amount), amount, null),
};
hapticFeedback.Perform(mapped);
return Task.CompletedTask;
Expand Down
10 changes: 0 additions & 10 deletions LiftLog.Maui/Services/AppThemeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,6 @@ public Task SetSeedColor(uint? seed, ThemePreference themePreference)
ThemePreference.FollowSystem => AppTheme.Unspecified,
ThemePreference.Light => AppTheme.Light,
ThemePreference.Dark => AppTheme.Dark,
_ => throw new ArgumentOutOfRangeException(
nameof(themePreference),
themePreference,
null
),
};
if (seed is not null)
{
Expand Down Expand Up @@ -55,11 +50,6 @@ public ThemePreference GetThemePreference()
AppTheme.Unspecified => ThemePreference.FollowSystem,
AppTheme.Light => ThemePreference.Light,
AppTheme.Dark => ThemePreference.Dark,
_ => throw new ArgumentOutOfRangeException(
nameof(Microsoft.Maui.Controls.Application.Current.UserAppTheme),
Microsoft.Maui.Controls.Application.Current.UserAppTheme,
null
),
};
}

Expand Down
2 changes: 2 additions & 0 deletions LiftLog.Ui/LiftLog.Ui.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EmitCompilerGeneratedFiles>true</EmitCompilerGeneratedFiles>
<!-- Enums not handled exhaustively with numbers -->
<NoWarn>CS8524</NoWarn>
</PropertyGroup>

<PropertyGroup>
Expand Down
11 changes: 11 additions & 0 deletions LiftLog.Ui/Models/FeedStateDao.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,4 +216,15 @@ value is null
FromUserId = value.UserId,
FollowRequest = new FollowRequestDao { Name = value.Name },
};

public byte[] GetPayloadBytes()
{
return MessagePayloadCase switch
{
MessagePayloadOneofCase.FollowRequest => FollowRequest.ToByteArray(),
MessagePayloadOneofCase.FollowResponse => FollowResponse.ToByteArray(),
MessagePayloadOneofCase.UnfollowNotification => UnfollowNotification.ToByteArray(),
MessagePayloadOneofCase.None => [],
};
}
}
2 changes: 2 additions & 0 deletions LiftLog.Ui/Models/UserEvent.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ message InboxMessageDao {
FollowResponseDao follow_response = 3;
UnFollowNotification unfollow_notification = 4;
}
// The signature of the [message +from_user_id+ the to_user_id], signed with the private key of the from_user_id
bytes signature = 5;
}

message FollowRequestDao {
Expand Down
30 changes: 30 additions & 0 deletions LiftLog.Ui/Services/FeedFollowService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ public async Task<ApiResult> RequestToFollowAUserAsync(FeedIdentity identity, Fe
FromUserId = identity.Id,
FollowRequest = new FollowRequestDao { Name = identity.Name },
};
inboxMessage.Signature = ByteString.CopyFrom(
await encryptionService.SignRsaPssSha256Async(
[
.. inboxMessage.GetPayloadBytes(),
.. identity.Id.ToByteArray(),
.. toFollow.Id.ToByteArray(),
],
identity.RsaKeyPair.PrivateKey
)
);
var response = await feedApiService.PutInboxMessageAsync(
new PutInboxMessageRequest(
ToUserId: toFollow.Id,
Expand Down Expand Up @@ -65,6 +75,16 @@ RsaPublicKey userPublicKey
},
},
};
inboxMessage.Signature = ByteString.CopyFrom(
await encryptionService.SignRsaPssSha256Async(
[
.. inboxMessage.GetPayloadBytes(),
.. identity.Id.ToByteArray(),
.. request.UserId.ToByteArray(),
],
identity.RsaKeyPair.PrivateKey
)
);
var encryptedMessage = await encryptionService.EncryptRsaOaepSha256Async(
inboxMessage.ToByteArray(),
userPublicKey
Expand Down Expand Up @@ -95,6 +115,16 @@ RsaPublicKey userPublicKey
FromUserId = identity.Id,
FollowResponse = new FollowResponseDao { Rejected = new FollowResponseRejectedDao() },
};
inboxMessage.Signature = ByteString.CopyFrom(
await encryptionService.SignRsaPssSha256Async(
[
.. inboxMessage.GetPayloadBytes(),
.. identity.Id.ToByteArray(),
.. request.UserId.ToByteArray(),
],
identity.RsaKeyPair.PrivateKey
)
);
RsaEncryptedData? encryptedMessage;
try
{
Expand Down
1 change: 0 additions & 1 deletion LiftLog.Ui/Shared/Smart/SessionComponent.razor
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ else
SessionTarget.WorkoutSession => CurrentSessionState.Value.WorkoutSession,
SessionTarget.HistorySession => CurrentSessionState.Value.HistorySession,
SessionTarget.FeedSession => CurrentSessionState.Value.FeedSession,
_ => throw new InvalidOperationException("Invalid session target")
} ?? Session.Empty;

[EditorRequired] [Parameter] public ImmutableDictionary<KeyedExerciseBlueprint, ImmutableListValue<DatedRecordedExercise>> PreviouslyCompleted { get; set; } = null!;
Expand Down
3 changes: 0 additions & 3 deletions LiftLog.Ui/Store/CurrentSession/CurrentSessionEffects.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ IDispatcher dispatcher
SessionTarget.WorkoutSession => state.Value.WorkoutSession,
SessionTarget.HistorySession => state.Value.HistorySession,
SessionTarget.FeedSession => state.Value.FeedSession,
_ => throw new Exception(),
};
if (session is not null)
await progressRepository.SaveCompletedSessionAsync(session);
Expand Down Expand Up @@ -54,7 +53,6 @@ public async Task NotifySetTimer(NotifySetTimerAction action, IDispatcher dispat
SessionTarget.WorkoutSession => state.Value.WorkoutSession,
SessionTarget.HistorySession => state.Value.HistorySession,
SessionTarget.FeedSession => state.Value.FeedSession,
_ => throw new Exception(),
};
if (session?.NextExercise is not null && session.LastExercise is not null)
{
Expand All @@ -77,7 +75,6 @@ IDispatcher dispatcher
SessionTarget.WorkoutSession => state.Value.WorkoutSession,
SessionTarget.HistorySession => state.Value.HistorySession,
SessionTarget.FeedSession => state.Value.FeedSession,
_ => throw new Exception(),
};
if (session?.NextExercise is not null)
{
Expand Down
3 changes: 0 additions & 3 deletions LiftLog.Ui/Store/CurrentSession/CurrentSessionReducers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,6 @@ private static CurrentSessionState WithActiveSession(
SessionTarget.WorkoutSession => state with { WorkoutSession = session },
SessionTarget.HistorySession => state with { HistorySession = session },
SessionTarget.FeedSession => state with { FeedSession = session },
_ => throw new Exception(),
};

private static CurrentSessionState WithActiveSession(
Expand All @@ -383,7 +382,6 @@ private static CurrentSessionState WithActiveSession(
HistorySession = sessionMap(state.HistorySession),
},
SessionTarget.FeedSession => state with { FeedSession = sessionMap(state.FeedSession) },
_ => throw new Exception(),
};

private static Session? ActiveSession(this CurrentSessionState state, SessionTarget target) =>
Expand All @@ -392,6 +390,5 @@ private static CurrentSessionState WithActiveSession(
SessionTarget.WorkoutSession => state.WorkoutSession,
SessionTarget.HistorySession => state.HistorySession,
SessionTarget.FeedSession => state.FeedSession,
_ => throw new Exception(),
};
}
55 changes: 54 additions & 1 deletion LiftLog.Ui/Store/Feed/FeedEffects.Following.cs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,16 @@ public async Task UnfollowFeedUser(UnfollowFeedUserAction action, IDispatcher di
FollowSecret = action.FeedUser.FollowSecret,
},
};
inboxMessage.Signature = ByteString.CopyFrom(
await encryptionService.SignRsaPssSha256Async(
[
.. inboxMessage.GetPayloadBytes(),
.. identity.Id.ToByteArray(),
.. action.FeedUser.Id.ToByteArray(),
],
identity.RsaKeyPair.PrivateKey
)
);
var encryptedInboxMessage = await encryptionService.EncryptRsaOaepSha256Async(
inboxMessage.ToByteArray(),
action.FeedUser.PublicKey
Expand Down Expand Up @@ -277,6 +287,20 @@ IDispatcher dispatcher
dispatcher.Dispatch(new RemoveFollowerAction(action.User));
});

private async ValueTask<RsaPublicKey> GetUserPublicKey(Guid userId)
{
if (state.Value.FollowedUsers.TryGetValue(userId, out var followedUser))
{
return followedUser.PublicKey;
}
var userResponse = await feedApiService.GetUserAsync(userId.ToString());
if (!userResponse.IsSuccess)
{
throw new InvalidOperationException("Failed to fetch user for public key");
}
return new RsaPublicKey(userResponse.Data.RsaPublicKey);
}

private async Task<InboxMessageDao?> DecryptIfValid(
GetInboxMessageResponse inboxMessage,
RsaPrivateKey privateKey
Expand All @@ -288,7 +312,36 @@ RsaPrivateKey privateKey
new RsaEncryptedData(inboxMessage.EncryptedMessage),
privateKey
);
return InboxMessageDao.Parser.ParseFrom(decrypted);
var unverifiedInboxMessage = InboxMessageDao.Parser.ParseFrom(decrypted);

if (
unverifiedInboxMessage.Signature is null
|| unverifiedInboxMessage.Signature.Length == 0
)
{
// Temporary until majority of users have updated to new version
return unverifiedInboxMessage;
}

var payloadBytes = unverifiedInboxMessage.GetPayloadBytes();
var myUserIdBytes = state.Value.Identity!.Id.ToByteArray();
var subjectUserIdBytes = unverifiedInboxMessage.FromUserId.ToByteArray();

byte[] signedPayload = [.. payloadBytes, .. subjectUserIdBytes, .. myUserIdBytes];
var publicKey = await GetUserPublicKey(unverifiedInboxMessage.FromUserId);

var verified = await encryptionService.VerifyRsaPssSha256Async(
signedPayload,
unverifiedInboxMessage.Signature.ToByteArray(),
publicKey
);

if (!verified)
{
throw new InvalidOperationException("Failed to verify inbox message signature");
}

return unverifiedInboxMessage;
}
catch (Exception e)
{
Expand Down
2 changes: 2 additions & 0 deletions LiftLog.Web/LiftLog.Web.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ImplicitUsings>enable</ImplicitUsings>

<!-- Enums not handled exhaustively with numbers -->
<NoWarn>CS8524</NoWarn>
<!-- <RunAOTCompilation>true</RunAOTCompilation>
<RunAOTCompilationAfterBuild>true</RunAOTCompilationAfterBuild>
<WasmNativeStrip>false</WasmNativeStrip> -->
Expand Down
14 changes: 14 additions & 0 deletions LiftLog.Web/Services/JsEncryptionService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,4 +64,18 @@ public ValueTask<RsaKeyPair> GenerateRsaKeysAsync()
{
return jSRuntime.InvokeAsync<RsaKeyPair>("CryptoUtils.generateRsaKeys");
}

public Task<byte[]> SignRsaPssSha256Async(byte[] data, RsaPrivateKey privateKey)
{
return jSRuntime
.InvokeAsync<byte[]>("CryptoUtils.signRsaPssSha256Async", data, privateKey)
.AsTask();
}

public Task<bool> VerifyRsaPssSha256Async(byte[] data, byte[] signature, RsaPublicKey publicKey)
{
return jSRuntime
.InvokeAsync<bool>("CryptoUtils.verifyRsaPssSha256Async", data, signature, publicKey)
.AsTask();
}
}
5 changes: 0 additions & 5 deletions LiftLog.Web/Services/WebThemeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,6 @@ public async Task SetSeedColor(uint? seed, ThemePreference themePreference)
ThemePreference.FollowSystem => windowThemePrefersDark ? Dark() : Light(),
ThemePreference.Light => Light(),
ThemePreference.Dark => Dark(),
_ => throw new ArgumentOutOfRangeException(
nameof(themePreference),
themePreference,
null
),
};
dispatcher.Dispatch(
new ThemeColorUpdatedAction(
Expand Down
Loading

0 comments on commit 2d48fc4

Please sign in to comment.