Add support for P25 encryption; several fixes and features

pull/1/head
firealarmss 11 months ago
parent 2aeb714d0c
commit 7fda7dc2c7

@ -70,8 +70,8 @@ Global
{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|Any CPU.Build.0 = Release|Any CPU {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|Any CPU.Build.0 = Release|Any CPU
{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x64.ActiveCfg = Release|x64 {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x64.ActiveCfg = Release|x64
{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x64.Build.0 = Release|x64 {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x64.Build.0 = Release|x64
{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x86.ActiveCfg = Release|Any CPU {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x86.ActiveCfg = Release|x86
{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x86.Build.0 = Release|Any CPU {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.Release|x86.Build.0 = Release|x86
{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU
{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|Any CPU.Build.0 = WIN32|Any CPU {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|Any CPU.Build.0 = WIN32|Any CPU
{710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x64.ActiveCfg = WIN32|x64 {710D1FA8-2E0D-42CB-9174-FCD9EB7A718F}.WIN32|x64.ActiveCfg = WIN32|x64
@ -98,10 +98,10 @@ Global
{1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x86.Build.0 = Release|Any CPU {1F06ECB1-9928-1430-63F4-2E01522A0510}.Release|x86.Build.0 = Release|Any CPU
{1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|Any CPU.ActiveCfg = WIN32|Any CPU
{1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|Any CPU.Build.0 = WIN32|Any CPU {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|Any CPU.Build.0 = WIN32|Any CPU
{1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x64.ActiveCfg = WIN32|Any CPU {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x64.ActiveCfg = Release|Any CPU
{1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x64.Build.0 = WIN32|Any CPU {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x64.Build.0 = Release|Any CPU
{1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x86.ActiveCfg = WIN32|Any CPU {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x86.ActiveCfg = Release|Any CPU
{1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x86.Build.0 = WIN32|Any CPU {1F06ECB1-9928-1430-63F4-2E01522A0510}.WIN32|x86.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE

@ -0,0 +1,55 @@
/*
* WhackerLink - WhackerLinkConsoleV2
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Copyright (C) 2025 Caleb, K4PHP
*
*/
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using WhackerLinkLib.Models;
using YamlDotNet.Serialization.NamingConventions;
using YamlDotNet.Serialization;
using System.Diagnostics;
namespace WhackerLinkConsoleV2
{
public static class AliasTools
{
public static List<RadioAlias> LoadAliases(string filePath)
{
if (!File.Exists(filePath))
{
throw new FileNotFoundException("Alias file not found.", filePath);
}
var deserializer = new DeserializerBuilder()
.WithNamingConvention(CamelCaseNamingConvention.Instance)
.Build();
var yamlText = File.ReadAllText(filePath);
return deserializer.Deserialize<List<RadioAlias>>(yamlText);
}
public static string GetAliasByRid(List<RadioAlias> aliases, int rid)
{
var match = aliases.FirstOrDefault(a => a.Rid == rid);
return match?.Alias;
}
}
}

@ -75,9 +75,12 @@ namespace WhackerLinkConsoleV2.Controls
public MBEEncoder encoder; public MBEEncoder encoder;
public MBEDecoder decoder; public MBEDecoder decoder;
public MBEToneDetector toneDetector = new MBEToneDetector();
public P25Crypto crypter = new P25Crypto(); public P25Crypto crypter = new P25Crypto();
public bool IsReceiving { get; set; } = false; public bool IsReceiving { get; set; } = false;
public bool IsReceivingEncrypted { get; set; } = false;
public string LastSrcId public string LastSrcId
{ {
@ -221,10 +224,6 @@ namespace WhackerLinkConsoleV2.Controls
PageSelectButton.IsEnabled = false; PageSelectButton.IsEnabled = false;
ChannelMarkerBtn.IsEnabled = false; ChannelMarkerBtn.IsEnabled = false;
} }
byte[] key = { 00, 00, 00, 00, 00 };
crypter.AddKey(00, 0xaa, key);
} }
private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) private void ChannelBox_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)

@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NWaves.Signals;
using NWaves.Transforms;
namespace WhackerLinkConsoleV2
{
public class MBEToneDetector
{
// Samplerate is 8000 Hz
private static int sample_rate = 8000;
// We operate on 160 sample (20ms @ 8kHz) windows
private static int window_size = 160;
// There are 128 possible tone indexes per TIA-102.BABA-1
private static int num_coeffs = 128;
// Bin size in hz
private static float bin_size_hz = (float)sample_rate / 2f / (float)num_coeffs;
// Bounds of tone detection (in bin index format)
private int low_bin_limit;
private int high_bin_limit;
// This is the tone detection ratio (amplitude of max bin divided by average of all others)
private int detect_ratio;
// This is the number of "hits" on a frequency we need to get before we detect a valid tone
private int hits_reqd;
// Counter for the above hits
private int hits_freq;
private int num_hits;
// The STFT (short-time fourier transform) operator
private Stft stft;
/// <summary>
/// Create a pitch detector which reports the running average of pitch for a sequence of samples
/// </summary>
/// <param name="detect_ratio">Ratio required for a valid tone detection</param>
/// <param name="hits_reqd">Number of repeated "hits" on a frequency to count as a tone detection</param>
public MBEToneDetector(int detect_ratio = 90, int hits_reqd = 2, int low_limit = 250, int high_limit = 3000)
{
this.detect_ratio = detect_ratio;
this.hits_reqd = hits_reqd;
stft = new Stft(window_size, 1, NWaves.Windows.WindowType.Hann, num_coeffs);
hits_freq = 0;
num_hits = 0;
low_bin_limit = (int)(low_limit / bin_size_hz);
high_bin_limit = (int)(high_limit / bin_size_hz);
}
/// <summary>
/// Perform a tone analysis on the provided samples, and return a tone frequency if one is detected
/// </summary>
/// <param name="samples"></param>
/// <returns></returns>
public int Detect(DiscreteSignal signal)
{
// Validate input
if (signal.Length != window_size)
{
throw new ArgumentOutOfRangeException($"Signal must be {window_size} samples long!");
}
if (signal.SamplingRate != sample_rate)
{
throw new ArgumentOutOfRangeException($"Signal must have sample rate of {sample_rate} Hz!");
}
// Analyze
float[] values = stft.Spectrogram(signal)[0];
// Remove bins outside our limit
float[] limited_values = values[low_bin_limit..high_bin_limit];
// Find max (from https://stackoverflow.com/a/50239922/1842613)
(float max_val, int max_idx) = limited_values.Select((n, i) => (n, i)).Max();
// Add back in our lower limit so the index is correct
max_idx += low_bin_limit;
// Calculate sum of all others
float sum = values.Sum() - max_val;
// Find average
float avg = sum / (window_size - 1);
// Find ratio
float ratio = max_val / avg;
// Debug
//Log.Logger.Debug($"(Tone detector): max at {max_idx} ({(int)(max_idx * bin_size_hz)} Hz): {max_val}, ratio: {ratio}");
// Return if above threshold
if (ratio > detect_ratio)
{
// Calculate the tone frequency
int tone_freq = (int)(bin_size_hz * max_idx);
// Determine hits
if (hits_freq == tone_freq)
{
num_hits++;
if (num_hits >= hits_reqd)
{
// Debug
//Log.Logger.Debug($"Detected {tone_freq} Hz tone! (ratio {ratio})");
return tone_freq;
}
}
else
{
num_hits = 1;
hits_freq = tone_freq;
}
}
return 0;
}
}
}

@ -11,7 +11,7 @@
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto"/> <RowDefinition Height="Auto"/>
<RowDefinition Height="52" /> <RowDefinition Height="52" />
<RowDefinition/> <RowDefinition Height="*"/>
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Border BorderBrush="#FFB7B7B7" BorderThickness="1" Grid.Row="1" Grid.ColumnSpan="2"> <Border BorderBrush="#FFB7B7B7" BorderThickness="1" Grid.Row="1" Grid.ColumnSpan="2">
@ -42,7 +42,7 @@
</MenuItem> </MenuItem>
</Menu> </Menu>
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Grid.Row="2" Grid.ColumnSpan="2"> <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled" Grid.Row="2" Grid.ColumnSpan="2" VerticalAlignment="Stretch">
<Canvas x:Name="ChannelsCanvas" Background="#FFF2F2F2" VerticalAlignment="Top"/> <Canvas x:Name="ChannelsCanvas" Background="#FFF2F2F2" VerticalAlignment="Top"/>
</ScrollViewer> </ScrollViewer>
@ -162,6 +162,19 @@
</LinearGradientBrush> </LinearGradientBrush>
</Button.Background> </Button.Background>
</Button> </Button>
<Button VerticalContentAlignment="Center" Content="Select All" HorizontalAlignment="Left" Margin="649,0,0,0" VerticalAlignment="Center" Height="46" Width="44" Click="SelectAll_Click" BorderBrush="#FFC1C1C1" BorderThickness="1,1,1,1" FontSize="10" FontFamily="Arial" Grid.Row="1">
<Button.Resources>
<Style TargetType="{x:Type Border}">
<Setter Property="CornerRadius" Value="2"/>
</Style>
</Button.Resources>
<Button.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFF0F0F0" Offset="0.485"/>
<GradientStop Color="#FFDBDBDB" Offset="0.517"/>
</LinearGradientBrush>
</Button.Background>
</Button>
<Image HorizontalAlignment="Left" Margin="514,3,0,3" Grid.Row="1" Width="38" Source="/Assets/page.png" IsHitTestVisible="False"/> <Image HorizontalAlignment="Left" Margin="514,3,0,3" Grid.Row="1" Width="38" Source="/Assets/page.png" IsHitTestVisible="False"/>
<Button Content="" VerticalContentAlignment="Bottom" HorizontalAlignment="Left" Margin="556,0,0,0" VerticalAlignment="Center" Height="46" Width="44" Click="AudioSettings_Click" BorderBrush="#FFC1C1C1" BorderThickness="1,1,1,1" Grid.Row="1" FontSize="10" FontFamily="Arial"> <Button Content="" VerticalContentAlignment="Bottom" HorizontalAlignment="Left" Margin="556,0,0,0" VerticalAlignment="Center" Height="46" Width="44" Click="AudioSettings_Click" BorderBrush="#FFC1C1C1" BorderThickness="1,1,1,1" Grid.Row="1" FontSize="10" FontFamily="Arial">
<Button.Resources> <Button.Resources>

@ -46,6 +46,8 @@ using Nancy;
using Constants = fnecore.Constants; using Constants = fnecore.Constants;
using System.Security.Cryptography; using System.Security.Cryptography;
using fnecore.P25.LC.TSBK; using fnecore.P25.LC.TSBK;
using WebSocketSharp;
using NWaves.Signals;
namespace WhackerLinkConsoleV2 namespace WhackerLinkConsoleV2
{ {
@ -82,10 +84,12 @@ namespace WhackerLinkConsoleV2
private Dictionary<string, SlotStatus> systemStatuses = new Dictionary<string, SlotStatus>(); private Dictionary<string, SlotStatus> systemStatuses = new Dictionary<string, SlotStatus>();
private FneSystemManager _fneSystemManager = new FneSystemManager(); private FneSystemManager _fneSystemManager = new FneSystemManager();
private bool cryptodev = false; private bool cryptodev = true;
private static HashSet<uint> usedRids = new HashSet<uint>(); private static HashSet<uint> usedRids = new HashSet<uint>();
List<Tuple<uint, uint>> fneAffs = new List<Tuple<uint, uint>>();
public MainWindow() public MainWindow()
{ {
#if DEBUG #if DEBUG
@ -195,6 +199,9 @@ namespace WhackerLinkConsoleV2
offsetY += 106; offsetY += 106;
} }
if (File.Exists(system.AliasPath))
system.RidAlias = AliasTools.LoadAliases(system.AliasPath);
if (!system.IsDvm) if (!system.IsDvm)
{ {
_webSocketManager.AddWebSocketHandler(system.Name); _webSocketManager.AddWebSocketHandler(system.Name);
@ -314,6 +321,8 @@ namespace WhackerLinkConsoleV2
{ {
var channelBox = new ChannelBox(_selectedChannelsManager, _audioManager, channel.Name, channel.System, channel.Tgid); var channelBox = new ChannelBox(_selectedChannelsManager, _audioManager, channel.Name, channel.System, channel.Tgid);
channelBox.crypter.AddKey(channel.GetKeyId(), channel.GetAlgoId(), channel.GetEncryptionKey());
if (_settingsManager.ChannelPositions.TryGetValue(channel.Name, out var position)) if (_settingsManager.ChannelPositions.TryGetValue(channel.Name, out var position))
{ {
Canvas.SetLeft(channelBox, position.X); Canvas.SetLeft(channelBox, position.X);
@ -524,14 +533,29 @@ namespace WhackerLinkConsoleV2
if (channel.IsSelected) if (channel.IsSelected)
{ {
uint uniqueRid = GetUniqueRid(system.Rid); uint newTgid = UInt32.Parse(cpgChannel.Tgid);
bool exists = fneAffs.Any(aff => aff.Item2 == newTgid);
Console.WriteLine("sending FNE master aff " + uniqueRid); if (!exists)
fneAffs.Add(new Tuple<uint, uint>(GetUniqueRid(system.Rid), newTgid));
fne.peer.SendMasterGroupAffiliation(uniqueRid, UInt32.Parse(cpgChannel.Tgid)); //Console.WriteLine("FNE Affiliations:");
//foreach (var aff in fneAffs)
//{
// Console.WriteLine($" RID: {aff.Item1}, TGID: {aff.Item2}");
//}
} }
} }
} }
foreach (Codeplug.System system in Codeplug.Systems)
{
if (system.IsDvm)
{
PeerSystem fne = _fneSystemManager.GetFneSystem(system.Name);
fne.peer.SendMasterAffiliationUpdate(fneAffs);
}
}
} }
private void AudioSettings_Click(object sender, RoutedEventArgs e) private void AudioSettings_Click(object sender, RoutedEventArgs e)
@ -610,6 +634,10 @@ namespace WhackerLinkConsoleV2
Buffer.BlockCopy(toneB, 0, combinedAudio, toneA.Length, toneB.Length); Buffer.BlockCopy(toneB, 0, combinedAudio, toneA.Length, toneB.Length);
int chunkSize = 1600; int chunkSize = 1600;
if (system.IsDvm)
chunkSize = 320;
int totalChunks = (combinedAudio.Length + chunkSize - 1) / chunkSize; int totalChunks = (combinedAudio.Length + chunkSize - 1) / chunkSize;
Task.Run(() => Task.Run(() =>
@ -650,12 +678,16 @@ namespace WhackerLinkConsoleV2
} else } else
{ {
PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name); PeerSystem handler = _fneSystemManager.GetFneSystem(system.Name);
// send to DVM
if (chunk.Length == 320)
{
P25EncodeAudioFrame(chunk, handler, channel, cpgChannel, system);
}
} }
} }
}); });
double totalDurationMs = (toneADuration + toneBDuration) + 250; double totalDurationMs = (toneADuration + toneBDuration) * 1000 + 750;
await Task.Delay((int)totalDurationMs); await Task.Delay((int)totalDurationMs);
if (!system.IsDvm) if (!system.IsDvm)
@ -1015,7 +1047,20 @@ namespace WhackerLinkConsoleV2
else if (response.Status == (int)ResponseType.GRANT && response.SrcId != system.Rid && response.DstId == cpgChannel.Tgid) else if (response.Status == (int)ResponseType.GRANT && response.SrcId != system.Rid && response.DstId == cpgChannel.Tgid)
{ {
channel.VoiceChannel = response.Channel; channel.VoiceChannel = response.Channel;
channel.LastSrcId = "Last SRC: " + response.SrcId;
string alias = string.Empty;
try
{
alias = AliasTools.GetAliasByRid(system.RidAlias, int.Parse(response.SrcId));
}
catch (Exception) { }
if (alias.IsNullOrEmpty())
channel.LastSrcId = "Last SRC: " + response.SrcId;
else
channel.LastSrcId = "Last: " + alias;
Dispatcher.Invoke(() => Dispatcher.Invoke(() =>
{ {
channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48"); channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48");
@ -1529,6 +1574,34 @@ namespace WhackerLinkConsoleV2
private void Button_Click(object sender, RoutedEventArgs e) { /* sub */ } private void Button_Click(object sender, RoutedEventArgs e) { /* sub */ }
private void SelectAll_Click(object sender, RoutedEventArgs e)
{
foreach (ChannelBox channel in ChannelsCanvas.Children.OfType<ChannelBox>())
{
if (channel.SystemName == PLAYBACKSYS || channel.ChannelName == PLAYBACKCHNAME || channel.DstId == PLAYBACKTG)
continue;
Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
if (!channel.IsSelected)
{
channel.IsSelected = true;
channel.Background = channel.IsSelected ? (Brush)new BrushConverter().ConvertFrom("#FF0B004B") : Brushes.Gray;
if (channel.IsSelected)
{
_selectedChannelsManager.AddSelectedChannel(channel);
}
else
{
_selectedChannelsManager.RemoveSelectedChannel(channel);
}
}
}
}
/// <summary> /// <summary>
/// Helper to encode and transmit PCM audio as P25 IMBE frames. /// Helper to encode and transmit PCM audio as P25 IMBE frames.
/// </summary> /// </summary>
@ -1562,22 +1635,43 @@ namespace WhackerLinkConsoleV2
smpIdx++; smpIdx++;
} }
// Convert to floats
float[] fSamples = AudioConverter.PcmToFloat(samples);
// Convert to signal
DiscreteSignal signal = new DiscreteSignal(8000, fSamples, true);
// Log.Logger.Debug($"SAMPLE BUFFER {FneUtils.HexDump(samples)}"); // Log.Logger.Debug($"SAMPLE BUFFER {FneUtils.HexDump(samples)}");
// encode PCM samples into IMBE codewords // encode PCM samples into IMBE codewords
byte[] imbe = new byte[FneSystemBase.IMBE_BUF_LEN]; byte[] imbe = new byte[FneSystemBase.IMBE_BUF_LEN];
int tone = 0;
if (true) // TODO: Disable/enable detection
{
tone = channel.toneDetector.Detect(signal);
}
if (tone > 0)
{
MBEToneGenerator.IMBEEncodeSingleTone((ushort)tone, imbe);
Console.WriteLine($"({system.Name}) P25D: {tone} HZ TONE DETECT");
}
else
{
#if WIN32 #if WIN32
if (channel.extFullRateVocoder == null) if (channel.extFullRateVocoder == null)
channel.extFullRateVocoder = new AmbeVocoder(true); channel.extFullRateVocoder = new AmbeVocoder(true);
channel.extFullRateVocoder.encode(samples, out imbe); channel.extFullRateVocoder.encode(samples, out imbe);
#else #else
if (channel.encoder == null) if (channel.encoder == null)
channel.encoder = new MBEEncoder(MBE_MODE.IMBE_88BIT); channel.encoder = new MBEEncoder(MBE_MODE.IMBE_88BIT);
channel.encoder.encode(samples, imbe); channel.encoder.encode(samples, imbe);
#endif #endif
}
// Log.Logger.Debug($"IMBE {FneUtils.HexDump(imbe)}"); // Log.Logger.Debug($"IMBE {FneUtils.HexDump(imbe)}");
// fill the LDU buffers appropriately // fill the LDU buffers appropriately
@ -1742,6 +1836,15 @@ namespace WhackerLinkConsoleV2
short[] samples = new short[FneSystemBase.MBE_SAMPLES_LENGTH]; short[] samples = new short[FneSystemBase.MBE_SAMPLES_LENGTH];
int errs = 0; int errs = 0;
#if WIN32 #if WIN32
if (cryptodev)
{
//Console.WriteLine($"MI: {FneUtils.HexDump(channel.mi)}");
//Console.WriteLine($"Algorithm ID: {channel.algId}");
//Console.WriteLine($"Key ID: {channel.kId}");
channel.crypter.Process(imbe, frameType, n);
}
if (channel.extFullRateVocoder == null) if (channel.extFullRateVocoder == null)
channel.extFullRateVocoder = new AmbeVocoder(true); channel.extFullRateVocoder = new AmbeVocoder(true);
@ -1846,6 +1949,8 @@ namespace WhackerLinkConsoleV2
Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName); Codeplug.System system = Codeplug.GetSystemForChannel(channel.ChannelName);
Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName); Codeplug.Channel cpgChannel = Codeplug.GetChannelByName(channel.ChannelName);
bool isEmergency = false;
if (!system.IsDvm) if (!system.IsDvm)
continue; continue;
@ -1857,9 +1962,6 @@ namespace WhackerLinkConsoleV2
if (cpgChannel.Tgid != e.DstId.ToString()) if (cpgChannel.Tgid != e.DstId.ToString())
continue; continue;
bool ignoreCall = false;
byte callAlgoId = 0x00;
if (!systemStatuses.ContainsKey(system.Name)) if (!systemStatuses.ContainsKey(system.Name))
{ {
systemStatuses[system.Name] = new SlotStatus(); systemStatuses[system.Name] = new SlotStatus();
@ -1872,23 +1974,51 @@ namespace WhackerLinkConsoleV2
SlotStatus slot = systemStatuses[system.Name]; SlotStatus slot = systemStatuses[system.Name];
// if this is an LDU1 see if this is the first LDU with HDU encryption data
if (e.DUID == P25DUID.LDU1)
{
byte frameType = e.Data[180];
// get the initial MI and other enc info (bug found by the screeeeeeeeech on initial tx...)
if (frameType == P25Defines.P25_FT_HDU_VALID)
{
channel.algId = e.Data[181];
channel.kId = (ushort)((e.Data[182] << 8) | e.Data[183]);
Array.Copy(e.Data, 184, channel.mi, 0, P25Defines.P25_MI_LENGTH);
channel.crypter.Prepare(channel.algId, channel.kId, P25Crypto.ProtocolType.P25Phase1, channel.mi);
}
}
// is this a new call stream? // is this a new call stream?
if (e.StreamId != slot.RxStreamId && ((e.DUID != P25DUID.TDU) && (e.DUID != P25DUID.TDULC))) if (e.StreamId != slot.RxStreamId && ((e.DUID != P25DUID.TDU) && (e.DUID != P25DUID.TDULC)))
{ {
channel.IsReceiving = true; channel.IsReceiving = true;
callAlgoId = P25Defines.P25_ALGO_UNENCRYPT;
slot.RxStart = pktTime; slot.RxStart = pktTime;
// Console.WriteLine($"({system.Name}) P25D: Traffic *CALL START * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} [STREAM ID {e.StreamId}]"); // Console.WriteLine($"({system.Name}) P25D: Traffic *CALL START * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} [STREAM ID {e.StreamId}]");
channel.LastSrcId = "Last SRC: " + e.SrcId; string alias = string.Empty;
channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48");
try
{
alias = AliasTools.GetAliasByRid(system.RidAlias, (int)e.SrcId);
}
catch (Exception) { }
if (alias.IsNullOrEmpty())
channel.LastSrcId = "Last SRC: " + e.SrcId;
else
channel.LastSrcId = "Last: " + alias;
if (channel.algId != P25Defines.P25_ALGO_UNENCRYPT)
channel.Background = (Brush)new BrushConverter().ConvertFrom("#ffdeaf0a");
else
channel.Background = (Brush)new BrushConverter().ConvertFrom("#FF00BC48");
} }
// Is the call over? // Is the call over?
if (((e.DUID == P25DUID.TDU) || (e.DUID == P25DUID.TDULC)) && (slot.RxType != FrameType.TERMINATOR)) if (((e.DUID == P25DUID.TDU) || (e.DUID == P25DUID.TDULC)) && (slot.RxType != FrameType.TERMINATOR))
{ {
ignoreCall = false;
callAlgoId = P25Defines.P25_ALGO_UNENCRYPT;
channel.IsReceiving = false; channel.IsReceiving = false;
TimeSpan callDuration = pktTime - slot.RxStart; TimeSpan callDuration = pktTime - slot.RxStart;
// Console.WriteLine($"({system.Name}) P25D: Traffic *CALL END * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} DUR {callDuration} [STREAM ID {e.StreamId}]"); // Console.WriteLine($"({system.Name}) P25D: Traffic *CALL END * PEER {e.PeerId} SRC_ID {e.SrcId} TGID {e.DstId} DUR {callDuration} [STREAM ID {e.StreamId}]");
@ -1896,26 +2026,11 @@ namespace WhackerLinkConsoleV2
return; return;
} }
if (ignoreCall && callAlgoId == P25Defines.P25_ALGO_UNENCRYPT) if (channel.algId != cpgChannel.GetAlgoId() && channel.algId != P25Defines.P25_ALGO_UNENCRYPT)
ignoreCall = false; continue;
// if this is an LDU1 see if this is the first LDU with HDU encryption data
if (e.DUID == P25DUID.LDU1 && !ignoreCall)
{
byte frameType = e.Data[180];
if (frameType == P25Defines.P25_FT_HDU_VALID)
callAlgoId = e.Data[181];
}
if (e.DUID == P25DUID.LDU2 && !ignoreCall)
callAlgoId = data[88];
if (ignoreCall)
return;
bool isEmergency = false;
int count = 0; int count = 0;
switch (e.DUID) switch (e.DUID)
{ {
case P25DUID.LDU1: case P25DUID.LDU1:
@ -1937,10 +2052,8 @@ namespace WhackerLinkConsoleV2
// The '64' record - IMBE Voice 3 + Link Control // The '64' record - IMBE Voice 3 + Link Control
Buffer.BlockCopy(data, count, channel.netLDU1, 50, 17); Buffer.BlockCopy(data, count, channel.netLDU1, 50, 17);
byte serviceOptions = data[count + 3]; byte serviceOptions = data[count + 3];
isEmergency = (serviceOptions & 0x80) == 0x80; isEmergency = (serviceOptions & 0x80) == 0x80;
count += 17; count += 17;
// The '65' record - IMBE Voice 4 + Link Control // The '65' record - IMBE Voice 4 + Link Control
@ -1967,12 +2080,6 @@ namespace WhackerLinkConsoleV2
Buffer.BlockCopy(data, count, channel.netLDU1, 200, 16); Buffer.BlockCopy(data, count, channel.netLDU1, 200, 16);
count += 16; count += 16;
if (cryptodev)
{
if (channel.mi != null && channel.mi.Length > 0)
channel.crypter.Prepare(channel.algId, channel.kId, P25Crypto.ProtocolType.P25Phase1, channel.mi);
}
// decode 9 IMBE codewords into PCM samples // decode 9 IMBE codewords into PCM samples
P25DecodeAudioFrame(channel.netLDU1, e, handler, channel, isEmergency); P25DecodeAudioFrame(channel.netLDU1, e, handler, channel, isEmergency);
} }
@ -2034,9 +2141,6 @@ namespace WhackerLinkConsoleV2
Buffer.BlockCopy(data, count, channel.netLDU2, 200, 16); Buffer.BlockCopy(data, count, channel.netLDU2, 200, 16);
count += 16; count += 16;
if (channel.mi != null && channel.mi.Length > 0 && cryptodev)
channel.crypter.Prepare(channel.algId, channel.kId, P25Crypto.ProtocolType.P25Phase1, channel.mi);
// decode 9 IMBE codewords into PCM samples // decode 9 IMBE codewords into PCM samples
P25DecodeAudioFrame(channel.netLDU2, e, handler, channel, isEmergency, P25Crypto.FrameType.LDU2); P25DecodeAudioFrame(channel.netLDU2, e, handler, channel, isEmergency, P25Crypto.FrameType.LDU2);
} }
@ -2044,6 +2148,9 @@ namespace WhackerLinkConsoleV2
break; break;
} }
if (channel.mi != null && cryptodev)
channel.crypter.Prepare(channel.algId, channel.kId, P25Crypto.ProtocolType.P25Phase1, channel.mi);
slot.RxRFS = e.SrcId; slot.RxRFS = e.SrcId;
slot.RxType = e.FrameType; slot.RxType = e.FrameType;
slot.RxTGId = e.DstId; slot.RxTGId = e.DstId;

@ -1,12 +1,33 @@
// Based on OP25 p25_crypt_algs.cpp /*
* WhackerLink - WhackerLinkConsoleV2
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* Derrived from https://github.com/boatbod/op25/op25/gr-op25_repeater/lib/p25_crypt_algs.cc
*
* Copyright (C) 2025 Caleb, K4PHP
*
*/
namespace WhackerLinkConsoleV2 namespace WhackerLinkConsoleV2
{ {
/// <summary>
/// P25 Crypto class
/// </summary>
public class P25Crypto public class P25Crypto
{ {
private int debug; private ProtocolType protocol;
private int msgqId;
private ProtocolType prType;
private byte algId; private byte algId;
private ushort keyId; private ushort keyId;
private byte[] messageIndicator = new byte[9]; private byte[] messageIndicator = new byte[9];
@ -14,21 +35,31 @@ namespace WhackerLinkConsoleV2
private byte[] adpKeystream = new byte[469]; private byte[] adpKeystream = new byte[469];
private int adpPosition; private int adpPosition;
public P25Crypto(int debug = 0, int msgqId = 0) /// <summary>
/// Creates an instance of <see cref="P25Crypto"/>
/// </summary>
public P25Crypto()
{ {
this.debug = debug; this.protocol = ProtocolType.Unknown;
this.msgqId = msgqId;
this.prType = ProtocolType.Unknown;
this.algId = 0x80; this.algId = 0x80;
this.keyId = 0; this.keyId = 0;
this.adpPosition = 0; this.adpPosition = 0;
} }
/// <summary>
/// Clear keys
/// </summary>
public void Reset() public void Reset()
{ {
keys.Clear(); keys.Clear();
} }
/// <summary>
/// Add key to keys list
/// </summary>
/// <param name="keyid"></param>
/// <param name="algid"></param>
/// <param name="key"></param>
public void AddKey(ushort keyid, byte algid, byte[] key) public void AddKey(ushort keyid, byte algid, byte[] key)
{ {
if (keyid == 0 || algid == 0x80) if (keyid == 0 || algid == 0x80)
@ -37,7 +68,15 @@ namespace WhackerLinkConsoleV2
keys[keyid] = new KeyInfo(algid, key); keys[keyid] = new KeyInfo(algid, key);
} }
public bool Prepare(byte algid, ushort keyid, ProtocolType prType, byte[] MI) /// <summary>
/// Prepare P25 encryption meta data info
/// </summary>
/// <param name="algid"></param>
/// <param name="keyid"></param>
/// <param name="protocol"></param>
/// <param name="MI"></param>
/// <returns></returns>
public bool Prepare(byte algid, ushort keyid, ProtocolType protocol, byte[] MI)
{ {
this.algId = algid; this.algId = algid;
this.keyId = keyid; this.keyId = keyid;
@ -45,19 +84,13 @@ namespace WhackerLinkConsoleV2
if (!keys.ContainsKey(keyid)) if (!keys.ContainsKey(keyid))
{ {
if (debug >= 10)
Console.Error.WriteLine($"P25Crypto::Prepare: KeyID [0x{keyid:X}] not found");
return false; return false;
} }
if (debug >= 10) if (algid == 0xAA)
Console.WriteLine($"P25Crypto::Prepare: KeyID [0x{keyid:X}] found");
if (algid == 0xAA) // ADP RC4
{ {
this.adpPosition = 0; this.adpPosition = 0;
this.prType = prType; this.protocol = protocol;
AdpKeystreamGen(); AdpKeystreamGen();
return true; return true;
} }
@ -65,17 +98,31 @@ namespace WhackerLinkConsoleV2
return false; return false;
} }
/// <summary>
/// Process P25 frames for crypto
/// </summary>
/// <param name="PCW"></param>
/// <param name="frameType"></param>
/// <param name="voiceSubframe"></param>
/// <returns></returns>
public bool Process(byte[] PCW, FrameType frameType, int voiceSubframe) public bool Process(byte[] PCW, FrameType frameType, int voiceSubframe)
{ {
if (!keys.ContainsKey(keyId)) if (!keys.ContainsKey(keyId))
return false; return false;
if (algId == 0xAA) // ADP RC4 if (algId == 0xAA)
return AdpProcess(PCW, frameType, voiceSubframe); return AdpProcess(PCW, frameType, voiceSubframe);
return false; return false;
} }
/// <summary>
/// Process RC4
/// </summary>
/// <param name="PCW"></param>
/// <param name="frameType"></param>
/// <param name="voiceSubframe"></param>
/// <returns></returns>
private bool AdpProcess(byte[] PCW, FrameType frameType, int voiceSubframe) private bool AdpProcess(byte[] PCW, FrameType frameType, int voiceSubframe)
{ {
int offset = 256; int offset = 256;
@ -92,9 +139,8 @@ namespace WhackerLinkConsoleV2
default: return false; default: return false;
} }
if (prType == ProtocolType.P25Phase1) if (protocol == ProtocolType.P25Phase1)
{ {
// FDMA
offset += (adpPosition * 11) + 267 + (adpPosition < 8 ? 0 : 2); offset += (adpPosition * 11) + 267 + (adpPosition < 8 ? 0 : 2);
adpPosition = (adpPosition + 1) % 9; adpPosition = (adpPosition + 1) % 9;
for (int j = 0; j < 11; ++j) for (int j = 0; j < 11; ++j)
@ -102,19 +148,21 @@ namespace WhackerLinkConsoleV2
PCW[j] ^= adpKeystream[j + offset]; PCW[j] ^= adpKeystream[j + offset];
} }
} }
else if (prType == ProtocolType.P25Phase2) else if (protocol == ProtocolType.P25Phase2)
{ {
// TDMA
for (int j = 0; j < 7; ++j) for (int j = 0; j < 7; ++j)
{ {
PCW[j] ^= adpKeystream[j + offset]; PCW[j] ^= adpKeystream[j + offset];
} }
PCW[6] &= 0x80; // Mask everything except MSB of the final codeword PCW[6] &= 0x80;
} }
return true; return true;
} }
/// <summary>
/// Create RC4 key stream
/// </summary>
private void AdpKeystreamGen() private void AdpKeystreamGen()
{ {
byte[] adpKey = new byte[13]; byte[] adpKey = new byte[13];
@ -136,7 +184,6 @@ namespace WhackerLinkConsoleV2
for (; i < 5; i++) for (; i < 5; i++)
adpKey[i] = keySize > 0 ? keyData[i - padding] : (byte)0; adpKey[i] = keySize > 0 ? keyData[i - padding] : (byte)0;
// Append MI bytes
for (i = 5; i < 13; ++i) for (i = 5; i < 13; ++i)
{ {
adpKey[i] = messageIndicator[i - 5]; adpKey[i] = messageIndicator[i - 5];
@ -151,7 +198,7 @@ namespace WhackerLinkConsoleV2
for (i = 0; i < 256; ++i) for (i = 0; i < 256; ++i)
{ {
j = (j + S[i] + K[i]) & 0xFF; j = (j + S[i] + K[i]) & 0xFF;
Swap(ref S[i], ref S[j]); Swap(S, i, j);
} }
i = j = 0; i = j = 0;
@ -159,18 +206,27 @@ namespace WhackerLinkConsoleV2
{ {
i = (i + 1) & 0xFF; i = (i + 1) & 0xFF;
j = (j + S[i]) & 0xFF; j = (j + S[i]) & 0xFF;
Swap(ref S[i], ref S[j]); Swap(S, i, j);
adpKeystream[k] = S[(S[i] + S[j]) & 0xFF]; adpKeystream[k] = S[(S[i] + S[j]) & 0xFF];
} }
} }
private void Swap(ref byte a, ref byte b) /// <summary>
/// Preform a swap
/// </summary>
/// <param name="S"></param>
/// <param name="i"></param>
/// <param name="j"></param>
private void Swap(byte[] S, int i, int j)
{ {
byte temp = a; byte temp = S[i];
a = b; S[i] = S[j];
b = temp; S[j] = temp;
} }
/// <summary>
/// P25 protocol type
/// </summary>
public enum ProtocolType public enum ProtocolType
{ {
Unknown = 0, Unknown = 0,
@ -178,6 +234,9 @@ namespace WhackerLinkConsoleV2
P25Phase2 P25Phase2
} }
/// <summary>
/// P25 frame type
/// </summary>
public enum FrameType public enum FrameType
{ {
Unknown = 0, Unknown = 0,
@ -190,6 +249,9 @@ namespace WhackerLinkConsoleV2
V4_3 V4_3
} }
/// <summary>
/// Key info object
/// </summary>
private class KeyInfo private class KeyInfo
{ {
public byte AlgId { get; } public byte AlgId { get; }

@ -20,6 +20,7 @@
using System.ComponentModel; using System.ComponentModel;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Windows.Controls; using System.Windows.Controls;
using WhackerLinkLib.Models;
namespace WhackerLinkConsoleV2.Controls namespace WhackerLinkConsoleV2.Controls
{ {

@ -48,6 +48,7 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="NAudio" Version="2.2.1" /> <PackageReference Include="NAudio" Version="2.2.1" />
<PackageReference Include="NWaves" Version="0.9.6" />
<PackageReference Include="YamlDotNet" Version="16.2.0" /> <PackageReference Include="YamlDotNet" Version="16.2.0" />
</ItemGroup> </ItemGroup>

@ -52,6 +52,9 @@ zones:
- name: "Channel 1" - name: "Channel 1"
system: "System 1" system: "System 1"
tgid: "2001" tgid: "2001"
keyId: 0x50
algoId: 0xaa
encryptionKey: 0x01, 0x23, 0x45, 0x67, 0x89
- name: "Channel 2" - name: "Channel 2"
system: "System 1" system: "System 1"
tgid: "15002" tgid: "15002"

@ -1 +1 @@
Subproject commit c97660436d2762b544ee2278b4447fb8d7d26357 Subproject commit 34ae0da1f296c1023455db9edc9b854f9b1eb0f2

@ -1 +1 @@
Subproject commit df87bd1ea5a944a272a979469b84e4b5f5f8d108 Subproject commit b4f7f377a71198a78f1859d84b2c30f388969502
Loading…
Cancel
Save

Powered by TurnKey Linux.