#1638 Enhances DMR decoder to include additional encryption details from two major vendor implementations. Adds Hytera short data Radio Registration Service (RRS) processing.

This commit is contained in:
Dennis Sheirer
2023-08-26 07:32:46 -04:00
parent 85b2a38058
commit 5d7b73f876
27 changed files with 662 additions and 291 deletions

View File

@@ -57,6 +57,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcem
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.announcement.VoteNowAdvice;
import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.grant.ChannelGrant;
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
@@ -74,13 +75,17 @@ import io.github.dsheirer.module.decode.dmr.message.data.terminator.Terminator;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceOptions;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceEMBMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.VoiceMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.Arc4EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedParameters;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionParameters;
import io.github.dsheirer.module.decode.event.DecodeEvent;
import io.github.dsheirer.module.decode.event.DecodeEventType;
import io.github.dsheirer.module.decode.event.PlottableDecodeEvent;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacket;
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
import io.github.dsheirer.module.decode.ip.mototrbo.ars.ARSPacket;
import io.github.dsheirer.module.decode.ip.mototrbo.lrrp.LRRPPacket;
import io.github.dsheirer.module.decode.ip.mototrbo.xcmp.XCMPPacket;
import io.github.dsheirer.protocol.Protocol;
import io.github.dsheirer.source.tuner.channel.rotation.AddChannelRotationActiveStateRequest;
import io.github.dsheirer.util.PacketUtil;
@@ -320,6 +325,32 @@ public class DMRDecoderState extends TimeslotDecoderState
.build();
broadcast(smsEvent);
}
//Hytera Short Data - Radio Registration Service (RRS)
else if(packet.getPacket() instanceof HyteraShortDataPacket hsdp)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
StringBuilder sb = new StringBuilder();
sb.append("HYTERA");
if(hsdp.getPacketSequence().isEncrypted())
{
HyteraDataEncryptionHeader hdeh = (HyteraDataEncryptionHeader)hsdp.getPacketSequence().getProprietaryDataHeader();
sb.append(" ENCRYPTED ALGORITHM:").append(hdeh.getAlgorithm());
sb.append(" KEY:").append(hdeh.getKeyId());
sb.append(" IV:").append(hdeh.getIV());
}
sb.append(" SHORT DATA:").append(hsdp.getMessage().toHexString());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(sb.toString())
.build();
broadcast(shortDataEvent);
}
//Unknown Hytera Long Data Service Token Message
else if(packet.getPacket() instanceof HyteraUnknownPacket hyteraUnknownPacket)
{
@@ -328,10 +359,46 @@ public class DMRDecoderState extends TimeslotDecoderState
DecodeEvent unknownTokenEvent = DMRDecodeEvent.builder(DecodeEventType.UNKNOWN_PACKET, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details("HYTERA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString())
.details("HYTERA LONG DATA UNK TOKEN MSG:" + hyteraUnknownPacket.getHeader().toString())
.build();
broadcast(unknownTokenEvent);
}
//Motorola ARS
else if(packet.getPacket() instanceof ARSPacket ars)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.RADIO_REGISTRATION_SERVICE, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(ars.toString())
.build();
broadcast(shortDataEvent);
}
//Motorola LRRP
else if(packet.getPacket() instanceof LRRPPacket lrrp)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.LRRP, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(lrrp.toString())
.build();
broadcast(shortDataEvent);
}
//Motorola XCMP
else if(packet.getPacket() instanceof XCMPPacket xcmp)
{
MutableIdentifierCollection mic = new MutableIdentifierCollection(packet.getIdentifiers());
DecodeEvent shortDataEvent = DMRDecodeEvent.builder(DecodeEventType.XCMP, packet.getTimestamp())
.identifiers(mic)
.timeslot(getTimeslot())
.details(xcmp.toString())
.build();
broadcast(shortDataEvent);
}
else
{
DecodeEvent packetEvent = DMRDecodeEvent.builder(DecodeEventType.DATA_PACKET, packet.getTimestamp())
@@ -380,7 +447,7 @@ public class DMRDecoderState extends TimeslotDecoderState
{
EmbeddedParameters embedded = voiceEmb.getEmbeddedParameters();
if(embedded.getShortBurst() instanceof Arc4EncryptionParameters arc4)
if(embedded.getShortBurst() instanceof EncryptionParameters arc4)
{
updateEncryptedCall(arc4, true, voiceEmb.getTimestamp());
}
@@ -972,6 +1039,15 @@ public class DMRDecoderState extends TimeslotDecoderState
{
switch(message.getOpcode())
{
case FULL_ENCRYPTION_PARAMETERS:
if(message instanceof io.github.dsheirer.module.decode.dmr.message.data.lc.full.EncryptionParameters ep)
{
if(mCurrentCallEvent != null)
{
mCurrentCallEvent.setDetails(ep.getDetails());
}
}
break;
case SHORT_CAPACITY_PLUS_REST_CHANNEL_NOTIFICATION:
if(message instanceof CapacityPlusRestChannel)
{
@@ -1141,7 +1217,7 @@ public class DMRDecoderState extends TimeslotDecoderState
* @param encryptionParameters decoded from the Voice Frame F
* @param isGroup true for group or false for individual call.
*/
private void updateEncryptedCall(Arc4EncryptionParameters encryptionParameters, boolean isGroup, long timestamp)
private void updateEncryptedCall(EncryptionParameters encryptionParameters, boolean isGroup, long timestamp)
{
if(mCurrentCallEvent != null)
{
@@ -1151,7 +1227,7 @@ public class DMRDecoderState extends TimeslotDecoderState
{
details = encryptionParameters.toString();
}
else if(!details.contains(encryptionParameters.toString()))
else if(!details.contains(encryptionParameters.toString()) && !details.contains("ENCRYPTION"))
{
details += " " + encryptionParameters;
}

View File

@@ -33,8 +33,8 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.CSBKMessageFactory
import io.github.dsheirer.module.decode.dmr.message.data.header.ConfirmedDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.DataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.DefinedShortDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
import io.github.dsheirer.module.decode.dmr.message.data.header.MBCHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.PiHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.RawShortDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ResponseDataHeader;
@@ -43,7 +43,7 @@ import io.github.dsheirer.module.decode.dmr.message.data.header.StatusDataHeader
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.UnconfirmedDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.VoiceHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.mbc.MBCContinuationBlock;
@@ -98,7 +98,7 @@ public class DMRDataMessageFactory
return new MBCHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
case CHANNEL_CONTROL_ENC_HEADER:
case PI_HEADER:
return new HeaderMessage(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
return new PiHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
case VOICE_HEADER:
return new VoiceHeader(pattern, getPayload(message), cach, slotType, timestamp, timeslot);
case DATA_ENC_HEADER:
@@ -138,10 +138,10 @@ public class DMRDataMessageFactory
return mprdh;
}
case HYTERA_68:
HyteraProprietaryDataHeader hpdh = new HyteraProprietaryDataHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
hpdh.setValid(valid);
return hpdh;
HyteraDataEncryptionHeader hsdh = new HyteraDataEncryptionHeader(pattern, payload,
cach, slotType, timestamp, timeslot);
hsdh.setValid(valid);
return hsdh;
default:
ProprietaryDataHeader pdh = new ProprietaryDataHeader(pattern, payload, cach,
slotType, timestamp, timeslot);

View File

@@ -34,7 +34,7 @@ import java.util.List;
*/
public abstract class DataMessageWithLinkControl extends DataMessage
{
private LCMessage mLCMessage;
protected LCMessage mLCMessage;
/**
* Constructs an instance.

View File

@@ -56,6 +56,10 @@ public class DataBlock1Rate extends DataBlock
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
if(!isValid())
{
sb.append(" [CRC ERROR]");

View File

@@ -56,6 +56,10 @@ public class DataBlock1_2Rate extends DataBlock
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
if(!isValid())
{
sb.append(" [CRC ERROR]");

View File

@@ -57,6 +57,10 @@ public class DataBlock3_4Rate extends DataBlock
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits());
}
if(!isValid())
{
sb.append(" [CRC ERROR]");

View File

@@ -27,7 +27,6 @@ import java.util.TreeMap;
/**
* Control Signalling Block (CSBK) Opcode enumeration
*
* ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B
* ETSI TS 102 361-4 DMR Trunking Protocol, Annex B
*/
@@ -91,6 +90,8 @@ public enum Opcode
HYTERA_08_ACKNOWLEDGE(Vendor.HYTERA_8, 32, "HYTERA 08 ACKNOWLEDGE"),
HYTERA_08_ANNOUNCEMENT(Vendor.HYTERA_8, 40, "HYTERA 08 ANNOUNCEMENT"),
//CSBKO 44 & 47 observed on Tier3 interleaved in voice group call terminator sequence. 44 was continuously transmitted
//and 47 was only transmitted 3x times in succession in the middle of the terminators and CSBKO 44 messages.
HYTERA_08_CSBKO_44(Vendor.HYTERA_8, 44, "HYTERA 08 CSBKO 44"),
HYTERA_08_CSBKO_47(Vendor.HYTERA_8, 47, "HYTERA 08 CSBKO 47"),
@@ -104,9 +105,9 @@ public enum Opcode
UNKNOWN(Vendor.UNKNOWN, -1, "UNKNOWN");
private Vendor mVendor;
private int mValue;
private String mLabel;
private final Vendor mVendor;
private final int mValue;
private final String mLabel;
/**
* Constructor

View File

@@ -17,18 +17,19 @@
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.data.header.hytera;
package io.github.dsheirer.module.decode.dmr.message.data.header;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCMessageFactory;
/**
* Hytera Proprietary Data Header
* PI/Encryption Header with Link Control
*/
public class HyteraProprietaryDataHeader extends ProprietaryDataHeader
public class PiHeader extends HeaderMessage
{
/**
* Constructs an instance.
@@ -40,23 +41,42 @@ public class HyteraProprietaryDataHeader extends ProprietaryDataHeader
* @param timestamp message was received
* @param timeslot for the DMR burst
*/
public HyteraProprietaryDataHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
public PiHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
{
super(syncPattern, message, cach, slotType, timestamp, timeslot);
}
/**
* Access the embedded link control message
*/
public LCMessage getLCMessage()
{
if(mLCMessage == null)
{
//Overrides default to ignore opcode and construct an encryption parameters message.
mLCMessage = LCMessageFactory.createFullEncryption(getMessage(), getTimestamp(), getTimeslot());
}
return mLCMessage;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(!isValid())
{
sb.append(" [CRC ERROR]");
}
sb.append(" HYTERA PROPRIETARY DATA HEADER");
sb.append(" SAP:").append(getServiceAccessPoint());
sb.append(" MSG:").append(getMessage().toHexString());
sb.append(getSlotType());
if(hasRAS())
{
sb.append(" RAS:").append(getBPTCReservedBits()).append(" ");
}
sb.append(" ").append(getLCMessage());
return sb.toString();
}
}

View File

@@ -0,0 +1,126 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* 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/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.data.header.hytera;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.DMRSyncPattern;
import io.github.dsheirer.module.decode.dmr.message.CACH;
import io.github.dsheirer.module.decode.dmr.message.data.SlotType;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
/**
* Hytera Proprietary Data Header for SAP Short Data
*/
public class HyteraDataEncryptionHeader extends ProprietaryDataHeader
{
private static final int[] ALGORITHM = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] KEY = new int[]{24, 25, 26, 27, 28, 29, 30, 31};
private static final int[] INITIALIZATION_VECTOR = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63};
private static final int[] UNKNOWN = new int[]{64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79};
/**
* Constructs an instance.
*
* @param syncPattern either BASE_STATION_DATA or MOBILE_STATION_DATA
* @param message containing extracted 196-bit payload.
* @param cach for the DMR burst
* @param slotType for this data message
* @param timestamp message was received
* @param timeslot for the DMR burst
*/
public HyteraDataEncryptionHeader(DMRSyncPattern syncPattern, CorrectedBinaryMessage message, CACH cach, SlotType slotType, long timestamp, int timeslot)
{
super(syncPattern, message, cach, slotType, timestamp, timeslot);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getSlotType().getColorCode());
if(!isValid())
{
sb.append(" [CRC ERROR]");
}
sb.append(" HYTERA DATA ENCRYPTION HEADER - SAP:").append(getServiceAccessPoint());
switch(getAlgorithm())
{
case NO_ENCRYPTION:
sb.append(" ALGORITHM:UNENCRYPTED");
break;
case UNKNOWN:
sb.append(" ALGORITHM:").append(getMessage().getInt(ALGORITHM));
break;
default:
sb.append(" ALGORITHM:").append(getAlgorithm());
break;
}
if(getAlgorithm() != EncryptionAlgorithm.NO_ENCRYPTION)
{
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getIV());
sb.append(" UNK:").append(getUnknown());
}
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
/**
* Encryption algorithm.
* @return algorithm or UNKNOWN if the value is not known.
*/
public EncryptionAlgorithm getAlgorithm()
{
return EncryptionAlgorithm.fromValue(getMessage().getInt(ALGORITHM));
}
/**
* Encryption key ID (1-255).
* @return key ID
*/
public int getKeyId()
{
return getMessage().getInt(KEY);
}
/**
* Hex string representation of the initialization vector
* @return IV in hex string.
*/
public String getIV()
{
return getMessage().getHex(INITIALIZATION_VECTOR, 8);
}
/**
* Unknown field.
* @return hex string from the field contents.
*/
public String getUnknown()
{
return getMessage().getHex(UNKNOWN, 4);
}
}

View File

@@ -64,8 +64,8 @@ public class MotorolaDataEncryptionHeader extends ProprietaryDataHeader
sb.append(" MOTOROLA DATA ENCRYPTION HEADER");
sb.append(" SAP:").append(getServiceAccessPoint());
sb.append(" ALGORITHM?:").append(getAlgorithm());
sb.append(" KEY?:").append(getKeyId());
sb.append(" IV?:").append(getInitializationVector());
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getInitializationVector());
sb.append(" UNK:").append(getUnknown());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();

View File

@@ -23,6 +23,7 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.CRCDMR;
import io.github.dsheirer.edac.Checksum_5_DMR;
import io.github.dsheirer.edac.ReedSolomon_12_9_4_DMR;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GPSInformation;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.GroupVoiceChannelUser;
@@ -33,13 +34,11 @@ import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TalkerAliasHead
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.TerminatorData;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnitToUnitVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.UnknownFullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraArc4EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraTerminator;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera.HyteraUnitToUnitVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusEncryptedVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.CapacityPlusWideAreaVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaArc4EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola.MotorolaGroupVoiceChannelUser;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.ActivityUpdateMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.shorty.CapacityPlusRestChannel;
@@ -61,8 +60,20 @@ public class LCMessageFactory
{
private final static Logger mLog = LoggerFactory.getLogger(LCMessageFactory.class);
private static final ReedSolomon_12_9_4_DMR REED_SOLOMON_12_9_4_DMR = new ReedSolomon_12_9_4_DMR();
private static int TERMINATOR_LINK_CONTROL_CRC_MASK = 0x99;
private static int VOICE_LINK_CONTROL_CRC_MASK = 0x96;
private static final int TERMINATOR_LINK_CONTROL_CRC_MASK = 0x99;
private static final int VOICE_LINK_CONTROL_CRC_MASK = 0x96;
/**
* Creates a full link control message specifically for the PI_HEADER message type.
* @param message bits
* @param timestamp of the original message
* @param timeslot of the original message
* @return encryption parameters link control.
*/
public static FullLCMessage createFullEncryption(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
return new EncryptionParameters(message, timestamp, timeslot);
}
/**
* Creates a full link control message
@@ -123,34 +134,9 @@ public class LCMessageFactory
case FULL_CAPACITY_PLUS_WIDE_AREA_VOICE_CHANNEL_USER:
flc = new CapacityPlusWideAreaVoiceChannelUser(message, timestamp, timeslot);
break;
case FULL_ARC4_ENCRYPTION_PARAMETERS:
boolean isHytera = false;
//This is apparently now a DMR standard FLC opcode, even though it's using Motorola's vendor ID (0x10).
//As observed on a known Hytera system, it's not using the standard FLC RS-12/9/4 check, which fails.
//It's apparently using the CRC-CCITT with 0x9696 initial fill. Not sure if both Hytera and Motorola
// are using this FEC. But, by using only 16 bits for checksum versus the standad 24, they're able to
// include a full 24-bit group/radio ID in the message. For now we'll identify as either Motorola
//(using reed solomon) or Hytera (using crc-ccitt).
if(!valid)
{
int bitErrors = CRCDMR.correctCCITT80(message, 0, 80, 0x9696);
if(bitErrors < 2)
{
valid = true;
isHytera = true;
}
}
if(isHytera)
{
flc = new HyteraArc4EncryptionParameters(message, timestamp, timeslot);
}
else
{
flc = new MotorolaArc4EncryptionParameters(message, timestamp, timeslot);
}
case FULL_ENCRYPTION_PARAMETERS:
flc = new EncryptionParameters(message, timestamp, timeslot);
valid = flc.isValid();
break;
case FULL_HYTERA_GROUP_VOICE_CHANNEL_USER:
flc = new HyteraGroupVoiceChannelUser(message, timestamp, timeslot);

View File

@@ -27,7 +27,6 @@ import java.util.TreeMap;
/**
* Full and Short Link Control Opcode enumeration
*
* ETSI TS 102 361-2 DMR Voice & Generic Services, Annex B.1 & B.2
* ETSI TS 102 361-4 DMR Trunking Protocol, Annex B.1 & B.2
*/
@@ -49,7 +48,7 @@ public enum LCOpcode
//Observed on Cap+ Multi-Site System during an encrypted voice call
FULL_CAPACITY_PLUS_ENCRYPTED_VOICE_CHANNEL_USER(Vendor.MOTOROLA_CAPACITY_PLUS, true, 32, "ENCRYPTED VOICE CHANNEL USER"),
//Observed on Cap+ Multi-Site System during an encrypted voice call
FULL_ARC4_ENCRYPTION_PARAMETERS(Vendor.MOTOROLA_CAPACITY_PLUS, true, 33, "ARC4/EP ENCRYPTION PARAMETERS"),
FULL_ENCRYPTION_PARAMETERS(Vendor.MOTOROLA_CAPACITY_PLUS, true, 33, "ENCRYPTION PARAMETERS"),
//Cap+ opcodes from https://forums.radioreference.com/threads/understanding-capacity-plus-trunking-some-more.452566/
//FLCO 0: Group Call Maintenance
//FLCO 3: Private Call Maintenance (TermLC)
@@ -81,10 +80,10 @@ public enum LCOpcode
private static final EnumSet<LCOpcode> TALKER_ALIAS_OPCODES = EnumSet.of(FULL_STANDARD_TALKER_ALIAS_HEADER,
FULL_STANDARD_TALKER_ALIAS_BLOCK_1, FULL_STANDARD_TALKER_ALIAS_BLOCK_2, FULL_STANDARD_TALKER_ALIAS_BLOCK_3);
private Vendor mVendor;
private boolean mFull;
private int mValue;
private String mLabel;
private final Vendor mVendor;
private final boolean mFull;
private final int mValue;
private final String mLabel;
/**
* Constructs an instance

View File

@@ -17,29 +17,30 @@
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera;
package io.github.dsheirer.module.decode.dmr.message.data.lc.full;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.edac.CRCDMR;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.data.lc.LCOpcode;
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
import java.util.ArrayList;
import java.util.List;
/**
* Hytera ARC4/EP Encryption Parameters
* <p>
* Note: observed as FLC payload for a PI_HEADER slot type.
* Note: observed on a Hytera system that was configured as IP Site Connect compatible.
* Encryption Parameters, carried by a PI_HEADER.
*
* Note: this message repurposes the OPCODE field to carry the encryption algorithm.
*/
public class HyteraArc4EncryptionParameters extends FullLCMessage
public class EncryptionParameters extends FullLCMessage
{
private static final int[] KEY_ID = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] INITIALIZATION_VECTOR = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] DESTINATION_GROUP = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70,
71, 72, 73, 74, 75, 76, 77, 78, 79};
//Hytera version uses CRC-CCITT with 0x9696 initial fill, bits: 80-95
//CRC-CCITT with 0x9696 initial fill, bits: 80-95
private DMRTalkgroup mTalkgroup;
private List<Identifier> mIdentifiers;
@@ -50,9 +51,23 @@ public class HyteraArc4EncryptionParameters extends FullLCMessage
* @param timestamp for the message
* @param timeslot of the message
*/
public HyteraArc4EncryptionParameters(CorrectedBinaryMessage message, long timestamp, int timeslot)
public EncryptionParameters(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
int bitErrors = CRCDMR.correctCCITT80(message, 0, 80, 0x9696);
setValid(bitErrors < 2);
}
/**
* Overrides the default method for opcode to return a static value, because the opcode field is repurposed to
* be the algorithm field in both Motorola and Hytera variants of this message, both carried by the Encryption
* Header message.
* @return full encryption parameters opcode.
*/
@Override
public LCOpcode getOpcode()
{
return LCOpcode.FULL_ENCRYPTION_PARAMETERS;
}
@Override
@@ -75,7 +90,17 @@ public class HyteraArc4EncryptionParameters extends FullLCMessage
sb.append(" *RESERVED-BIT*");
}
sb.append("FLC HYTERA ARC4/EP ENCRYPTION PARAMETERS");
sb.append("FLC ENCRYPTION PARAMETERS");
sb.append(" VENDOR:").append(getVendor());
if(getAlgorithm() == EncryptionAlgorithm.UNKNOWN)
{
//List the algorithm (aka opcod) value for unknown algorithms
sb.append(" ALGORITHM:").append(getOpcodeValue());
}
else
{
sb.append(" ALGORITHM:").append(getAlgorithm());
}
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getInitializationVector());
sb.append(" TALKGROUP:").append(getTalkgroup());
@@ -83,6 +108,29 @@ public class HyteraArc4EncryptionParameters extends FullLCMessage
return sb.toString();
}
/**
* Encryption parameter details
* @return details
*/
public String getDetails()
{
StringBuilder sb = new StringBuilder();
sb.append("ENCRYPTION ALGORITHM:").append(getAlgorithm());
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getInitializationVector());
sb.append(" VENDOR:").append(getVendor());
return sb.toString();
}
/**
* Encryption algorithm. Note: this message repurposes the OPCODE field to carry the algorithm ID.
* @return algorithm or UNKNOWN.
*/
public EncryptionAlgorithm getAlgorithm()
{
return EncryptionAlgorithm.fromValue(getOpcodeValue());
}
public DMRTalkgroup getTalkgroup()
{
if(mTalkgroup == null)

View File

@@ -30,7 +30,7 @@ public abstract class FullLCMessage extends LCMessage
{
private static final int ENCRYPTION_FLAG = 0;
private static final int RESERVED = 1;
private static final int[] OPCODE = new int[]{2, 3, 4, 5, 6, 7};
protected static final int[] OPCODE = new int[]{2, 3, 4, 5, 6, 7};
private static final int[] VENDOR = new int[]{8, 9, 10, 11, 12, 13, 14, 15};
/**

View File

@@ -37,11 +37,12 @@ public abstract class HyteraFullLC extends FullLCMessage implements IServiceOpti
protected static final int[] TARGET_ADDRESS = new int[]{32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] PRIORITY_CALL_HASHED_ADDRESS = new int[]{48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] SOURCE_ADDRESS = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
private static final int[] UNKNOWN_1 = new int[]{72, 73, 74, 75};
private static final int[] UNKNOWN_2 = new int[]{76, 77, 78, 79};
//FEC: 72-96
//TODO: Target and Source address fields may be full 24-bit values. How do we distinguish between Hytera repeater
//TODO: and a Hytera XPT repeater? Do the service options identify when it's XPT?
private ServiceOptions mServiceOptions;
private RadioIdentifier mSourceRadio;
protected RadioIdentifier mSourceRadio;
/**
* Constructs an instance.
@@ -110,24 +111,6 @@ public abstract class HyteraFullLC extends FullLCMessage implements IServiceOpti
return String.format("%02X", getMessage().getInt(PRIORITY_CALL_HASHED_ADDRESS)).toUpperCase();
}
/**
* Unknown nibble one. This value remains consistent for the FLC between the Voice Header and the Terminator
* for calls. It does not seem to have correlation to the channel/repeater number
*/
public int getUnknownField1()
{
return getMessage().getInt(UNKNOWN_1);
}
/**
* Unknown nibble two. For each talkgroup, this field shows one value for the Voice Header and the same value but
* bit-inverted in the Terminator.
*/
public int getUnknownField2()
{
return getMessage().getInt(UNKNOWN_2);
}
/**
* Source radio address
*/

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* 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
@@ -21,9 +21,10 @@ package io.github.dsheirer.module.decode.dmr.message.data.lc.full.hytera;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import java.util.ArrayList;
import java.util.List;
@@ -32,6 +33,11 @@ import java.util.List;
*/
public class HyteraGroupVoiceChannelUser extends HyteraFullLC
{
protected static final int[] TALKGROUP = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38,
39, 40, 41, 42, 43, 44, 45, 46, 47};
private static final int[] SOURCE_RADIO = new int[]{48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62,
63, 64, 65, 66, 67, 68, 69, 70, 71};
private TalkgroupIdentifier mTalkgroup;
private List<Identifier> mIdentifiers;
@@ -55,26 +61,11 @@ public class HyteraGroupVoiceChannelUser extends HyteraFullLC
sb.append("[CRC-ERROR] ");
}
sb.append("FLC HYTERA XPT GROUP VOICE CHANNEL USER FM:");
sb.append("FLC HYTERA GROUP VOICE CHANNEL USER FM:");
sb.append(getSourceRadio());
sb.append(" TO:").append(getTalkgroup());
if(isAllChannelsBusy())
{
sb.append(" ALL REPEATERS BUSY");
}
else
{
sb.append(" FREE REPEATER:").append(getFreeRepeater());
}
if(hasPriorityCall())
{
sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress());
sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
}
sb.append(" ").append(getServiceOptions());
sb.append(" UNK1:").append(getUnknownField1());
sb.append(" UNK2:").append(getUnknownField2());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
@@ -85,12 +76,25 @@ public class HyteraGroupVoiceChannelUser extends HyteraFullLC
{
if(mTalkgroup == null)
{
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TARGET_ADDRESS));
mTalkgroup = DMRTalkgroup.create(getMessage().getInt(TALKGROUP));
}
return mTalkgroup;
}
/**
* Source radio address
*/
public RadioIdentifier getSourceRadio()
{
if(mSourceRadio == null)
{
mSourceRadio = DMRRadio.createFrom(getMessage().getInt(SOURCE_RADIO));
}
return mSourceRadio;
}
@Override
public List<Identifier> getIdentifiers()
{

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* 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
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.talkgroup.TalkgroupIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import java.util.ArrayList;
import java.util.List;
@@ -73,8 +72,6 @@ public class HyteraTerminator extends HyteraFullLC
sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
}
sb.append(" ").append(getServiceOptions());
sb.append(" UNK1:").append(getUnknownField1());
sb.append(" UNK2:").append(getUnknownField2());
return sb.toString();
}

View File

@@ -1,6 +1,6 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2020 Dennis Sheirer
* Copyright (C) 2014-2023 Dennis Sheirer
*
* 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
@@ -23,7 +23,6 @@ import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.identifier.radio.RadioIdentifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRRadio;
import java.util.ArrayList;
import java.util.List;
@@ -72,8 +71,6 @@ public class HyteraUnitToUnitVoiceChannelUser extends HyteraFullLC
sb.append(" PRIORITY CALL FOR:").append(getPriorityCallHashedAddress());
sb.append(" ON REPEATER:").append(getPriorityCallRepeater());
}
sb.append(" UNK1:").append(getUnknownField1());
sb.append(" UNK2:").append(getUnknownField2());
sb.append(" ").append(getServiceOptions());
return sb.toString();
}

View File

@@ -1,116 +0,0 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* 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/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.data.lc.full.motorola;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.identifier.DMRTalkgroup;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import java.util.ArrayList;
import java.util.List;
/**
* Motorola ARC4/EP Encryption Parameters
* <p>
* Note: observed as FLC payload for a PI_HEADER slot type.
* Note: observed on a possible Hytera (clone) system that was configured as IP Site Connect compatible.
*/
public class MotorolaArc4EncryptionParameters extends FullLCMessage
{
private static final int[] KEY_ID = new int[]{16, 17, 18, 19, 20, 21, 22, 23};
private static final int[] INITIALIZATION_VECTOR = new int[]{24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37,
38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] DESTINATION_GROUP = new int[]{56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71};
//Motorola Reed Solomon FEC bits: 72-95
private DMRTalkgroup mTalkgroup;
private List<Identifier> mIdentifiers;
/**
* Constructor
* @param message bits
* @param timestamp for the message
* @param timeslot of the message
*/
public MotorolaArc4EncryptionParameters(CorrectedBinaryMessage message, long timestamp, int timeslot)
{
super(message, timestamp, timeslot);
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
if(!isValid())
{
sb.append("[CRC-ERROR] ");
}
if(isEncrypted())
{
sb.append(" *ENCRYPTED*");
}
if(isReservedBitSet())
{
sb.append(" *RESERVED-BIT*");
}
sb.append("FLC MOTOROLA ARC4/EP ENCRYPTION PARAMETERS");
sb.append(" KEY:").append(getKeyId());
sb.append(" IV:").append(getInitializationVector());
sb.append(" TALKGROUP:").append(getTalkgroup());
sb.append(" MSG:").append(getMessage().toHexString());
return sb.toString();
}
public DMRTalkgroup getTalkgroup()
{
if(mTalkgroup == null)
{
mTalkgroup = new DMRTalkgroup(getMessage().getInt(DESTINATION_GROUP));
}
return mTalkgroup;
}
public int getKeyId()
{
return getMessage().getInt(KEY_ID);
}
public String getInitializationVector()
{
return getMessage().getHex(INITIALIZATION_VECTOR, 8);
}
@Override
public List<Identifier> getIdentifiers()
{
if(mIdentifiers == null)
{
mIdentifiers = new ArrayList<>();
mIdentifiers.add(getTalkgroup());
}
return mIdentifiers;
}
}

View File

@@ -24,7 +24,9 @@ import io.github.dsheirer.module.decode.dmr.message.data.csbk.standard.Preamble;
import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
import java.util.ArrayList;
import java.util.List;
@@ -33,12 +35,12 @@ import java.util.List;
*/
public class PacketSequence
{
private int mTimeslot;
private final int mTimeslot;
private PacketSequenceHeader mPacketSequenceHeader;
private UDTHeader mUDTHeader;
private ProprietaryDataHeader mProprietaryDataHeader;
private List<Preamble> mPreambles = new ArrayList<>();
private List<DataBlock> mDataBlocks = new ArrayList<>();
private final List<Preamble> mPreambles = new ArrayList<>();
private final List<DataBlock> mDataBlocks = new ArrayList<>();
public PacketSequence(int timeslot)
{
@@ -96,7 +98,19 @@ public class PacketSequence
*/
public boolean isEncrypted()
{
return hasProprietaryDataHeader() && getProprietaryDataHeader() instanceof MotorolaDataEncryptionHeader;
if(hasProprietaryDataHeader())
{
if(getProprietaryDataHeader() instanceof MotorolaDataEncryptionHeader)
{
return true;
}
else if(getProprietaryDataHeader() instanceof HyteraDataEncryptionHeader hdeh)
{
return hdeh.getAlgorithm() != EncryptionAlgorithm.NO_ENCRYPTION;
}
}
return false;
}
public int getTimeslot()

View File

@@ -25,17 +25,18 @@ import io.github.dsheirer.message.IMessage;
import io.github.dsheirer.module.decode.dmr.message.data.block.DataBlock;
import io.github.dsheirer.module.decode.dmr.message.data.header.HeaderMessage;
import io.github.dsheirer.module.decode.dmr.message.data.header.PacketSequenceHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.ProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.UDTHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MNISProprietaryDataHeader;
import io.github.dsheirer.module.decode.dmr.message.data.header.motorola.MotorolaDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.type.ApplicationType;
import io.github.dsheirer.module.decode.dmr.message.type.DataPacketFormat;
import io.github.dsheirer.module.decode.dmr.message.type.ServiceAccessPoint;
import io.github.dsheirer.module.decode.ip.DefinedShortDataPacket;
import io.github.dsheirer.module.decode.ip.UnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraTokenHeader;
import io.github.dsheirer.module.decode.ip.hytera.sds.HyteraUnknownPacket;
import io.github.dsheirer.module.decode.ip.hytera.shortdata.HyteraShortDataPacket;
import io.github.dsheirer.module.decode.ip.hytera.sms.HyteraSmsPacket;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Header;
import io.github.dsheirer.module.decode.ip.ipv4.IPV4Packet;
@@ -167,19 +168,30 @@ public class PacketSequenceMessageFactory
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
}
else if(secondaryHeader instanceof HyteraProprietaryDataHeader)
else if(secondaryHeader instanceof HyteraDataEncryptionHeader hdeh)
{
HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet);
ServiceAccessPoint sap = hdeh.getServiceAccessPoint();
if(hyteraTokenHeader.isSMSMessage())
switch(sap)
{
return new DMRPacketMessage(packetSequence, new HyteraSmsPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
else
{
return new DMRPacketMessage(packetSequence, new HyteraUnknownPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
case SHORT_DATA:
return new DMRPacketMessage(packetSequence, new HyteraShortDataPacket(packetSequence, packet), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
case PROPRIETARY_DATA:
HyteraTokenHeader hyteraTokenHeader = new HyteraTokenHeader(packet);
if(hyteraTokenHeader.isSMSMessage())
{
return new DMRPacketMessage(packetSequence, new HyteraSmsPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
else
{
return new DMRPacketMessage(packetSequence, new HyteraUnknownPacket(hyteraTokenHeader), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
default:
return new DMRPacketMessage(packetSequence, new UnknownPacket(packet, 0), packet,
packetSequence.getTimeslot(), packetSequence.getPacketSequenceHeader().getTimestamp());
}
}
else if(secondaryHeader instanceof MotorolaDataEncryptionHeader &&
@@ -248,7 +260,7 @@ public class PacketSequenceMessageFactory
if(sequence.hasProprietaryDataHeader())
{
CorrectedBinaryMessage prefix = ((ProprietaryDataHeader)sequence.getProprietaryDataHeader()).getPacketPrefix();
CorrectedBinaryMessage prefix = sequence.getProprietaryDataHeader().getPacketPrefix();
if(prefix != null)
{

View File

@@ -0,0 +1,82 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* 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/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.dmr.message.type;
/**
* Enumeration of encryption algorithms
*/
public enum EncryptionAlgorithm
{
NO_ENCRYPTION(0x00, "NO ENCRYPTION"),
HYTERA_BASIC_PRIVACY(0x01, "HYTERA BP"),
HYTERA_ENHANCED_PRIVACY(0x02, "HYTERA RC4/EP"),
DMRA_RC4(0x21, "DMRA RC4/EP"),
DMRA_AES128(0x24, "DMRA AES128"),
DMRA_AES256(0x25, "DMRA AES256" ),
HYTERA_ENHANCED_PRIVACY_2(0x26, "HYTERA RC4/EP"),
UNKNOWN(-1, "UNKNOWN");
private final int mValue;
private final String mLabel;
/**
* Constructor
* @param value for the algorithm
* @param label to display
*/
EncryptionAlgorithm(int value, String label)
{
mValue = value;
mLabel = label;
}
/**
* Numeric value for the algorithm
* @return value.
*/
public int getValue()
{
return mValue;
}
/**
* Utility method to lookup the encryption from the value.
* @param value to lookup
* @return matching entry or UNKNOWN
*/
public static EncryptionAlgorithm fromValue(int value)
{
for(EncryptionAlgorithm algorithm: EncryptionAlgorithm.values())
{
if(algorithm.getValue() == value)
{
return algorithm;
}
}
return UNKNOWN;
}
@Override
public String toString()
{
return mLabel;
}
}

View File

@@ -25,8 +25,8 @@ import io.github.dsheirer.edac.BPTC_16_2;
import io.github.dsheirer.edac.Golay24;
import io.github.dsheirer.module.decode.dmr.message.IServiceOptionsProvider;
import io.github.dsheirer.module.decode.dmr.message.data.lc.full.FullLCMessage;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.Arc4EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EmbeddedParameters;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.EncryptionParameters;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.NonStandardShortBurst;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.NullShortBurst;
import io.github.dsheirer.module.decode.dmr.message.voice.embedded.ShortBurst;
@@ -38,8 +38,8 @@ import io.github.dsheirer.module.decode.dmr.message.voice.embedded.UnknownShortB
* Monitors audio call voice frame messaging to detect encrypted audio calls and extract the encryption parameters
* that are embedded into the six voice frames that comprise a voice super-frame.
* <p>
* See patent: https://patents.google.com/patent/EP2347540B1/en - embedding encryption parameters in voice super frame
* See patent: https://patents.google.com/patent/US8271009B2 - TXI - interrupting voice transmissions
* See patent: <a href="https://patents.google.com/patent/EP2347540B1/en">...</a> - embedding encryption parameters in voice super frame
* See patent: <a href="https://patents.google.com/patent/US8271009B2">...</a> - TXI - interrupting voice transmissions
*/
public class VoiceSuperFrameProcessor
{
@@ -47,8 +47,6 @@ public class VoiceSuperFrameProcessor
33, 34, 35, 48, 49, 50, 51, 52, 53, 54, 55};
private static final int[] CRC4 = new int[]{56, 57, 58, 59};
private boolean mEncrypted = false;
private int mAlgorithm;
private int mKey;
private byte[] mFragmentA;
private byte[] mFragmentB;
private byte[] mFragmentC;
@@ -83,8 +81,6 @@ public class VoiceSuperFrameProcessor
private void softReset()
{
mAlgorithm = 0;
mKey = 0;
mFragmentA = null;
mFragmentB = null;
mFragmentC = null;
@@ -124,9 +120,6 @@ public class VoiceSuperFrameProcessor
{
if(voiceMessage instanceof VoiceEMBMessage voiceEMB)
{
boolean valid = voiceEMB.getEMB().isValid();
boolean encrypted = voiceEMB.getEMB().isEncrypted();
if(voiceEMB.getEMB().isValid() && voiceEMB.getEMB().isEncrypted())
{
mEncrypted = true;
@@ -175,7 +168,6 @@ public class VoiceSuperFrameProcessor
* initialization vector to return a parameters object.
*
* @param frameFFragment containing the short-burst FLC fragment
* @param iv that was previously extracted from the voice super-frame
* @return embedded parameters.
*/
private ShortBurst extractShortBurst(BinaryMessage frameFFragment)
@@ -191,7 +183,7 @@ public class VoiceSuperFrameProcessor
return switch(opcode)
{
case NULL -> new NullShortBurst(decoded);
case ARC4_ENCRYPTION -> new Arc4EncryptionParameters(decoded);
case ARC4_ENCRYPTION, AES128_ENCRYPTION, AES256_ENCRYPTION -> new EncryptionParameters(decoded);
case TXI_DELAY -> new TransmitInterrupt(decoded);
default -> new UnknownShortBurst(decoded);
};
@@ -233,11 +225,11 @@ public class VoiceSuperFrameProcessor
if(passes)
{
return String.format("%04X", iv);
return String.format("%08X", iv);
}
else
{
return String.format("%04X", iv) + "(CRC-FAIL " + check1 + "/" + check2 + "/" + check3 + "/" +
return String.format("%08X", iv) + "(CRC-FAIL " + check1 + "/" + check2 + "/" + check3 + "/" +
reassembled.getCorrectedBitCount() + ")";
}
}
@@ -262,10 +254,10 @@ public class VoiceSuperFrameProcessor
*/
public static boolean crc4(int value, int crc)
{
long checksum = (value & 0x0FFFFFFFFl) << 4;
long checksum = (value & 0x0FFFFFFFFL) << 4;
checksum ^= 0xF; //Initial fill
long polynomial = 0x013l << 31;
long checkBit = 0x1l << 35;
long polynomial = 0x013L << 31;
long checkBit = 0x1L << 35;
for(int x = 31; x >= 0; x--)
{

View File

@@ -20,11 +20,12 @@
package io.github.dsheirer.module.decode.dmr.message.voice.embedded;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.module.decode.dmr.message.type.EncryptionAlgorithm;
/**
* Encryption parameters short burst payload.
*/
public class Arc4EncryptionParameters extends ShortBurst
public class EncryptionParameters extends ShortBurst
{
private static final int[] KEY = new int[]{0, 1, 2, 3, 4, 5, 6, 7};
private static final int[] ALGORITHM = new int[]{8, 9, 10};
@@ -34,7 +35,7 @@ public class Arc4EncryptionParameters extends ShortBurst
*
* @param message containing the de-interleaved and error-corrected short burst payload.
*/
public Arc4EncryptionParameters(CorrectedBinaryMessage message)
public EncryptionParameters(CorrectedBinaryMessage message)
{
super(message);
}
@@ -49,31 +50,45 @@ public class Arc4EncryptionParameters extends ShortBurst
}
/**
* Encryption algorithm
* @return algorithm (0 - 7). 1 = ARC4
* Encryption algorithm value
* @return algorithm (0 - 7).
*/
public int getAlgorithmValue()
{
return getMessage().getInt(ALGORITHM);
}
public String getAlgorithm()
/**
* Encryption algorithm.
* @return algorithm
*/
public EncryptionAlgorithm getAlgorithm()
{
int algorithm = getAlgorithmValue();
if(algorithm == 1)
return switch(algorithm)
{
return "EP/ARC4";
}
return "ALGORITHM:" + algorithm;
case 1 -> EncryptionAlgorithm.DMRA_RC4;
case 4 -> EncryptionAlgorithm.DMRA_AES128;
case 5 -> EncryptionAlgorithm.DMRA_AES256;
default -> EncryptionAlgorithm.UNKNOWN;
};
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("ENCRYPTION:").append(getAlgorithm());
sb.append("ENCRYPTION ALGORITHM:");
if(getAlgorithm() == EncryptionAlgorithm.UNKNOWN)
{
sb.append(getAlgorithmValue());
}
else
{
sb.append(getAlgorithm());
}
sb.append(" KEY:").append(getKey());
return sb.toString();
}

View File

@@ -21,17 +21,18 @@ package io.github.dsheirer.module.decode.dmr.message.voice.embedded;
/**
* Short Burst opcode
*
* See: https://patents.google.com/patent/US8271009B2 Page 12
* See: <a href="https://patents.google.com/patent/US8271009B2">...</a> Page 12
*/
public enum ShortBurstOpcode
{
NULL(0),
ARC4_ENCRYPTION(1),
TXI_DELAY(3),
AES128_ENCRYPTION(4),
AES256_ENCRYPTION(5),
UNKNOWN(-1);
private int mValue;
private final int mValue;
/**
* Constructor

View File

@@ -67,6 +67,7 @@ public enum DecodeEventType
PAGE("Page"),
QUERY("Query"),
RADIO_CHECK("Radio Check"),
RADIO_REGISTRATION_SERVICE("Hytera RRS"),
REGISTER("Register"),
REGISTER_ESN("ESN"),
REQUEST("Request"),
@@ -78,9 +79,10 @@ public enum DecodeEventType
STATUS("Status"),
UDP_PACKET("UDP/IP Packet"),
UNKNOWN_PACKET("Unknown Packet"),
XCMP("Motorola XCMP"),
UNKNOWN("Unknown");
private String mLabel;
private final String mLabel;
/**
* Encrypted voice call event types for filtering

View File

@@ -0,0 +1,120 @@
/*
* *****************************************************************************
* Copyright (C) 2014-2023 Dennis Sheirer
*
* 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/>
* ****************************************************************************
*/
package io.github.dsheirer.module.decode.ip.hytera.shortdata;
import io.github.dsheirer.bits.CorrectedBinaryMessage;
import io.github.dsheirer.identifier.Identifier;
import io.github.dsheirer.module.decode.dmr.message.data.header.hytera.HyteraDataEncryptionHeader;
import io.github.dsheirer.module.decode.dmr.message.data.packet.PacketSequence;
import io.github.dsheirer.module.decode.ip.IHeader;
import io.github.dsheirer.module.decode.ip.IPacket;
import java.util.List;
/**
* Hytera Short Data Packet Sequence
*/
public class HyteraShortDataPacket implements IPacket
{
private final PacketSequence mPacketSequence;
private CorrectedBinaryMessage mMessage;
/**
* Constructor
* @param packetSequence containing the headers and data blocks
* @param payload containing the reconstructed short data message from the data blocks.
*/
public HyteraShortDataPacket(PacketSequence packetSequence, CorrectedBinaryMessage payload)
{
mPacketSequence = packetSequence;
mMessage = payload;
}
@Override
public String toString()
{
StringBuilder sb = new StringBuilder();
sb.append("CC:").append(getPacketSequence().getPacketSequenceHeader().getSlotType().getColorCode());
sb.append(" HYTERA SHORT DATA MESSAGE FM:").append(getPacketSequence().getPacketSequenceHeader().getSourceLLID());
sb.append(" TO:").append(getPacketSequence().getPacketSequenceHeader().getDestinationLLID());
if(getPacketSequence().isEncrypted())
{
HyteraDataEncryptionHeader hdeh = (HyteraDataEncryptionHeader)getPacketSequence().getProprietaryDataHeader();
sb.append(" ENCRYPTED ALGORITHM:").append(hdeh.getAlgorithm());
sb.append(" KEY:").append(hdeh.getKeyId());
sb.append(" IV:").append(hdeh.getIV());
}
sb.append(" SHORT DATA:").append(getMessage().toHexString());
return sb.toString();
}
@Override
public IHeader getHeader()
{
return null;
}
@Override
public IPacket getPayload()
{
return null;
}
@Override
public boolean hasPayload()
{
return false;
}
/**
* Payload message
* @return message
*/
public CorrectedBinaryMessage getMessage()
{
return mMessage;
}
/**
* Captured packet sequence
* @return packet sequence
*/
public PacketSequence getPacketSequence()
{
return mPacketSequence;
}
/**
* Indicates if this packet is encrypted, meaning it contains a Hytera Data Encryption Header.
* @return true if encrypted
*/
public boolean isEncrypted()
{
return getPacketSequence().isEncrypted();
}
@Override
public List<Identifier> getIdentifiers()
{
return getPacketSequence().getPacketSequenceHeader().getIdentifiers();
}
}