using System.Security.Cryptography; using System.Text; using Org.BouncyCastle.Crypto.Digests; using Voicecat.V1; namespace VoiceCat.Server.Data; public sealed partial class AccountStore { private static byte[] ChannelDigest(string password, byte[] salt) { var digest = new Blake2bDigest(salt, 32, null, null); byte[] bytes = Encoding.UTF8.GetBytes(password); byte[] hash = new byte[32]; try { digest.BlockUpdate(bytes, 0, bytes.Length); digest.DoFinal(hash, 0); return hash; } finally { CryptographicOperations.ZeroMemory(bytes); } } public bool CheckChannelPassword(uint id, string password) { using var connection = Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT password_hash FROM channels WHERE id=$id"; command.Parameters.AddWithValue("$id", id); if (command.ExecuteScalar() is not string stored) return false; if (stored.Length == 0) return true; if (stored.Length != 97 || stored[32] != ':') return false; try { byte[] salt = Convert.FromHexString(stored[..32]); return CryptographicOperations.FixedTimeEquals(ChannelDigest(password, salt), Convert.FromHexString(stored[33..])); } catch (FormatException) { return false; } } internal Channel SaveChannel(Channel channel, string password, bool create) { using var connection = Open(); using var command = connection.CreateCommand(); string hash = ""; if (password.Length != 0) { byte[] salt = RandomNumberGenerator.GetBytes(16); hash = Convert.ToHexString(salt).ToLowerInvariant() + ":" + Convert.ToHexString(ChannelDigest(password, salt)).ToLowerInvariant(); } string[] columns = ["parent_id", "name", "topic", "max_users", "type", "sort_order", "audio_codec", "audio_mode", "audio_sample_rate", "audio_bitrate_bps", "audio_frame_ms", "audio_application", "audio_fec", "audio_expected_packet_loss", "audio_dtx", "audio_complexity"]; var a = channel.Audio; object[] values = [channel.ParentId, channel.Name, channel.Topic, channel.MaxUsers, (int)channel.Type, channel.Order, a.Codec, (int)a.Mode, a.SampleRate, a.BitrateBps, a.FrameMs, (int)a.Application, a.Fec, a.ExpectedPacketLoss, a.Dtx, a.Complexity]; for (int i = 0; i < columns.Length; i++) command.Parameters.AddWithValue("$" + columns[i], values[i]); command.Parameters.AddWithValue("$hash", hash); command.Parameters.AddWithValue("$id", channel.Id); command.CommandText = create ? $"INSERT INTO channels ({string.Join(',', columns)},password_hash) VALUES ({string.Join(',', columns.Select(c => "$" + c))},$hash) RETURNING id" : $"UPDATE channels SET {string.Join(',', columns.Select(c => c + "=$" + c))},password_hash=CASE WHEN $hash='' THEN password_hash ELSE $hash END WHERE id=$id RETURNING id"; var saved = channel.Clone(); saved.Id = checked((uint)(long)(command.ExecuteScalar() ?? throw new InvalidDataException("Channel not found."))); saved.PasswordProtected = hash.Length != 0 || !create && CheckChannelPasswordPresent(saved.Id); return saved; } private bool CheckChannelPasswordPresent(uint id) { using var connection = Open(); using var command = connection.CreateCommand(); command.CommandText = "SELECT length(password_hash)>0 FROM channels WHERE id=$id"; command.Parameters.AddWithValue("$id", id); return (long)command.ExecuteScalar()! != 0; } internal void DeleteChannel(uint id) { using var connection = Open(); using var command = connection.CreateCommand(); command.CommandText = "DELETE FROM channels WHERE id=$id"; command.Parameters.AddWithValue("$id", id); command.ExecuteNonQuery(); } }