class ScanPlugin(PluginBase):
    @arg("--speed",
         action="store",
         dest="speed",
         type=int,
         help="Scan speed. Value range 0-5.",
         default=4)
    @arg_group(
        name="RTL-SDR configuration",
        args=[
            arg("-p",
                action="store",
                dest="ppm",
                type=int,
                help="Set ppm. Default: value from config file."),
            arg("-s",
                action="store",
                dest="samp_rate",
                type=float,
                help="Set sample rate. Default: value from config file."),
            arg("-g",
                action="store",
                type=float,
                dest="gain",
                help="Set gain. Default: value from config file.")
        ])
    @arg("-b",
         action="store",
         dest="band",
         choices=(grgsm.arfcn.get_bands()),
         help="GSM band of the ARFCN.")
    @cmd(name="scan_rtlsdr",
         description="Scan a GSM band using a RTL-SDR device.")
    def scan_rtlsdr(self, args):

        if args.speed < 0 or args.speed > 5:
            raise PluginError("Invalid speed")

        path = self._config_provider.get("gr-gsm", "apps_path")
        grgsm_scanner = imp.load_source("",
                                        os.path.join(path, "grgsm_scanner"))

        band = args.band
        sample_rate = args.samp_rate
        ppm = args.ppm
        gain = args.gain
        speed = args.speed

        if ppm is None:
            ppm = self._config_provider.getint("rtl_sdr", "ppm")
        if sample_rate is None:
            sample_rate = self._config_provider.getint("rtl_sdr",
                                                       "sample_rate")
        if gain is None:
            gain = self._config_provider.getint("rtl_sdr", "gain")

        channels_num = int(sample_rate / 0.2e6)

        for arfcn_range in grgsm.arfcn.get_arfcn_ranges(args.band):
            first_arfcn = arfcn_range[0]
            last_arfcn = arfcn_range[1]
            last_center_arfcn = last_arfcn - int((channels_num / 2) - 1)

            current_freq = grgsm.arfcn.arfcn2downlink(
                first_arfcn + int(channels_num / 2) - 1, band)
            last_freq = grgsm.arfcn.arfcn2downlink(last_center_arfcn, band)
            stop_freq = last_freq + 0.2e6 * channels_num

            while current_freq < stop_freq:
                # silence rtl_sdr output:
                # open 2 fds
                null_fds = [os.open(os.devnull, os.O_RDWR) for x in xrange(2)]
                # save the current file descriptors to a tuple
                save = os.dup(1), os.dup(2)
                # put /dev/null fds on 1 and 2
                os.dup2(null_fds[0], 1)
                os.dup2(null_fds[1], 2)

                # instantiate scanner and processor
                scanner = grgsm_scanner.wideband_scanner(
                    rec_len=6 - speed,
                    sample_rate=sample_rate,
                    carrier_frequency=current_freq,
                    ppm=ppm,
                    args="")
                # start recording
                scanner.start()
                scanner.wait()
                scanner.stop()

                freq_offsets = numpy.fft.ifftshift(
                    numpy.array(
                        range(int(-numpy.floor(channels_num / 2)),
                              int(numpy.floor((channels_num + 1) / 2)))) * 2e5)
                detected_c0_channels = scanner.gsm_extract_system_info.get_chans(
                )

                found_list = []

                if detected_c0_channels:
                    chans = numpy.array(
                        scanner.gsm_extract_system_info.get_chans())
                    found_freqs = current_freq + freq_offsets[(chans)]

                    cell_ids = numpy.array(
                        scanner.gsm_extract_system_info.get_cell_id())
                    lacs = numpy.array(
                        scanner.gsm_extract_system_info.get_lac())
                    mccs = numpy.array(
                        scanner.gsm_extract_system_info.get_mcc())
                    mncs = numpy.array(
                        scanner.gsm_extract_system_info.get_mnc())
                    ccch_confs = numpy.array(
                        scanner.gsm_extract_system_info.get_ccch_conf())
                    powers = numpy.array(
                        scanner.gsm_extract_system_info.get_pwrs())

                    for i in range(0, len(chans)):
                        cell_arfcn_list = scanner.gsm_extract_system_info.get_cell_arfcns(
                            chans[i])
                        neighbour_list = scanner.gsm_extract_system_info.get_neighbours(
                            chans[i])

                        info = grgsm_scanner.channel_info(
                            grgsm.arfcn.downlink2arfcn(found_freqs[i], band),
                            found_freqs[i], cell_ids[i], lacs[i], mccs[i],
                            mncs[i], ccch_confs[i], powers[i], neighbour_list,
                            cell_arfcn_list)
                        found_list.append(info)

                scanner = None

                # restore file descriptors so we can print the results
                os.dup2(save[0], 1)
                os.dup2(save[1], 2)
                # close the temporary fds
                os.close(null_fds[0])
                os.close(null_fds[1])

                for info in sorted(found_list):
                    self.printmsg(info.__repr__())

                current_freq += channels_num * 0.2e6
class A51ReconstructionPlugin(PluginBase):
    attack_modes = ['SDCCH', 'SACCH', 'SDCCH/SACCH']
    channel_modes = ['BCCH', 'BCCH_SDCCH4', 'SDCCH8']

    @arg(
        "-m",
        action="store",
        dest="mode",
        choices=channel_modes,
        help=
        "Channel mode. This determines on which channels to search for messages that can be cracked.",
        default="BCCH_SDCCH4")
    @arg(
        "--attack-mode",
        action="store",
        dest="attackmode",
        choices=attack_modes,
        help=
        "Attack mode. This determines on which channels to search for messages that can be cracked.",
        default="SDCCH/SACCH")
    @arg("-t",
         action="store",
         dest="timeslot",
         type=int,
         help="Timeslot of the Immediate Assignment or Cipher Mode Command.",
         default=0)
    @arg("-v",
         action="store_true",
         dest="verbose",
         help="If enabled the command displays verbose information.")
    @arg_exclusive(args=[
        arg("--cfile", action="store_path", dest="cfile", help="cfile."),
        arg("--bursts", action="store_path", dest="bursts", help="bursts.")
    ])
    @arg_group(
        name="Cfile Options",
        args=[
            arg("-a",
                action="store",
                dest="arfcn",
                type=int,
                help="ARFCN of the cfile capture."),
            arg("-f",
                action="store",
                dest="freq",
                type=float,
                help="Frequency of the cfile capture."),
            arg("-b",
                action="store",
                dest="band",
                choices=grgsm.arfcn.get_bands(),
                help="GSM of the cfile capture."),
            arg("-p",
                action="store",
                dest="ppm",
                type=int,
                help="Set ppm. Default: value from config file."),
            arg("-s",
                action="store",
                dest="samp_rate",
                type=float,
                help="Set sample rate. Default: value from config file."),
            arg("-g",
                action="store",
                type=float,
                dest="gain",
                help="Set gain. Default: value from config file.")
        ])
    @arg_exclusive(args=[
        arg("--frame-ia",
            action="store",
            dest="fnr_ia",
            type=int,
            help="Framenumber of the Immediate Assignment."),
        arg("--frame-cmc",
            action="store",
            dest="fnr_cmc",
            type=int,
            help="Framenumber of the Cipher Mode Command.")
    ])
    @cmd(
        name="a51_kraken",
        description=
        "Reconstruct A51 session key from captured messages using Kraken TMTO."
    )
    def a51_kraken(self, args):
        fnr_cmc = args.fnr_cmc
        timeslot = args.timeslot
        subchannel = None
        is_cmc_provided = False
        burst_file = args.bursts
        mode = args.mode

        if args.fnr_cmc is not None:
            is_cmc_provided = True
        elif args.fnr_ia is not None:
            ia_extractor = ImmediateAssignmentExtractor(
                burst_file, timeslot, mode, args.fnr_ia)
            ia_extractor.start()
            ia_extractor.wait()

            error = True
            mode = "BCCH_SDCCH4"
            immediate_assignments = ia_extractor.extract_immediate_assignment.get_frame_numbers(
            )
            for i in range(len(immediate_assignments)):
                if immediate_assignments[i] == args.fnr_ia:
                    self.printmsg("Immediate Assignment at %s" %
                                  immediate_assignments[i])
                    timeslot = ia_extractor.extract_immediate_assignment.get_timeslots(
                    )[i]
                    subchannel = ia_extractor.extract_immediate_assignment.get_subchannels(
                    )[i]
                    if ia_extractor.extract_immediate_assignment.get_channel_types(
                    )[i] == "SDCCH/8":
                        mode = "SDCCH8"
                    error = False
                    break
            if error:
                self.printmsg(
                    "No valid framenumber for immediate assignment was provided."
                )
                return

            cmc_finder = CMCFinder(burst_file, timeslot, subchannel, mode,
                                   args.fnr_ia)  # ToDo: channeltype from ia
            cmc_finder.start()
            cmc_finder.wait()

            fnr_cmc = cmc_finder.get_cmc()
            if fnr_cmc is None:
                self.printmsg("No cipher mode command was found.")
                return
        else:
            self.printmsg(
                "No valid framenumber for cipher mode command or immediate assignment was provided."
            )
            return

        fnr_start = fnr_cmc - 2 * 102  # should be (args.fnr_cmc - 3 * 102 + max_fnr) mod max_fnr
        fnr_end = fnr_cmc + 3 * 102 + 3  # should be (args.fnr_cmc + 3 * 102 + 3) mod max_fnr

        cmc_analyzer = CMCAnalyzer(timeslot, burst_file, mode, fnr_start,
                                   fnr_end)
        cmc_analyzer.start()
        cmc_analyzer.wait()

        if not cmc_analyzer.is_a51_cmc(fnr_cmc):
            self.printmsg("Cipher Mode Command at %s does not assign A5/1" %
                          fnr_cmc)
            return
        else:
            self.printmsg("Cipher Mode Command at %s" % fnr_cmc)

        if is_cmc_provided:
            subchannel = cmc_analyzer.get_subchannel(fnr_cmc)

        kraken_burst_sets = cmc_analyzer.createLapdmUiBurstSets(fnr_cmc)

        kraken_adapter = KrakenA51ReconstructorAdapter(self._config_provider)

        key_found = False

        if args.attackmode != "SACCH":
            sdcch_counter = 0
            for burst_set in kraken_burst_sets:
                if sdcch_counter % 4 == 0 and args.verbose:
                    self.printmsg(
                        "Using SDCCH message bursts %s - %s" %
                        (burst_set.frame_number, burst_set.frame_number + 4))
                sdcch_counter += 1

                key = kraken_adapter.send2kraken(burst_set, args.verbose)
                if key is not None:
                    key_found = True
                    self.printmsg("Key found: %s" % key)
                    break
                else:
                    # self.printmsg("%s - no key found" % burst_set.frame_number)
                    pass

        if key_found or args.attackmode == "SDCCH":
            return

        # self.printmsg("Starting attack on SACCH")

        last_sit_fnr = -1
        last_si_type = None
        timingadvance = -1

        plaintext_si_msgs = dict()

        for sit_fnr in cmc_analyzer.sacch_sits:
            if sit_fnr > last_sit_fnr and sit_fnr < fnr_cmc:
                last_sit_fnr = sit_fnr
                # extract timing advance
                last_si_type = cmc_analyzer.sacch_sits[sit_fnr][1]
                data_string = cmc_analyzer.sacch_sits[sit_fnr][2]
                # byte_arr = array.array('B', data_string.decode("hex"))

                byte_list = self.byte_string_to_list(data_string)
                timingadvance = byte_list[1]

                # add the system information messages from the attacked sacch
                # those should have the right timing advance anyway (at least in most cases)
                if not plaintext_si_msgs.has_key(last_si_type):
                    plaintext_si_msgs[last_si_type] = byte_list

        if last_sit_fnr == -1:
            self.printmsg(
                "Could not determine last System Information message")
            return
        #self.printmsg("Last SI message at " + str(last_sit_fnr))

        si_collector = SICollector(timeslot, burst_file, mode)
        si_collector.start()
        si_collector.wait()

        # collect all system information message types used on SACCH by the network
        for t in si_collector.si_messages:
            # there can be at most four different system information message types on SACCH.
            if len(plaintext_si_msgs) >= 4:
                break

            # if the type is not in the plaintext dictionary or has another timing advance
            # we put it in the dict
            if not plaintext_si_msgs.has_key(
                    t) or plaintext_si_msgs[t][1] != timingadvance:
                plaintext_si_msgs[t] = self.byte_string_to_list(
                    si_collector.si_messages[t])

        for msg in plaintext_si_msgs:
            # correct timing advance
            if plaintext_si_msgs[msg][1] != timingadvance:
                plaintext_si_msgs[msg][1] = timingadvance

        # create bursts for all system information message types
        plaintext_si_bursts = dict()
        for msg in plaintext_si_msgs:
            plaintext_si_bursts[msg] = self.message_to_bursts(
                plaintext_si_msgs[msg])

        sacch_si_types = [
            "System Information Type 5", "System Information Type 5bis",
            "System Information Type 5ter", "System Information Type 6"
        ]
        if not plaintext_si_msgs.has_key("System Information Type 5bis"):
            sacch_si_types.remove("System Information Type 5bis")
            if not plaintext_si_msgs.has_key("System Information Type 5ter"):
                sacch_si_types.remove("System Information Type 5ter")

        type_pool = cycle(sacch_si_types)
        dropwhile(lambda x: x != last_si_type, type_pool)
        next(type_pool
             )  # next one would last_si_type, which we use as starting point

        # assemble burst sets
        sacch_burst_sets = []
        for i in range(1, 4):
            type_of_msg = next(type_pool)  # expected type of next message
            fnr_of_msg = last_sit_fnr + i * 102

            bursts_of_plaintext = plaintext_si_bursts[type_of_msg]

            for j in range(0, 4):
                fnr = fnr_of_msg + j
                check_burst_index = 0 if j > 0 else 1
                sacch_burst_sets.append(
                    A5BurstSet(
                        fnr,  # framenumber of the burst we want to use
                        cmc_analyzer.bursts[
                            fnr],  # data (payload) of the burst we want to use
                        bursts_of_plaintext[
                            j],  # plaintext data (payload) of a lapdm ui message
                        fnr_of_msg +
                        check_burst_index,  # framenumber of verification burst.
                        # we use the first burst of the message as check burst, if j > 0
                        cmc_analyzer.bursts[
                            fnr_of_msg + check_burst_index
                        ],  # data (payload) of the verification burst
                        bursts_of_plaintext[
                            check_burst_index]  # plaintextdata (payload) of
                        # the verification burst
                    ))

        sacch_counter = 0
        for burst_set in sacch_burst_sets:
            if sacch_counter % 4 == 0 and args.verbose:
                self.printmsg(
                    "Using SACCH message bursts %s - %s" %
                    (burst_set.frame_number, burst_set.frame_number + 4))
                sacch_counter += 1

            key = kraken_adapter.send2kraken(burst_set, args.verbose)
            if key is not None:
                key_found = True
                self.printmsg("Key found: %s" % key)
                break
            else:
                pass
                # self.printmsg("%s - no key found" % burst_set.frame_number)

        # self.printmsg("I am done....")
        # Todo: look at a lapdm ui message: if randomized, we wont do the attempt on sdcch

    def byte_string_to_list(self, string):
        byte_arr = array.array('B', string.decode("hex"))
        return byte_arr.tolist()

    def message_to_bursts(self, message_bytes):
        result = []
        message = ""

        for byte in message_bytes:
            message += "%0.2X" % byte

        output = check_output(["gsmframecoder", message]).split("\n")
        if len(output) >= 9:
            for i in range(4):
                result.append(output[(i + 1) * 2])
        return result
Пример #3
0
class CapturePlugin(PluginBase):
    @arg_group(name="Capturing",
               args=[
                   arg("--gsmtap",
                       action="store_true",
                       dest="gsmtap",
                       help="Output to GSMTap.",
                       default=False),
                   arg("--print-bursts",
                       action="store_true",
                       dest="print_bursts",
                       help="Print captured bursts.",
                       default=False),
                   arg("--length",
                       action="store",
                       dest="length",
                       type=int,
                       help="Length of the record in seconds."),
                   arg("--cfile",
                       action="store_path",
                       dest="cfile",
                       help="cfile."),
                   arg("--bursts",
                       action="store_path",
                       dest="bursts",
                       help="bursts."),
               ])
    @arg_group(
        name="RTL-SDR configuration",
        args=[
            arg("-p",
                action="store",
                dest="ppm",
                type=int,
                help="Set ppm. Default: value from config file."),
            arg("-s",
                action="store",
                dest="samp_rate",
                type=float,
                help="Set sample rate. Default: value from config file."),
            arg("-g",
                action="store",
                type=float,
                dest="gain",
                help="Set gain. Default: value from config file.")
        ])
    @arg_exclusive(args=[
        arg("-a",
            action="store",
            dest="arfcn",
            type=int,
            help="ARFCN of the BTS."),
        arg("-f",
            action="store",
            dest="freq",
            type=float,
            help="Frequency of the BTS.")
    ])
    @arg("-b",
         action="store",
         dest="band",
         choices=(grgsm.arfcn.get_bands()),
         help="GSM band of the ARFCN.")
    @cmd(
        name="capture_rtlsdr",
        description="Capture and save GSM transmissions using a RTL-SDR device."
    )
    def capture_rtlsdr(self, args):
        path = self._config_provider.get("gr-gsm", "apps_path")
        capture = imp.load_source("", os.path.join(path, "grgsm_capture.py"))

        freq = args.freq
        arfcn = args.arfcn
        band = args.band
        ppm = args.ppm
        sample_rate = args.samp_rate
        gain = args.gain
        cfile = None
        burstfile = None
        verbose = args.print_bursts
        gsmtap = args.gsmtap
        length = args.length

        if freq is not None:
            if band:
                if not grgsm.arfcn.is_valid_downlink(freq, band):
                    self.printmsg(
                        "Frequency is not valid in the specified band")
                    return
                else:
                    arfcn = grgsm.arfcn.downlink2arfcn(freq, band)
            else:
                for band in grgsm.arfcn.get_bands():
                    if grgsm.arfcn.is_valid_downlink(freq, band):
                        arfcn = grgsm.arfcn.downlink2arfcn(freq, band)
                        break
        elif arfcn is not None:
            if band:
                if not grgsm.arfcn.is_valid_arfcn(arfcn, band):
                    self.printmsg("ARFCN is not valid in the specified band")
                    return
                else:
                    freq = grgsm.arfcn.arfcn2downlink(arfcn, band)
            else:
                for band in grgsm.arfcn.get_bands():
                    if grgsm.arfcn.is_valid_arfcn(arfcn, band):
                        freq = grgsm.arfcn.arfcn2downlink(arfcn, band)
                        break

        if ppm is None:
            ppm = self._config_provider.getint("rtl_sdr", "ppm")
        if sample_rate is None:
            sample_rate = self._config_provider.getint("rtl_sdr",
                                                       "sample_rate")
        if gain is None:
            gain = self._config_provider.getint("rtl_sdr", "gain")

        if args.cfile is not None:
            cfile = self._data_access_provider.getfilepath(args.cfile)
        if args.bursts is not None:
            burstfile = self._data_access_provider.getfilepath(args.bursts)

        if cfile is None and burstfile is None:
            self.printmsg(
                "You must provide either a cfile or a burst file as destination."
            )
            return

        tb = grgsm_capture(fc=freq,
                           gain=gain,
                           samp_rate=sample_rate,
                           ppm=ppm,
                           arfcn=arfcn,
                           cfile=cfile,
                           burst_file=burstfile,
                           band=band,
                           verbose=verbose,
                           gsmtap=gsmtap,
                           rec_length=length)

        def signal_handler(signal, frame):
            tb.stop()
            tb.wait()

        signal.signal(signal.SIGINT, signal_handler)

        tb.start()
        tb.wait()
class TmsiPlugin(PluginBase):
    channel_modes = ['BCCH', 'BCCH_SDCCH4']

    @arg("-v",
         action="store_true",
         dest="verbose",
         help="If set, the captured TMSI / IMSI are printed.")
    @arg(
        "-o",
        action="store",
        dest="dest_file",
        help="If set, the captured TMSI / IMSI are stored in the specified file."
    )
    @arg("-m",
         action="store",
         dest="mode",
         choices=channel_modes,
         help="Channel mode.",
         default="BCCH")
    @arg_group(
        name="Cfile Options",
        args=[
            arg("-a",
                action="store",
                dest="arfcn",
                type=int,
                help="ARFCN of the cfile capture."),
            arg("-f",
                action="store",
                dest="freq",
                type=float,
                help="Frequency of the cfile capture."),
            arg("-b",
                action="store",
                dest="band",
                choices=grgsm.arfcn.get_bands(),
                help="GSM of the cfile capture."),
            arg("-p",
                action="store",
                dest="ppm",
                type=int,
                help="Set ppm. Default: value from config file."),
            arg("-s",
                action="store",
                dest="samp_rate",
                type=float,
                help="Set sample rate. Default: value from config file."),
            arg("-g",
                action="store",
                type=float,
                dest="gain",
                help="Set gain. Default: value from config file.")
        ])
    @arg("-t",
         action="store",
         dest="timeslot",
         type=int,
         help="Timeslot of the CCCH.",
         default=0)
    @arg_exclusive(args=[
        arg("--cfile", action="store_path", dest="cfile", help="cfile."),
        arg("--bursts", action="store_path", dest="bursts", help="bursts.")
    ])
    @cmd(name="tmsi_capture", description="TMSI capturing.")
    def tmsi_capture(self, args):
        verbose = args.verbose
        destfile = None
        mode = args.mode
        freq = args.freq
        arfcn = args.arfcn
        band = args.band
        ppm = args.ppm
        sample_rate = args.samp_rate
        gain = args.gain
        timeslot = args.timeslot
        cfile = None
        burstfile = None

        if args.cfile is None and args.bursts is None:
            raise PluginError("Provide a cfile or burst file.")

        if args.dest_file is not None:
            destfile = self._data_access_provider.getfilepath(args.dest_file)
        if args.cfile is not None:
            cfile = self._data_access_provider.getfilepath(args.cfile)
        if args.bursts is not None:
            burstfile = self._data_access_provider.getfilepath(args.bursts)

        flowgraph = TmsiCapture(timeslot=timeslot,
                                chan_mode=mode,
                                burst_file=burstfile,
                                cfile=cfile,
                                fc=freq,
                                samp_rate=sample_rate,
                                ppm=ppm)
        flowgraph.start()
        flowgraph.wait()

        tmsis = dict()
        imsis = dict()

        with open("tmsicount.txt") as file:
            content = file.readlines()

            for line in content:
                segments = line.strip().split("-")
                if segments[0] != "0":
                    key = segments[0]
                    if tmsis.has_key(key):
                        tmsis[key] += 1
                    else:
                        tmsis[key] = 1
                else:
                    key = segments[2]
                    if imsis.has_key(key):
                        imsis[key] += 1
                    else:
                        imsis[key] = 1

        self.printmsg("Captured {} TMSI, {} IMSI\n".format(
            len(tmsis), len(imsis)))

        if verbose or destfile is not None:
            sorted_tmsis = sorted(tmsis, key=tmsis.__getitem__, reverse=True)
            sorted_imsis = sorted(imsis, key=imsis.__getitem__, reverse=True)

            if destfile is not None:
                with open(destfile, "w") as file:
                    for key in sorted_tmsis:
                        file.write("{}:{}\n".format(key, tmsis[key]))
                    for key in sorted_imsis:
                        file.write("{}:{}\n".format(key, imsis[key]))

            if verbose:
                for key in sorted_tmsis:
                    self.printmsg("{} ({} times)".format(key, tmsis[key]))
                for key in sorted_imsis:
                    self.printmsg("{} ({} times)".format(key, imsis[key]))

        os.remove("tmsicount.txt")
class DecoderPlugin(PluginBase):
    channel_modes = ['BCCH', 'BCCH_SDCCH4', 'SDCCH8', 'TCHF']
    tch_codecs = collections.OrderedDict([('FR', grgsm.TCH_FS),
                                          ('EFR', grgsm.TCH_EFR),
                                          ('AMR12.2', grgsm.TCH_AFS12_2),
                                          ('AMR10.2', grgsm.TCH_AFS10_2),
                                          ('AMR7.95', grgsm.TCH_AFS7_95),
                                          ('AMR7.4', grgsm.TCH_AFS7_4),
                                          ('AMR6.7', grgsm.TCH_AFS6_7),
                                          ('AMR5.9', grgsm.TCH_AFS5_9),
                                          ('AMR5.15', grgsm.TCH_AFS5_15),
                                          ('AMR4.75', grgsm.TCH_AFS4_75)])

    @arg("-m",
         action="store",
         dest="mode",
         choices=channel_modes,
         help="Channel mode.",
         default="BCCH")
    @arg("-t",
         action="store",
         dest="timeslot",
         type=int,
         help="Timeslot to decode.",
         default=0)
    @arg(
        "--subslot",
        action="store",
        dest="subslot",
        type=int,
        help=
        "Subslot to decode. Use in combination with channel type BCCH_SDCCH4 and SDCCH8."
    )
    @arg_exclusive(args=[
        arg("--cfile", action="store_path", dest="cfile", help="cfile."),
        arg("--bursts", action="store_path", dest="bursts", help="bursts.")
    ])
    @arg("--print-messages",
         action="store_true",
         dest="print_messages",
         help="Print decoded messages.",
         default=False)
    @arg("--print-bursts",
         action="store_true",
         dest="print_bursts",
         help="Print decoded messages.",
         default=False)
    @arg_group(
        name="Cfile Options",
        args=[
            arg("-a",
                action="store",
                dest="arfcn",
                type=int,
                help="ARFCN of the cfile capture."),
            arg("-f",
                action="store",
                dest="freq",
                type=float,
                help="Frequency of the cfile capture."),
            arg("-b",
                action="store",
                dest="band",
                choices=grgsm.arfcn.get_bands(),
                help="GSM of the cfile capture."),
            arg("-p",
                action="store",
                dest="ppm",
                type=int,
                help="Set ppm. Default: value from config file."),
            arg("-s",
                action="store",
                dest="samp_rate",
                type=float,
                help="Set sample rate. Default: value from config file."),
            arg("-g",
                action="store",
                type=float,
                dest="gain",
                help="Set gain. Default: value from config file.")
        ])
    @arg_group(name="Decryption Options",
               args=[
                   arg("-5",
                       "--a5",
                       action="store",
                       dest="a5",
                       type=int,
                       help="A5 version.",
                       default=1),
                   arg("-k",
                       "--kc",
                       action="store",
                       dest="kc",
                       help="A5 session key Kc. Valid formats are "
                       "'0x12,0x34,0x56,0x78,0x90,0xAB,0xCD,0xEF' "
                       "and '1234567890ABCDEF'"),
               ])
    @arg_group(
        name="TCH Options",
        args=[
            arg("-c",
                action="store",
                dest="speech_codec",
                choices=tch_codecs.keys(),
                help="TCH-F speech codec."),
            arg("-o",
                action="store",
                dest="speech_output_file",
                help="TCH/F speech output file"),
            arg("--voice-boundary-detect",
                action="store_true",
                dest="enable_voice_boundary_detection",
                help=
                "Enable voice boundary detection for traffic channels. This can help reduce noice in the output.",
                default=False),
        ])
    @cmd(name="decode", description="Decodes GSM messages.")
    def decode(self, args):
        path = self._config_provider.get("gr-gsm", "apps_path")
        decoder = imp.load_source("", os.path.join(path, "grgsm_decode"))

        timeslot = args.timeslot
        subslot = args.subslot
        mode = args.mode

        burstfile = None
        cfile = None

        freq = args.freq
        arfcn = args.arfcn
        band = args.band
        ppm = args.ppm
        sample_rate = args.samp_rate
        gain = args.gain

        verbose = args.print_messages
        kc = []

        def kc_parse(kc, value):
            """ Callback function that parses Kc """

            # format 0x12,0x34,0x56,0x78,0x90,0xAB,0xCD,0xEF
            if ',' in value:
                value_str = value.split(',')

                for s in value_str:
                    val = int(s, 16)
                    if val < 0 or val > 255:
                        pass  # error
                    kc.append(val)
                if len(kc) != 8:
                    kc = []  # error
            elif len(value) == 16:
                for i in range(8):
                    s = value[2 * i:2 * i + 2]
                    val = int(s, 16)
                    if val < 0 or val > 255:
                        pass  # error
                        # parser.error("Invalid Kc % s\n" % s)
                    kc.append(val)
            else:
                pass  # error

        if args.kc is not None:
            kc_parse(kc, args.kc)

        if freq is not None:
            if band:
                if not grgsm.arfcn.is_valid_downlink(freq, band):
                    self.printmsg(
                        "Frequency is not valid in the specified band")
                    return
                else:
                    arfcn = grgsm.arfcn.downlink2arfcn(freq, band)
            else:
                for band in grgsm.arfcn.get_bands():
                    if grgsm.arfcn.is_valid_downlink(freq, band):
                        arfcn = grgsm.arfcn.downlink2arfcn(freq, band)
                        break
        elif arfcn is not None:
            if band:
                if not grgsm.arfcn.is_valid_arfcn(arfcn, band):
                    self.printmsg("ARFCN is not valid in the specified band")
                    return
                else:
                    freq = grgsm.arfcn.arfcn2downlink(arfcn, band)
            else:
                for band in grgsm.arfcn.get_bands():
                    if grgsm.arfcn.is_valid_arfcn(arfcn, band):
                        freq = grgsm.arfcn.arfcn2downlink(arfcn, band)
                        break

        if ppm is None:
            ppm = self._config_provider.getint("rtl_sdr", "ppm")
        if sample_rate is None:
            sample_rate = self._config_provider.getint("rtl_sdr",
                                                       "sample_rate")
        if gain is None:
            gain = self._config_provider.getint("rtl_sdr", "gain")

        if args.cfile is not None:
            cfile = self._data_access_provider.getfilepath(args.cfile)
        if args.bursts is not None:
            burstfile = self._data_access_provider.getfilepath(args.bursts)

        if cfile is None and burstfile is None:
            self.printmsg(
                "You must provide either a cfile or a burst file as destination."
            )
            return

        tb = decoder.grgsm_decoder(timeslot=timeslot,
                                   subslot=subslot,
                                   chan_mode=mode,
                                   burst_file=burstfile,
                                   cfile=cfile,
                                   fc=freq,
                                   samp_rate=sample_rate,
                                   a5=args.a5,
                                   a5_kc=kc,
                                   speech_file=args.speech_output_file,
                                   speech_codec=self.tch_codecs.get(
                                       args.speech_codec),
                                   enable_voice_boundary_detection=False,
                                   verbose=verbose,
                                   print_bursts=args.print_bursts,
                                   ppm=ppm)
        tb.start()
        tb.wait()