watfordjc / LTO-Encryption-Manager

LTO tape AES key management using SLIP-0021 symmetric key derivation from a BIP-0039 wallet mnemonic.
MIT License
3 stars 0 forks source link

Migrate C SPTI to C# #4

Open watfordjc opened 2 years ago

watfordjc commented 2 years ago

Feature Branch

Current feature branch for this issue: not created yet.

Progress


Background

This issue was originally at watfordjc/LTO-Encryption-SPTI#5

LTO-Encryption-SPTI is currently a linear Win32 C application.

LTO-Encryption-Manager has now reached the point where it needs to use SPTI.

The goal of this issue is to implement the most basic functions as library functions, starting with opening a file descriptor for the tape drive, using a Win32 API to obtain the serial number and WWN/LUN, and then closing the file descriptor.

The Data Backups and Archiving Project contains this loosely related note:

Improve design of LTO-Encryption-SPTI

At the moment there is a lot of repetitive code, redundant code leftover from the template, and the code isn't that clean. There is also a lot of debugging code and output and very little data being retained after it is queried. The data in the SCSI VPDs, for example, are mostly burned into the drive's persistent non-volatile memory at the time of manufacture or during a firmware update. Translate the SCSI pages into structured variables that can be referenced rather than doing the byte and bit stuff inline.

watfordjc commented 2 years ago

Data Redesign

The LTO-Encryption-SPTI application at the moment is mostly a load of constants and structs and manual parsing. Part of the reason for that is that the data is received unformatted.

Having decided to P/Invoke CreateFile() directly in LTO-Encryption-Manager rather than creating a wrapper function in a C library, I am now considering the feasibility of doing things mostly in C# rather than using a C library. The SPTI sample is C-based, and LTO-Encryption-SPTI is written in C, but almost everything is converting to structs and parsing. Sometimes the parsing is on fields of a structure that can't be converted to a struct.

In order to enable encryption, the following are needed via P/Invoke and SPTI at a minimum:

The first item, a HANDLE to the drive, is obtained by getting the drive path via CIM or an alternative method, and then (if necessary) appending a hash/octothorpe and the GUID for a tape drive (GUID_DEVINTERFACE_TAPE): #{53F5630B-B6BF-11D0-94F2-00A0C91EFB8B}, and using that full path with CreateFile().

Step one of obtaining the WWN/LUN (or anything else) requires calling DeviceIoControl() 4 times to check we can communicate with the drive, although the only thing I'm checking is that SrbType is of the newer/extended SCSI block variety STORAGE_REQUEST_BLOCK:

  1. Obtain a STORAGE_DESCRIPTOR_HEADER (parsed for the size of a STORAGE_ADAPTER_DESCRIPTOR).
  2. Obtain a STORAGE_ADAPTER_DESCRIPTOR (parsed for the AlignmentMask and SrbType values).
  3. Obtain a STORAGE_DESCRIPTOR_HEADER (parsed for the size of a STORAGE_DEVICE_DESCRIPTOR).
  4. Obtain a STORAGE_DEVICE_DESCRIPTOR (parsed for the BusType and SerialNumber).

Next, the process of sending/getting data to/from the drive is similar for most pages. SPTI wraps the SRB and sends it to the drive. Let's look at the C code:

if (srbType == SRB_TYPE_STORAGE_REQUEST_BLOCK)
{
    int pageCode;

    /*
    * CDB: Inquiry, Device Identifiers VPD page
    */
    length = ResetSrbIn(psptwb_ex, SCSIOP_INQUIRY);
    if (length == 0) { goto Cleanup; }
    psptwb_ex->spt.Cdb[1] = CDB_INQUIRY_EVPD;
    psptwb_ex->spt.Cdb[2] = VPD_DEVICE_IDENTIFIERS;
    status = SendSrb(fileHandle, psptwb_ex, length, &returned);

    if (CheckStatus(fileHandle, psptwb_ex, status, returned, length))
    {
        pageCode = psptwb_ex->ucDataBuf[1];
        if (pageCode == VPD_DEVICE_IDENTIFIERS)
        {
            ParseDeviceIdentifiers((PVPD_IDENTIFICATION_PAGE)psptwb_ex->ucDataBuf, &logicalUnitIdentifierLength, &logicalUnitIdentifier);
        }
    }
...
}

I'm going to need one of those psptwb_ex variables. That would be a PSCSI_PASS_THROUGH_WITH_BUFFERS_EX. I'm going to need to convert some C structs to C#. After spending way to long trying to marshal between C# and C and back, and ending up with a 9 byte offset issue, I have decided I am going to do it all in C# with CsWin32 and (where not covered) Win32 P/Invoke.

Let's compare what I've currently got in terms of C# code for the Device Identifiers VPD page:

public static void GetTapeDriveIdentifiers(TapeDrive tapeDrive)
{
    IntPtr psptwb_ex = Marshal.AllocHGlobal(Marshal.SizeOf<SCSI_PASS_THROUGH_WITH_BUFFERS_EX>());
    SCSI_PASS_THROUGH_WITH_BUFFERS_EX sptwb_ex = new();
    Marshal.StructureToPtr(sptwb_ex, psptwb_ex, true);
    uint length = ResetSrbIn(psptwb_ex, Constants.SCSIOP_INQUIRY);
    sptwb_ex = Marshal.PtrToStructure<SCSI_PASS_THROUGH_WITH_BUFFERS_EX>(psptwb_ex);
    Windows.Win32.InlineArrayIndexerExtensions.ItemRef(ref sptwb_ex.spt.Cdb, 1) = Constants.CDB_INQUIRY_EVPD;
    Windows.Win32.InlineArrayIndexerExtensions.ItemRef(ref sptwb_ex.spt.Cdb, 2) = Constants.VPD_DEVICE_IDENTIFIERS;
    Marshal.StructureToPtr(sptwb_ex, psptwb_ex, true);

    uint returnedData = 0;
    Windows.Win32.System.IO.OVERLAPPED overlapped;
    bool ok;
    unsafe
    {
        ok = Windows.Win32.PInvoke.DeviceIoControl(tapeDrive.Handle,
            Windows.Win32.PInvoke.IOCTL_SCSI_PASS_THROUGH_EX,
            (void*)psptwb_ex,
            (uint)Marshal.SizeOf(sptwb_ex),
            (void*)psptwb_ex,
            length,
            &returnedData,
            &overlapped);
    }
    if (ok)
    {
        sptwb_ex = Marshal.PtrToStructure<SCSI_PASS_THROUGH_WITH_BUFFERS_EX>(psptwb_ex);
        Trace.WriteLine(Convert.ToHexString(sptwb_ex.ucDataBuf, 0, (int)sptwb_ex.spt.DataInTransferLength));
    }
    Marshal.FreeHGlobal(psptwb_ex);
}

ResetSrbIn() pretty much does what its name suggests: it resets an SRB for usage in the in/read direction. Having found the InlineArrayIndexerExtensions method (it doesn't even have any Google results) after hunting through IntelliSense for a way to access the 2nd byte of a 1 byte variable sized array (UCHAR[ANYSIZE_ARRAY] in Win32 C), I migrated ResetSrbIn() to C#.

There is one issue with migrating to C#: many constants and structs, such as the VPD page struct, do not appear to be available via CsWin32. C# also doesn't really do bit fields in structs. Piping the Trace.WriteLine(Convert.ToHexString(...)) output through xxd and hexdump, though, I can see the LUN (and HP_____Ultrium 6-SCSI__[SerialNumber], where _ is a space) in the bytes.

I need to parse the pages though, and with CsWin32 appearing to not have the page structs, I am going to have to implement all of them myself, including working out how to handle bit fields. This is one of the problems with manual parsing: having to reimplement the same thing again.

I am considering defining the pages using ASN.1+ECN, although there might not be a library or tool that can convert those definitions to C#.

The Compaq/HP OID 1.3.6.1.4.1.232.5.5.4.1 (iso.identified-organization.dod.internet.private.enterprise.compaq.cpqScsi.cpqSasComponent.cpqSasTapeDrv.cpqSasTapeDrvTable.cpqSasTapeDrvEntry) is a table entry for a SAS tape drive used with tape libraries that do SNMP and it has some MIB objects defined such as 1.3.6.1.4.1.232.5.5.4.1.1.8 (cpqSasTapeDrvSerialNumber):

cpqSasTapeDrvSerialNumber OBJECT-TYPE
    SYNTAX  DisplayString (SIZE (0..40))
    ACCESS  read-only
    STATUS  mandatory
    DESCRIPTION
        "SAS Tape Drive Serial Number.

        This is the serial number assigned to the tape drive."
::= { cpqSasTapeDrvEntry 8 }

Such objects/modules may have a use if SNMP support were desired, however getting the pages from the drive and parsing them so that such an object can be populated is something for a tape library to implement. As far as I can see, those tables in the LTO manuals are the only definitions the LTO publish.

Let's take the VPD struct from scsi.h (with comments removed), and a non-ECN translation in bullet list form:

typedef struct _VPD_IDENTIFICATION_PAGE {
    UCHAR DeviceType : 5;
    UCHAR DeviceTypeQualifier : 3;
    UCHAR PageCode;
    UCHAR Reserved;
    UCHAR PageLength;
    UCHAR Descriptors[0];
#endif
} VPD_IDENTIFICATION_PAGE, *PVPD_IDENTIFICATION_PAGE;

Using the ASN.1 example RLC-RADIO-HANDOVER-COMPLETE (from ETSI TS 101 761-2 V1.3.1, AKA the Hiperlan specification) as a template (the specification has ASN.1 throughout and transfer syntax tables in Annex A), the ASN.1 for VPD-Identification-Page arguments would be something like this:

Device-Identification-Page-Arg ::= SEQUENCE {
    device-type                 Device-Type
    device-type-qualifier           Device-Type-Qualifier
    page-code               Page-Code
    page-length             Page-Length
    device-identification-descriptor-list   Device-Identification-Descriptor-List
}

Adding in the reserved/padding, the initial encoding structure would be something like this:

#Device-Identification-Page-Arg-struct ::= #CONCATENATION {
    device-type                 #Device-Type
    device-type-qualifier           #Device-Type-Qualifier
    page-code               #Page-Code
    aux-reserved-1              #PAD
    page-length             #Page-Length
    device-identification-descriptor-list   #Device-Identification-Descriptor-List
}

[TODO...]


I can't seem to get SPIN working. Keep getting Medium not present errors.

watfordjc commented 2 years ago

Ubuntu

This is a side-note on installing tape and LTFS software in Ubuntu, because testing transfer rates when reading/writing from/to LTFS volumes in Windows revealed abysmal speeds. Using two operating systems isn't ideal, however enabling LTO encryption on a drive can leave that key installed on the drive through multiple reboots (i.e. not setting the CKOD bit to 1).

This is a fresh (non-live desktop) install of Ubuntu 20.04.4 on a 64 GB flash drive, installed by clicking on install from within a live session of Ubuntu 20.04.4 on a different USB flash drive, installed using rufus in Windows 10.

This is an 8 core/16 thread machine, so I use 12 threads -j 12 when I call make during installation of ltfs.

A note on formatting in this comment, given markdown's limitations:

typed command at a $ bash prompt in gnome-terminal

output from the command

Existing Software Information

Kernal and Operating System Versions

uname -a

Linux john-desktop 5.13.0-39-generic #44~20.04.1-Ubuntu SMP Thu Mar 24 16:43:35 UTC 2022 x86_64 x86_64 x86_64 GNU/Linux

lsb_release -a

No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.4 LTS
Release:    20.04
Codename:   focal

Installed Packages/Software

comm -23 <(apt-mark showmanual | sort -u) <(gzip -dc /var/log/installer/initial-status.gz | sed -n 's/^Package: //p' | sort -u)

git
google-chrome-stable
gparted
grub2-common
keepass2
nvidia-driver-510
uuid
veracrypt

Installation

Cloning the ltfs Repository

cd /opt sudo mkdir ltfs sudo chown john:john ltfs git clone 'https://github.com/LinearTapeFileSystem/ltfs.git' cd ltfs git log -n1 --oneline

98e8854 (HEAD -> master, origin/master, origin/HEAD) Fix filehandle corruption on LP64 (#345)

Installing ltfs Dependencies

sudo apt install automake icu-devtools libfuse-dev libsnmp-dev libtool libxml2-dev mt-st python3-pyxattr uuid-dev

There is one more thing needed: /usr/bin/icu-config

stat /usr/bin/icu-config

stat: cannot stat '/usr/bin/icu-config': No such file or directory

sudo cp .github/workflows/icu-config /usr/bin/icu-config sudo chmod +x /usr/bin/icu-config

Building and Installing ltfs

cd /opt/ltfs ./autogen.sh ./configure make -j12 sudo ldconfig sudo make install sudo ldconfig


Testing ltfs Drive Detection

sudo mkdir /mnt/ltfs sudo chown john:john /mnt/ltfs sudo ltfs -o device_list

98df LTFS14000I LTFS starting, LTFS version 2.5.0.0 (Prelim), log level 2.
98df LTFS14058I LTFS Format Specification version 2.4.0.
98df LTFS14104I Launched by "ltfs -o device_list".
98df LTFS14105I This binary is built for Linux (x86_64).
98df LTFS14106I GCC version is 9.4.0.
98df LTFS17087I Kernel version: Linux version 5.13.0-39-generic (buildd@lcy02-amd64-080) (gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #44~20.04.1-Ubuntu SMP Thu Mar 24 16:43:35 UTC 2022 i386.
98df LTFS17089I Distribution: DISTRIB_ID=Ubuntu.
98df LTFS17089I Distribution: NAME="Ubuntu".
98df LTFS17085I Plugin: Loading "sg" tape backend.
Tape Device list:.
Device Name = /dev/sg0 (11.0.0.0), Vendor ID = HP      , Product ID = Ultrium 6-SCSI  , Serial Number = [REDACTED], Product Name =[Ultrium 6-SCSI].

sudo ltfs /mnt/ltfs/ -o devname=/dev/sg0 -o sync_type=unmount

98e7 LTFS14000I LTFS starting, LTFS version 2.5.0.0 (Prelim), log level 2.
98e7 LTFS14058I LTFS Format Specification version 2.4.0.
98e7 LTFS14104I Launched by "ltfs /mnt/ltfs/ -o devname=/dev/sg0 -o sync_type=unmount".
98e7 LTFS14105I This binary is built for Linux (x86_64).
98e7 LTFS14106I GCC version is 9.4.0.
98e7 LTFS17087I Kernel version: Linux version 5.13.0-39-generic (buildd@lcy02-amd64-080) (gcc (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0, GNU ld (GNU Binutils for Ubuntu) 2.34) #44~20.04.1-Ubuntu SMP Thu Mar 24 16:43:35 UTC 2022 i386.
98e7 LTFS17089I Distribution: DISTRIB_ID=Ubuntu.
98e7 LTFS17089I Distribution: NAME="Ubuntu".
98e7 LTFS14064I Sync type is "unmount".
98e7 LTFS17085I Plugin: Loading "sg" tape backend.
98e7 LTFS17085I Plugin: Loading "unified" iosched backend.
98e7 LTFS14095I Set the tape device write-anywhere mode to avoid cartridge ejection.
98e7 LTFS30209I Opening a device through sg-ibmtape driver (/dev/sg0).
98e7 LTFS30250I Opened the SCSI tape device 11.0.0.0 (/dev/sg0).
98e7 LTFS30207I Vendor ID is HP      .
98e7 LTFS30208I Product ID is Ultrium 6-SCSI  .
98e7 LTFS30214I Firmware revision is 35GD.
98e7 LTFS30215I Drive serial is [REDACTED].
98e7 LTFS30285I The reserved buffer size of /dev/sg0 is 1048576.
98e7 LTFS30294I Setting up timeout values from RSOC.
98e7 LTFS17160I Maximum device block size is 1048576.
98e7 LTFS11330I Loading cartridge.
98e7 LTFS11332I Load successful.
98e7 LTFS17157I Changing the drive setting to write-anywhere mode.
98e7 LTFS11005I Mounting the volume from device.
98e7 LTFS17192W Cannot read: medium is encrypted.
98e7 LTFS12049E Cannot read: backend call failed (-21600).
98e7 LTFS11174E Cannot read ANSI label: read failed (-21600).
98e7 LTFS11170E Failed to read label (-21600) from partition 0.
98e7 LTFS11009E Cannot read volume: failed to read partition labels.
98e7 LTFS14013E Cannot mount the volume from device.
98e7 LTFS30205I MODESELECT (0x55) returns -20500.
98e7 LTFS30263I MODESELECT returns Invalid Field in Parameter List (-20500) /dev/sg0.

Reading MAM/CM Attributes

Other than some things displayed by HPE Tape Tools in Windows, the only thing I have used for reading MAM attributes has been LTO-Encryption-SPTI.

Cloning the lto-info Repository

cd /opt sudo mkdir lto-info sudo chown john:john lto-info git clone 'https://github.com/speed47/lto-info'

Installing lto-info Dependencies

sudo snap install go --classic

Building and Installing lto-info

cd /opt/lto-info make sudo ln -s /opt/lto-info/lto-info /usr/local/bin/lto-info

Testing lto-info

sudo lto-info

Drive information:
Vendor  : HP
Model   : Ultrium 6-SCSI
Firmware: 35GD
Medium information:
Cartridge Type: 0x00 - Data cartridge
Medium format : 0x5a - LTO-6
Formatted as  : 0x5a - LTO-6
MAM Capacity  : 16384 bytes
Format specs:
Capacity  :  2500 GB native   -  6250 GB compressed with a 2.5:1 ratio
R/W Speed :   160 MB/s native -   400 MB/s compressed
Partitions:     4 max partitions supported
Phy. specs: 4 bands/tape, 34 wraps/band, 16 tracks/wrap, 2176 total tracks
Duration  : 4h20 to fill tape with 136 end-to-end passes (115 seconds/pass)
Usage information:
Previous sessions:

Cloning the mam-info Repository

cd /opt sudo mkdir lto-info sudo chown john:john lto-info git clone 'https://github.com/arogge/maminfo.git'

Installing mam-info Dependencies

sudo apt install libsgutils2-dev

Building and Installing mam-info

cd maminfo make sudo ln -s /opt/maminfo/mam-info /usr/local/bin/mam-info

Testing mam-info

sudo mam-info -f /dev/sg0

ERROR : Problem reading attribute 0400
ERROR : Read failed (try verbose opt)

The Problem

Looking at the source code for mam-info.c, it seems apparent that these tools make assumptions about what attributes to ask for.

LTO-Encryption-SPTI, on the other hand, does the following for the MAM/CM attribute parsing section:

  1. Ask the drive for the Volume List.
  2. Ask the drive for the Partition List.
  3. Ask the drive for the Supported Attributes.
  4. Ask the drive for the MAM Attribute List, and then parse and display that list.
  5. Ask the drive for the MAM Attribute List for each partition in the Partition List, and then parse and display that list.
  6. Ask the drive for the MAM default locale IDs (ASCII, binary, etc.) for each partition.
  7. Ask the drive for the MAM Attribute Values for each partition in the Partition List, and then parse and display those values, using the default locale ID (or the locale ID for a attribute) to display the string/hex raw value of unknown attributes (or known attributes with an unexpected locale ID).

The Solution

sg_read_attr can be used in combination with reference to some of the constants defined in spti.h in LTO-Encryption-SPTI:

/* Read Attribute Extensions to scsi.h */
#define READ_ATTRIBUTE_SERVICE_ATTRIBUTE_VALUES 0x00
#define READ_ATTRIBUTE_SERVICE_ATTRIBUTE_LIST 0x01
#define READ_ATTRIBUTE_SERVICE_VOLUME_LIST 0x02
#define READ_ATTRIBUTE_SERVICE_PARTITION_LIST 0x03
#define READ_ATTRIBUTE_SERVICE_SUPPORTED_ATTRIBUTES 0x05

A rather simple dash/bash script should do, although this is something I put together rather quickly and is not well tested:

#!/bin/sh

if [ ! "$(id -u)" = "0" ]; then
  echo "Run with sudo" >&1
  exit 1
elif [ ! -c "$1" ]; then
  echo "Usage: $0 /dev/sg0"
  return 0
fi

DRIVE="$1"

echo "-----"
echo "Tape Attributes follow"
echo "---"
sudo sg_read_attr --cache --sa=0x00 "$1"
echo "-----"

VOLUME_LIST_RAW=$(sudo sg_read_attr --cache --sa=0x02 -H "$DRIVE" | sed 's/  */ /g;s/^ //g;s/ $//g' | cut -d' ' -f2-)
if [ ! $(echo "$VOLUME_LIST_RAW" | head -n1 | awk '{print $1 $2}' | sed 's/^0*//') = 2 ]; then
    echo "Volume List data is unexpected length" >&1
    exit 1
else
    VOLUME_COUNT=$(echo "$VOLUME_LIST_RAW" | head -n1 | awk '{print $3 $4}' | sed 's/^0*//')
    echo "Number of volumes: $VOLUME_COUNT"
    if [ "$VOLUME_COUNT" = "" ]; then
        echo "No volumes?" >&1
        exit 1
    elif [ $VOLUME_COUNT -ne 1 ]; then
        echo "Error: LTO only allows 1 volume. Is $DRIVE an LTO tape drive?" >&1
        exit 1
    fi
fi

PARTITION_LIST_RAW=$(sudo sg_read_attr --cache --sa=0x03 -H "$DRIVE" | sed 's/  */ /g;s/^ //g;s/ $//g' | cut -d' ' -f2-)
if [ ! $(echo "$PARTITION_LIST_RAW" | head -n1 | awk '{print $1 $2}' | sed 's/^0*//') = 2 ]; then
    echo "Partition List data is unexpected length" >&1
    exit 1
else
    PARTITION_COUNT=$(echo "$PARTITION_LIST_RAW" | head -n1 | awk '{print $3 $4}' | sed 's/^0*//')
    echo "Number of partitions: $PARTITION_COUNT"
fi

if [ "$PARTITION_COUNT" = "" ]; then
    echo "No partitions?" >&1
    exit 0
fi

MAXIMUM_PARTITION=$(expr $PARTITION_COUNT - 1)
for CURRENT_PARTITION in $(seq 0 $MAXIMUM_PARTITION); do
    echo "-----"
    echo "Partition $CURRENT_PARTITION Attributes follow"
    echo "---"
    sudo sg_read_attr --cache --sa=0x00 -p $CURRENT_PARTITION "$1"
done

Similarly, getting the full barcode of a tape could be done with a bash script (again, not thoroughly tested, and it assumes the textual output of the commands won't change format):

#!/bin/sh

if [ ! "$(id -u)" = "0" ]; then
  echo "Run with sudo" >&1
  exit 1
elif [ ! -c "$1" ]; then
  echo "Usage: $0 /dev/sg0"
  return 0
fi

DRIVE="$1"
MAM_BARCODE="0x0806"
MAM_MEDIUM_DENSITY_CODE="0x0405"
MAM_MEDIUM_TYPE="0x0408"

sg_read_attr --sa=0x00 "$DRIVE" >/dev/null
BARCODE=$(sg_read_attr --cache --sa=0x00 --filter="$MAM_BARCODE" "$DRIVE" | cut -d':' -f2 | sed 's/^ *//g;s/ *$//g')
MEDIUM_DENSITY_CODE=$(sg_read_attr --cache --sa=0x00 --filter="$MAM_MEDIUM_DENSITY_CODE" "$DRIVE" | cut -d':' -f2 | sed 's/^ *//g;s/ *$//g')
MEDIUM_TYPE=$(sg_read_attr --cache --sa=0x00 --filter="$MAM_MEDIUM_TYPE" "$DRIVE" | cut -d':' -f2 | sed 's/^ *//g;s/ *$//g')

if [ "$BARCODE" = "" ]; then
    echo "No barcode" >&1
    exit 0
fi

#echo "Barcode: $BARCODE"
#echo "Medium Density Code: $MEDIUM_DENSITY_CODE"
#echo "Medium Type: $MEDIUM_TYPE"

LTO_GENERATION=""

case "$MEDIUM_DENSITY_CODE" in

    "0x40")
        LTO_GENERATION="1"
    ;;
    "0x42")
        LTO_GENERATION="2"
    ;;
    "0x44")
        LTO_GENERATION="3"
    ;;
    "0x46")
        LTO_GENERATION="4"
    ;;
    "0x58")
        LTO_GENERATION="5"
    ;;
    "0x5a")
        LTO_GENERATION="6"
    ;;
    "0x5c")
        LTO_GENERATION="7"
    ;;
    "0x5d")
        LTO_GENERATION="M8"
    ;;
    "0x5e")
        LTO_GENERATION="8"
    ;;
    "0x60")
        LTO_GENERATION="9"
    ;;
    *)
        echo "Unhandled Medium Density Code: $MEDIUM_DENSITY_CODE" >&1
        exit 1
    ;;
esac

MEDIUM_TYPE_DATA="0X0"
MEDIUM_TYPE_CLEAN="0X1"
MEDIUM_TYPE_WORM="0X80"

BARCODE_SUFFIX_CHAR1=""

BARCODE_SUFFIX=""

case "$MEDIUM_TYPE" in

    "0x0")
        if [ ! "$LTO_GENERATION" = "M8" ]; then
            BARCODE_SUFFIX="L$LTO_GENERATION"
        else
            BARCODE_SUFFIX="M8"
        fi
    ;;
    "0x1")
        if [ ! "$LTO_GENERATION" = "M8" ]; then
            BARCODE_SUFFIX="L$LTO_GENERATION"
        else
            BARCODE_SUFFIX="M8"
        fi
    ;;
    "0x80")
        case "LTO_GENERATION" in

            "3")
                BARCODE_SUFFIX="LT"
            ;;
            "4")
                BARCODE_SUFFIX="LU"
            ;;
            "5")
                BARCODE_SUFFIX="LV"
            ;;
            "6")
                BARCODE_SUFFIX="LW"
            ;;
            "7")
                BARCODE_SUFFIX="LX"
            ;;
            "8")
                BARCODE_SUFFIX="LY"
            ;;
            "9")
                BARCODE_SUFFIX="LZ"
            ;;
        esac
    ;;
    *)
        echo "Unhandle Medium Type: $MEDIUM_TYPE" >&1
        exit 1
    ;;
esac

#echo "Full barcode: $BARCODE$BARCODE_SUFFIX"
echo "$BARCODE$BARCODE_SUFFIX"

Writing a C program that uses the libsgutils2-dev library, rather than parsing the output of the sg_read_attr command, could remove the issue of things breaking if the text output format changes, but dash/bash scripting is my usual scripting option on Linux/WSL2.