100 lines
4.7 KiB
C#
100 lines
4.7 KiB
C#
using System.Security.Cryptography;
|
|
|
|
namespace WebDrop.Core;
|
|
|
|
public sealed partial class ChannelService
|
|
{
|
|
private static (byte[]? Hash, byte[]? Salt) HashJoinPassword(string? password)
|
|
{
|
|
if (string.IsNullOrEmpty(password)) return (null, null);
|
|
if (password.Length is < 8 or > 256) throw new WebDropException(ErrorCode.BadRequest);
|
|
var salt = RandomNumberGenerator.GetBytes(16);
|
|
return (Rfc2898DeriveBytes.Pbkdf2(password, salt, 210_000, HashAlgorithmName.SHA256, 32), salt);
|
|
}
|
|
|
|
private static void CheckAdmission(IResourceTx tx, Channel channel, Credentials creds, string? password)
|
|
{
|
|
if (creds.AccountName is not null && creds.AccountName == channel.CreatorAccount) return;
|
|
if (tx.IsJoinDenied(channel.Id, creds.AccountName, creds.EndpointHash)) throw new WebDropException(ErrorCode.Forbidden);
|
|
bool invited = creds.AccountName is not null && tx.HasInvitation(channel.Id, creds.AccountName);
|
|
if (invited) return;
|
|
if (creds.AccountName is not null && tx.GetChannelMember(channel.Id, creds.AccountName) is not null) return;
|
|
if (channel.Settings.JoinPolicy == "invite") throw new WebDropException(ErrorCode.InvitationRequired);
|
|
if (!channel.Settings.AllowGuests && creds.AccountName is null) throw new WebDropException(ErrorCode.AccountRequired);
|
|
if (channel.JoinPasswordHash is null) return;
|
|
if (string.IsNullOrEmpty(password)) throw new WebDropException(ErrorCode.ChannelPasswordRequired);
|
|
if (password.Length > 256) throw new WebDropException(ErrorCode.ChannelPasswordInvalid);
|
|
var actual = Rfc2898DeriveBytes.Pbkdf2(password, channel.JoinPasswordSalt!, 210_000, HashAlgorithmName.SHA256, 32);
|
|
if (!CryptographicOperations.FixedTimeEquals(actual, channel.JoinPasswordHash)) throw new WebDropException(ErrorCode.ChannelPasswordInvalid);
|
|
}
|
|
|
|
public void InviteUser(Id16 id, Credentials creds, string username, string payload)
|
|
{
|
|
if (payload is null || payload.Length is < 1 or > 16_384) throw new WebDropException(ErrorCode.BadRequest);
|
|
store.Write(tx =>
|
|
{
|
|
var (_, principal) = Authenticate(tx, id, creds, SeatRule.None);
|
|
RequireOwner(principal);
|
|
tx.PutInvitation(new ChannelInvitation(id, username, payload, clock.NowMs));
|
|
if (tx.GetChannelMember(id, username)?.Role == "owner") throw new WebDropException(ErrorCode.Forbidden);
|
|
tx.PutKeyPackage(id, username, payload, clock.NowMs);
|
|
tx.AllowAccount(id, username);
|
|
return 0;
|
|
});
|
|
}
|
|
|
|
public IReadOnlyList<ChannelInvitation> Invitations(Id16 id, Credentials creds) => store.Read(tx =>
|
|
{
|
|
var (_, principal) = Authenticate(tx, id, creds, SeatRule.None);
|
|
RequireOwner(principal);
|
|
return tx.ListInvitations(id);
|
|
});
|
|
|
|
public IReadOnlyList<ChannelInvitation> UserInvitations(string username) => store.Read(tx => tx.ListUserInvitations(username));
|
|
|
|
public void RevokeInvitation(Id16 id, Credentials creds, string username)
|
|
{
|
|
store.Write(tx =>
|
|
{
|
|
var (_, principal) = Authenticate(tx, id, creds, SeatRule.None);
|
|
RequireOwner(principal);
|
|
if (tx.GetChannelMember(id, username)?.Role == "owner") throw new WebDropException(ErrorCode.Forbidden);
|
|
tx.DeleteInvitation(id, username);
|
|
tx.DeleteKeyPackage(id, username);
|
|
tx.DeleteAccountSeats(id, username);
|
|
tx.DeleteChannelMember(id, username);
|
|
tx.DenyAccount(id, username);
|
|
return 0;
|
|
});
|
|
events.SeatsChanged(id);
|
|
}
|
|
|
|
public IReadOnlyList<ChannelMember> Members(Id16 id, Credentials creds) => store.Read(tx =>
|
|
{
|
|
var (_, principal) = Authenticate(tx, id, creds, SeatRule.None);
|
|
if (principal.Role != Role.Owner) _ = Authenticate(tx, id, creds, SeatRule.Required);
|
|
return tx.ListChannelMembers(id);
|
|
});
|
|
|
|
public IReadOnlyList<ChannelKeyPackage> AccountChannels(string username) =>
|
|
store.Read(tx => tx.ListUserKeyPackages(username, clock.NowMs));
|
|
|
|
public void ReleaseAccountDevice(string username, string deviceId)
|
|
{
|
|
var channels = store.Write(tx => tx.DeleteDeviceSeats(username, deviceId));
|
|
foreach (var id in channels) events.SeatsChanged(id);
|
|
}
|
|
|
|
public void SaveOwnerKeyPackage(Id16 id, Credentials creds, string payload)
|
|
{
|
|
if (payload is null || payload.Length is < 1 or > 16_384) throw new WebDropException(ErrorCode.BadRequest);
|
|
store.Write(tx =>
|
|
{
|
|
var (_, principal) = Authenticate(tx, id, creds, SeatRule.None);
|
|
RequireOwner(principal);
|
|
tx.PutKeyPackage(id, principal.AccountName!, payload, clock.NowMs);
|
|
return 0;
|
|
});
|
|
}
|
|
}
|