├── go.mod ├── go.sum ├── AUTHORS ├── mem.go ├── LICENSE ├── main.go ├── README.md ├── Makefile ├── usb.go └── scsi.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/usbarmory/armory-ums 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/usbarmory/tamago v0.0.0-20250527211737-92513e7e3efd 7 | ) 8 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/usbarmory/tamago v0.0.0-20250527211737-92513e7e3efd h1:TAbdlUdB2NfImyrCYgE5iNaW4OzrlEe64/I6bzoaIXM= 2 | github.com/usbarmory/tamago v0.0.0-20250527211737-92513e7e3efd/go.mod h1:BtIwki3LlSlQJ/sW1wL2unqgYndZsSDioeyI3bVtZO8= 3 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | # This is the list of armory-ums significant contributors. 2 | # 3 | # This does not necessarily list everyone who has contributed code, 4 | # especially since many employees of one corporation may be contributing. 5 | # To see the full list of contributors, see the revision history in 6 | # source control. 7 | Andrea Barisani 8 | Andrej Rosano 9 | -------------------------------------------------------------------------------- /mem.go: -------------------------------------------------------------------------------- 1 | // https://github.com/usbarmory/armory-ums 2 | // 3 | // Copyright (c) The armory-ums authors. All Rights Reserved. 4 | // 5 | // Use of this source code is governed by the license 6 | // that can be found in the LICENSE file. 7 | 8 | package main 9 | 10 | import ( 11 | _ "unsafe" 12 | 13 | "github.com/usbarmory/tamago/dma" 14 | ) 15 | 16 | // Override standard memory allocation as this application requires large DMA 17 | // descriptors. 18 | 19 | //go:linkname ramSize runtime.ramSize 20 | var ramSize uint32 = 0x10000000 // 256MB 21 | 22 | // 2nd half of external RAM (256MB) 23 | var dmaStart uint = 0x90000000 24 | 25 | // 256MB 26 | var dmaSize = 0x10000000 27 | 28 | func init() { 29 | dma.Init(dmaStart, dmaSize) 30 | 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) The armory-ums authors. All Rights Reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are 5 | met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above 10 | copyright notice, this list of conditions and the following disclaimer 11 | in the documentation and/or other materials provided with the 12 | distribution. 13 | * Neither the name of Reversec Consulting AB nor the names of its 14 | contributors may be used to endorse or promote products derived from 15 | this software without specific prior written permission. 16 | 17 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 18 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 19 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 20 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 21 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 22 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 23 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 25 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // https://github.com/usbarmory/armory-ums 2 | // 3 | // Copyright (c) The armory-ums authors. All Rights Reserved. 4 | // 5 | // Use of this source code is governed by the license 6 | // that can be found in the LICENSE file. 7 | 8 | package main 9 | 10 | import ( 11 | "errors" 12 | "log" 13 | 14 | "github.com/usbarmory/tamago/soc/nxp/imx6ul" 15 | "github.com/usbarmory/tamago/soc/nxp/usb" 16 | "github.com/usbarmory/tamago/soc/nxp/usdhc" 17 | 18 | usbarmory "github.com/usbarmory/tamago/board/usbarmory/mk2" 19 | ) 20 | 21 | var cards []*usdhc.USDHC 22 | 23 | func init() { 24 | log.SetFlags(0) 25 | 26 | switch imx6ul.Model() { 27 | case "i.MX6ULL", "i.MX6ULZ": 28 | imx6ul.SetARMFreq(imx6ul.FreqMax) 29 | case "i.MX6UL": 30 | imx6ul.SetARMFreq(imx6ul.Freq528) 31 | } 32 | } 33 | 34 | func detect(card *usdhc.USDHC) (err error) { 35 | if card == nil { 36 | return errors.New("no such device") 37 | } 38 | 39 | if err = card.Detect(); err != nil { 40 | return 41 | } 42 | 43 | info := card.Info() 44 | capacity := int64(info.BlockSize) * int64(info.Blocks) 45 | giga := capacity / (1000 * 1000 * 1000) 46 | gibi := capacity / (1024 * 1024 * 1024) 47 | 48 | log.Printf("imx6_usdhc: %d GB/%d GiB card detected %+v", giga, gibi, info) 49 | 50 | cards = append(cards, card) 51 | 52 | return 53 | } 54 | 55 | func main() { 56 | err := detect(usbarmory.SD) 57 | 58 | if err != nil { 59 | usbarmory.LED("white", false) 60 | } else { 61 | usbarmory.LED("white", true) 62 | } 63 | 64 | err = detect(usbarmory.MMC) 65 | 66 | if err != nil { 67 | usbarmory.LED("blue", false) 68 | } else { 69 | usbarmory.LED("blue", true) 70 | } 71 | 72 | device := &usb.Device{ 73 | Setup: setup, 74 | } 75 | configureDevice(device) 76 | 77 | iface := buildMassStorageInterface() 78 | device.Configurations[0].AddInterface(iface) 79 | 80 | usbarmory.USB1.Init() 81 | usbarmory.USB1.DeviceMode() 82 | usbarmory.USB1.Reset() 83 | 84 | // never returns 85 | usbarmory.USB1.Start(device) 86 | } 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Introduction 2 | ============ 3 | 4 | This [TamaGo](https://github.com/usbarmory/tamago) based unikernel 5 | allows USB Mass Storage interfacing for the [USB armory Mk II](https://github.com/usbarmory/usbarmory/wiki) 6 | internal eMMC card as well as any inserted external microSD card. 7 | 8 | Binary releases 9 | =============== 10 | 11 | Pre-compiled binary releases are available 12 | [here](https://github.com/usbarmory/armory-ums/releases). 13 | 14 | Compiling 15 | ========= 16 | 17 | Build the [TamaGo compiler](https://github.com/usbarmory/tamago-go) 18 | (or use the [latest binary release](https://github.com/usbarmory/tamago-go/releases/latest)): 19 | 20 | ``` 21 | wget https://github.com/usbarmory/tamago-go/archive/refs/tags/latest.zip 22 | unzip latest.zip 23 | cd tamago-go-latest/src && ./all.bash 24 | cd ../bin && export TAMAGO=`pwd`/go 25 | ``` 26 | 27 | Build the `armory-ums.imx` application executable (note that on secure booted 28 | units the `imx_signed` target should be used with the relevant 29 | [`HAB_KEYS`](https://github.com/usbarmory/usbarmory/wiki/Secure-boot-(Mk-II)) set. 30 | 31 | 32 | ``` 33 | git clone https://github.com/usbarmory/armory-ums && cd armory-ums 34 | make CROSS_COMPILE=arm-none-eabi- imx 35 | ``` 36 | 37 | Note that the command above embeds build and VCS information that is useful 38 | for understanding the origin of a binary, but this prevents the same binary 39 | from being reproducibly built elsewhere. To strip this information so that 40 | the binary can be reproducibly built elsewhere: 41 | 42 | ``` 43 | REPRODUCIBLE=1 make imx 44 | ``` 45 | 46 | Executing 47 | ========= 48 | 49 | The resulting `armory-ums.imx` file can be executed over USB using 50 | [SDP](https://github.com/usbarmory/usbarmory/wiki/Boot-Modes-(Mk-II)#serial-download-protocol-sdp). 51 | 52 | SDP mode requires boot switch configuration towards microSD without any card 53 | inserted, however armory-ums detects microSD card only at startup. Therefore, 54 | when starting with SDP, to expose the microSD over mass storage, follow this 55 | procedure: 56 | 57 | 1. Remove the microSD card on a powered off device. 58 | 2. Set microSD boot mode switch. 59 | 3. Plug the device on a USB port to power it up in SDP mode. 60 | 4. Insert the microSD card. 61 | 5. Launch `imx_usb armory-ums.imx`. 62 | 63 | Alternatively, to expose the internal eMMC card, armory-ums can be 64 | [flashed on any microSD](https://github.com/usbarmory/usbarmory/wiki/Boot-Modes-(Mk-II)#flashing-imx-native-images). 65 | 66 | Operation 67 | ========= 68 | 69 | Once running, the USB armory Mk II can be used like any standard USB drive, 70 | exposing both internal eMMC card as well as the external microSD card (if 71 | present). 72 | 73 | | Card | Evaluation order | LED status¹ | 74 | |:-----------------:|------------------|-------------| 75 | | external microSD | 1st | white | 76 | | internal eMMC | 2nd | blue | 77 | 78 | ¹ LED on indicates successful detection 79 | 80 | Authors 81 | ======= 82 | 83 | Andrea Barisani 84 | andrea@inversepath.com 85 | 86 | License 87 | ======= 88 | 89 | armory-ums | https://github.com/usbarmory/armory-ums 90 | Copyright (c) The armory-ums authors. All Rights Reserved. 91 | 92 | These source files are distributed under the BSD-style license found in the 93 | [LICENSE](https://github.com/usbarmory/armory-ums/blob/master/LICENSE) file. 94 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # http://github.com/usbarmory/armory-ums 2 | # 3 | # Copyright (c) The armory-ums authors. All Rights Reserved. 4 | # 5 | # Use of this source code is governed by the license 6 | # that can be found in the LICENSE file. 7 | 8 | BUILD_USER ?= $(shell whoami) 9 | BUILD_HOST ?= $(shell hostname) 10 | BUILD_DATE ?= $(shell /bin/date -u "+%Y-%m-%d %H:%M:%S") 11 | BUILD = ${BUILD_USER}@${BUILD_HOST} on ${BUILD_DATE} 12 | REV = $(shell git rev-parse --short HEAD 2> /dev/null) 13 | 14 | SHELL = /bin/bash 15 | 16 | BUILD_FLAGS = -trimpath 17 | ifeq ("${REPRODUCIBLE}","1") 18 | BUILD := "REPRODUCIBLE" 19 | BUILD_FLAGS := ${BUILD_FLAGS} -buildvcs=false -buildmode=exe 20 | endif 21 | 22 | APP := armory-ums 23 | GOENV := GO_EXTLINK_ENABLED=0 CGO_ENABLED=0 GOOS=tamago GOARM=7 GOARCH=arm 24 | TEXT_START := 0x80010000 # ramStart (defined in imx6/imx6ul/memory.go) + 0x10000 25 | GOFLAGS := -tags linkramsize ${BUILD_FLAGS} -ldflags "-s -w -T $(TEXT_START) -E _rt0_arm_tamago -R 0x1000 -X 'main.Build=${BUILD}' -X 'main.Revision=${REV}'" 26 | 27 | .PHONY: clean 28 | 29 | #### primary targets #### 30 | 31 | all: $(APP) 32 | 33 | imx: $(APP).imx 34 | 35 | imx_signed: $(APP)-signed.imx 36 | 37 | elf: $(APP) 38 | 39 | #### utilities #### 40 | 41 | check_tamago: 42 | @if [ "${TAMAGO}" == "" ] || [ ! -f "${TAMAGO}" ]; then \ 43 | echo 'You need to set the TAMAGO variable to a compiled version of https://github.com/usbarmory/tamago-go'; \ 44 | exit 1; \ 45 | fi 46 | 47 | check_hab_keys: 48 | @if [ "${HAB_KEYS}" == "" ]; then \ 49 | echo 'You need to set the HAB_KEYS variable to the path of secure boot keys'; \ 50 | echo 'See https://github.com/usbarmory/usbarmory/wiki/Secure-boot-(Mk-II)'; \ 51 | exit 1; \ 52 | fi 53 | 54 | dcd: 55 | echo $(GOMODCACHE) 56 | echo $(TAMAGO_PKG) 57 | cp -f $(GOMODCACHE)/$(TAMAGO_PKG)/board/usbarmory/mk2/imximage.cfg $(APP).dcd 58 | 59 | clean: 60 | rm -f $(APP) 61 | @rm -fr $(APP).bin $(APP).imx $(APP)-signed.imx $(APP).csf $(APP).dcd 62 | 63 | #### dependencies #### 64 | 65 | $(APP): check_tamago 66 | $(GOENV) $(TAMAGO) build $(GOFLAGS) -o ${APP} 67 | 68 | $(APP).dcd: check_tamago 69 | $(APP).dcd: GOMODCACHE=$(shell ${TAMAGO} env GOMODCACHE) 70 | $(APP).dcd: TAMAGO_PKG=$(shell grep "github.com/usbarmory/tamago v" go.mod | awk '{print $$1"@"$$2}') 71 | $(APP).dcd: dcd 72 | 73 | $(APP).bin: CROSS_COMPILE=arm-none-eabi- 74 | $(APP).bin: $(APP) 75 | $(CROSS_COMPILE)objcopy --enable-deterministic-archives \ 76 | -j .text -j .rodata -j .shstrtab -j .typelink \ 77 | -j .itablink -j .gopclntab -j .go.buildinfo -j .noptrdata -j .data \ 78 | -j .bss --set-section-flags .bss=alloc,load,contents \ 79 | -j .noptrbss --set-section-flags .noptrbss=alloc,load,contents \ 80 | $(APP) -O binary $(APP).bin 81 | 82 | $(APP).imx: SOURCE_DATE_EPOCH=0 83 | $(APP).imx: $(APP).bin $(APP).dcd 84 | mkimage -n $(APP).dcd -T imximage -e $(TEXT_START) -d $(APP).bin $(APP).imx 85 | # Copy entry point from ELF file 86 | dd if=$(APP) of=$(APP).imx bs=1 count=4 skip=24 seek=4 conv=notrunc 87 | 88 | #### secure boot #### 89 | 90 | $(APP)-signed.imx: check_hab_keys $(APP).imx 91 | ${TAMAGO} install github.com/usbarmory/crucible/cmd/habtool@latest 92 | $(shell ${TAMAGO} env GOPATH)/bin/habtool \ 93 | -A ${HAB_KEYS}/CSF_1_key.pem \ 94 | -a ${HAB_KEYS}/CSF_1_crt.pem \ 95 | -B ${HAB_KEYS}/IMG_1_key.pem \ 96 | -b ${HAB_KEYS}/IMG_1_crt.pem \ 97 | -t ${HAB_KEYS}/SRK_1_2_3_4_table.bin \ 98 | -x 1 \ 99 | -s \ 100 | -i $(APP).imx \ 101 | -o $(APP).csf && \ 102 | cat $(APP).imx $(APP).csf > $(APP)-signed.imx 103 | -------------------------------------------------------------------------------- /usb.go: -------------------------------------------------------------------------------- 1 | // https://github.com/usbarmory/armory-ums 2 | // 3 | // Copyright (c) The armory-ums authors. All Rights Reserved. 4 | // 5 | // Use of this source code is governed by the license 6 | // that can be found in the LICENSE file. 7 | 8 | package main 9 | 10 | import ( 11 | "bytes" 12 | "encoding/binary" 13 | "errors" 14 | "fmt" 15 | 16 | "github.com/usbarmory/tamago/dma" 17 | "github.com/usbarmory/tamago/soc/nxp/imx6ul" 18 | "github.com/usbarmory/tamago/soc/nxp/usb" 19 | ) 20 | 21 | const maxPacketSize = 512 22 | 23 | // queue for IN device responses 24 | var send = make(chan []byte, 2) 25 | 26 | // queue for IN device DMA buffers for later release 27 | var free = make(chan uint, 1) 28 | 29 | func configureDevice(device *usb.Device) { 30 | // Supported Language Code Zero: English 31 | device.SetLanguageCodes([]uint16{0x0409}) 32 | 33 | // device descriptor 34 | device.Descriptor = &usb.DeviceDescriptor{} 35 | device.Descriptor.SetDefaults() 36 | 37 | // http://pid.codes/1209/2702/ 38 | device.Descriptor.VendorId = 0x1209 39 | device.Descriptor.ProductId = 0x2702 40 | 41 | device.Descriptor.Device = 0x0001 42 | 43 | iManufacturer, _ := device.AddString(`TamaGo`) 44 | device.Descriptor.Manufacturer = iManufacturer 45 | 46 | iProduct, _ := device.AddString(`Storage Media`) 47 | device.Descriptor.Product = iProduct 48 | 49 | // p9, 4.1.1 Serial Number, USB Mass Storage Class 1.0 50 | // 51 | // The serial number format is [0-9A-F]{12,}, the NXP Unique 52 | // ID is converted accordingly. 53 | iSerial, _ := device.AddString(fmt.Sprintf("%X", imx6ul.UniqueID())) 54 | device.Descriptor.SerialNumber = iSerial 55 | 56 | conf := &usb.ConfigurationDescriptor{} 57 | conf.SetDefaults() 58 | 59 | device.AddConfiguration(conf) 60 | 61 | // device qualifier 62 | device.Qualifier = &usb.DeviceQualifierDescriptor{} 63 | device.Qualifier.SetDefaults() 64 | device.Qualifier.NumConfigurations = uint8(len(device.Configurations)) 65 | } 66 | 67 | func buildMassStorageInterface() (iface *usb.InterfaceDescriptor) { 68 | // interface 69 | iface = &usb.InterfaceDescriptor{} 70 | iface.SetDefaults() 71 | 72 | iface.NumEndpoints = 2 73 | iface.InterfaceClass = usb.MASS_STORAGE_CLASS 74 | iface.InterfaceSubClass = usb.SCSI_CLASS 75 | iface.InterfaceProtocol = usb.BULK_ONLY_TRANSPORT_PROTOCOL 76 | iface.Interface = 0 77 | 78 | // EP1 IN endpoint (bulk) 79 | ep1IN := &usb.EndpointDescriptor{} 80 | ep1IN.SetDefaults() 81 | ep1IN.EndpointAddress = 0x81 82 | ep1IN.Attributes = 2 83 | ep1IN.MaxPacketSize = maxPacketSize 84 | ep1IN.Zero = false 85 | ep1IN.Function = tx 86 | 87 | iface.Endpoints = append(iface.Endpoints, ep1IN) 88 | 89 | // EP2 OUT endpoint (bulk) 90 | ep1OUT := &usb.EndpointDescriptor{} 91 | ep1OUT.SetDefaults() 92 | ep1OUT.EndpointAddress = 0x01 93 | ep1OUT.Attributes = 2 94 | ep1OUT.MaxPacketSize = maxPacketSize 95 | ep1OUT.Zero = false 96 | ep1OUT.Function = rx 97 | 98 | iface.Endpoints = append(iface.Endpoints, ep1OUT) 99 | 100 | return 101 | } 102 | 103 | // setup handles the class specific control requests specified at 104 | // p7, 3.1 - 3.2, USB Mass Storage Class 1.0 105 | func setup(setup *usb.SetupData) (in []byte, ack bool, done bool, err error) { 106 | switch setup.Request { 107 | case usb.BULK_ONLY_MASS_STORAGE_RESET: 108 | // For we ack this request without resetting. 109 | case usb.GET_MAX_LUN: 110 | if len(cards) == 0 { 111 | err = errors.New("unsupported") 112 | } else { 113 | in = []byte{byte(len(cards) - 1)} 114 | } 115 | } 116 | 117 | return 118 | } 119 | 120 | func parseCBW(buf []byte) (cbw *usb.CBW, err error) { 121 | if len(buf) == 0 { 122 | return 123 | } 124 | 125 | if len(buf) != usb.CBW_LENGTH { 126 | return nil, fmt.Errorf("invalid CBW size %d != %d", len(buf), usb.CBW_LENGTH) 127 | } 128 | 129 | cbw = &usb.CBW{} 130 | err = binary.Read(bytes.NewReader(buf), binary.LittleEndian, cbw) 131 | 132 | if err != nil { 133 | return 134 | } 135 | 136 | if cbw.Length < 6 || cbw.Length > usb.CBW_CB_MAX_LENGTH { 137 | return nil, fmt.Errorf("invalid Command Block Length %d", cbw.Length) 138 | } 139 | 140 | if cbw.Signature != usb.CBW_SIGNATURE { 141 | return nil, fmt.Errorf("invalid CBW signature %x", cbw.Signature) 142 | } 143 | 144 | return 145 | } 146 | 147 | func rx(buf []byte, lastErr error) (res []byte, err error) { 148 | var cbw *usb.CBW 149 | 150 | if dataPending != nil { 151 | defer dma.Release(dataPending.addr) 152 | err = handleWrite(dataPending.buf) 153 | 154 | if err != nil { 155 | return 156 | } 157 | 158 | csw := dataPending.csw 159 | csw.DataResidue = 0 160 | 161 | send <- dataPending.csw.Bytes() 162 | 163 | dataPending = nil 164 | 165 | return 166 | } 167 | 168 | cbw, err = parseCBW(buf) 169 | 170 | if err != nil { 171 | return 172 | } 173 | 174 | csw, data, err := handleCDB(cbw.CommandBlock, cbw) 175 | 176 | defer func() { 177 | if csw != nil { 178 | send <- csw.Bytes() 179 | } 180 | }() 181 | 182 | if err != nil { 183 | csw.DataResidue = cbw.DataTransferLength 184 | csw.Status = usb.CSW_STATUS_COMMAND_FAILED 185 | return 186 | } 187 | 188 | if len(data) > 0 { 189 | send <- data 190 | } 191 | 192 | if dataPending != nil { 193 | dataPending.addr, dataPending.buf = dma.Reserve(dataPending.size, usb.DTD_PAGE_SIZE) 194 | res = dataPending.buf 195 | } 196 | 197 | return 198 | } 199 | 200 | func tx(_ []byte, lastErr error) (in []byte, err error) { 201 | select { 202 | case buf := <-free: 203 | dma.Release(buf) 204 | default: 205 | } 206 | 207 | in = <-send 208 | 209 | if reserved, addr := dma.Reserved(in); reserved { 210 | free <- addr 211 | } 212 | 213 | return 214 | } 215 | -------------------------------------------------------------------------------- /scsi.go: -------------------------------------------------------------------------------- 1 | // https://github.com/usbarmory/armory-ums 2 | // 3 | // Copyright (c) The armory-ums authors. All Rights Reserved. 4 | // 5 | // Use of this source code is governed by the license 6 | // that can be found in the LICENSE file. 7 | 8 | package main 9 | 10 | import ( 11 | "bytes" 12 | "encoding/binary" 13 | "fmt" 14 | 15 | "github.com/usbarmory/tamago/dma" 16 | "github.com/usbarmory/tamago/soc/nxp/usb" 17 | "github.com/usbarmory/tamago/soc/nxp/usdhc" 18 | ) 19 | 20 | const ( 21 | // p65, 3. Direct Access Block commands (SPC-5 and SBC-4), SCSI Commands Reference Manual, Rev. J 22 | TEST_UNIT_READY = 0x00 23 | REQUEST_SENSE = 0x03 24 | INQUIRY = 0x12 25 | MODE_SENSE_6 = 0x1a 26 | MODE_SENSE_10 = 0x5a 27 | READ_CAPACITY_10 = 0x25 28 | READ_10 = 0x28 29 | WRITE_10 = 0x2a 30 | REPORT_LUNS = 0xa0 31 | 32 | // service actions 33 | SERVICE_ACTION = 0x9e 34 | READ_CAPACITY_16 = 0x10 35 | 36 | // 04-349r1 SPC-3 MMC-5 Merge PREVENT ALLOW MEDIUM REMOVAL commands 37 | PREVENT_ALLOW_MEDIUM_REMOVAL = 0x1e 38 | 39 | // p33, 4.10, USB Mass Storage Class – UFI Command Specification Rev. 1.0 40 | READ_FORMAT_CAPACITIES = 0x23 41 | ) 42 | 43 | const ( 44 | // exactly 8 bytes required, historical value kept 45 | VendorID = "F-Secure" 46 | // exactly 16 bytes required 47 | ProductID = "USB armory Mk II" 48 | // exactly 4 bytes required 49 | ProductRevision = "1.00" 50 | ) 51 | 52 | type writeOp struct { 53 | csw *usb.CSW 54 | lun int 55 | lba int 56 | blocks int 57 | size int 58 | addr uint 59 | buf []byte 60 | } 61 | 62 | // buffer for write commands (which spawn across multiple USB transfers) 63 | var dataPending *writeOp 64 | 65 | // p94, 3.6.2 Standard INQUIRY data, SCSI Commands Reference Manual, Rev. J 66 | func inquiry(length int) (data []byte) { 67 | data = make([]byte, 5) 68 | 69 | // device connected, direct access block device 70 | data[0] = 0x00 71 | // Removable Media 72 | data[1] = 0x80 73 | // SPC-3 compliant 74 | data[2] = 0x05 75 | // response data format (only 2 is allowed) 76 | data[3] = 0x02 77 | // additional length 78 | data[4] = byte(length - 5) 79 | 80 | // unused or obsolete flags 81 | data = append(data, make([]byte, 3)...) 82 | 83 | data = append(data, []byte(VendorID)...) 84 | data = append(data, []byte(ProductID)...) 85 | data = append(data, []byte(ProductRevision)...) 86 | 87 | if length > len(data) { 88 | // pad up to requested transfer length 89 | data = append(data, make([]byte, length-len(data))...) 90 | } else { 91 | data = data[0:length] 92 | } 93 | 94 | return 95 | } 96 | 97 | // p56, 2.4.1.2 Fixed format sense data, SCSI Commands Reference Manual, Rev. J 98 | func sense(length int) (data []byte, err error) { 99 | data = make([]byte, 18) 100 | 101 | // error code 102 | data[0] = 0x70 103 | // no specific sense key 104 | data[2] = 0x00 105 | // additional sense length 106 | data[7] = byte(len(data) - 1 - 7) 107 | // no additional sense code 108 | data[12] = 0x00 109 | // no additional sense qualifier 110 | 111 | if length < len(data) { 112 | return nil, fmt.Errorf("unsupported REQUEST_SENSE transfer length %d > %d", length, len(data)) 113 | } 114 | 115 | return 116 | } 117 | 118 | // p111, 3.11 MODE SENSE(6) command, SCSI Commands Reference Manual, Rev. J 119 | func modeSense(length int) (data []byte, err error) { 120 | // Unsupported, an empty response is returned on all requests. 121 | data = make([]byte, length) 122 | 123 | // p378, 5.3.3 Mode parameter header formats, SCSI Commands Reference Manual, Rev. J 124 | data[0] = byte(length) 125 | 126 | return 127 | } 128 | 129 | // p179, 3.33 REPORT LUNS command, SCSI Commands Reference Manual, Rev. J 130 | func reportLUNs(length int) (data []byte, err error) { 131 | buf := new(bytes.Buffer) 132 | luns := len(cards) 133 | 134 | binary.Write(buf, binary.BigEndian, uint32(luns*8)) 135 | buf.Write(make([]byte, 4)) 136 | 137 | for lun := 0; lun < luns; lun++ { 138 | // The information conforms to the Logical Unit Address Method defined 139 | // in SCC-2, and supports only First Level addressing (for each LUN, 140 | // only the second byte is used and contains the assigned LUN)." 141 | buf.WriteByte(0x00) 142 | binary.Write(buf, binary.BigEndian, uint8(lun)) 143 | buf.Write(make([]byte, 6)) 144 | } 145 | 146 | data = buf.Bytes() 147 | 148 | if length < buf.Len() { 149 | data = data[0:length] 150 | } 151 | 152 | return 153 | } 154 | 155 | // p155, 3.22 READ CAPACITY (10) command, SCSI Commands Reference Manual, Rev. J 156 | func readCapacity10(card *usdhc.USDHC) (data []byte, err error) { 157 | info := card.Info() 158 | 159 | if info.Blocks <= 0 { 160 | return nil, fmt.Errorf("invalid block count %d", info.Blocks) 161 | } 162 | 163 | buf := new(bytes.Buffer) 164 | 165 | binary.Write(buf, binary.BigEndian, uint32(info.Blocks)-1) 166 | binary.Write(buf, binary.BigEndian, uint32(info.BlockSize)) 167 | 168 | return buf.Bytes(), nil 169 | } 170 | 171 | // p157, 3.23 READ CAPACITY (16) command, SCSI Commands Reference Manual, Rev. J 172 | func readCapacity16(card *usdhc.USDHC, length int) (data []byte, err error) { 173 | info := card.Info() 174 | buf := new(bytes.Buffer) 175 | 176 | if info.Blocks <= 0 { 177 | return nil, fmt.Errorf("invalid block count %d", info.Blocks) 178 | } 179 | 180 | binary.Write(buf, binary.BigEndian, uint64(info.Blocks)-1) 181 | binary.Write(buf, binary.BigEndian, uint64(info.BlockSize)) 182 | buf.Grow(32 - buf.Len()) 183 | 184 | data = buf.Bytes() 185 | 186 | if length < buf.Len() { 187 | data = data[0:length] 188 | } 189 | 190 | return 191 | } 192 | 193 | // p33, 4.10, USB Mass Storage Class – UFI Command Specification Rev. 1.0 194 | func readFormatCapacities(card *usdhc.USDHC) (data []byte, err error) { 195 | info := card.Info() 196 | buf := new(bytes.Buffer) 197 | 198 | // capacity list length 199 | binary.Write(buf, binary.BigEndian, uint32(8)) 200 | // number of blocks 201 | binary.Write(buf, binary.BigEndian, uint32(info.Blocks)) 202 | // descriptor code: formatted media | block length 203 | binary.Write(buf, binary.BigEndian, uint32(0b10<<24|info.BlockSize&0xffffff)) 204 | 205 | return buf.Bytes(), nil 206 | } 207 | 208 | func read(card *usdhc.USDHC, lba int, blocks int) (err error) { 209 | addr, buf := dma.Reserve(blocks*card.Info().BlockSize, usb.DTD_PAGE_SIZE) 210 | 211 | err = card.ReadBlocks(lba, buf) 212 | 213 | if err != nil { 214 | dma.Release(addr) 215 | return 216 | } 217 | 218 | send <- buf 219 | 220 | return 221 | } 222 | 223 | func write(card *usdhc.USDHC, lba int, buf []byte) (err error) { 224 | return card.WriteBlocks(lba, buf) 225 | } 226 | 227 | func handleCDB(cmd [16]byte, cbw *usb.CBW) (csw *usb.CSW, data []byte, err error) { 228 | op := cmd[0] 229 | length := int(cbw.DataTransferLength) 230 | 231 | // p8, 3.3 Host/Device Packet Transfer Order, USB Mass Storage Class 1.0 232 | csw = &usb.CSW{Tag: cbw.Tag} 233 | csw.SetDefaults() 234 | 235 | lun := int(cbw.LUN) 236 | 237 | if int(lun+1) > len(cards) { 238 | err = fmt.Errorf("invalid LUN") 239 | return 240 | } 241 | 242 | card := cards[lun] 243 | 244 | switch op { 245 | case INQUIRY: 246 | data = inquiry(length) 247 | case REQUEST_SENSE: 248 | data, err = sense(length) 249 | case MODE_SENSE_6, MODE_SENSE_10: 250 | data, err = modeSense(length) 251 | case REPORT_LUNS: 252 | data, err = reportLUNs(length) 253 | case READ_FORMAT_CAPACITIES: 254 | data, err = readFormatCapacities(card) 255 | case READ_CAPACITY_10: 256 | data, err = readCapacity10(card) 257 | case READ_10, WRITE_10: 258 | lba := int(binary.BigEndian.Uint32(cmd[2:])) 259 | blocks := int(binary.BigEndian.Uint16(cmd[7:])) 260 | 261 | if op == READ_10 { 262 | err = read(card, lba, blocks) 263 | } else { 264 | size := int(cbw.DataTransferLength) 265 | 266 | if card.Info().BlockSize*blocks != size { 267 | err = fmt.Errorf("unexpected %d blocks write transfer length (%d)", blocks, size) 268 | } 269 | 270 | dataPending = &writeOp{ 271 | csw: csw, 272 | lun: lun, 273 | lba: lba, 274 | blocks: blocks, 275 | size: size, 276 | } 277 | 278 | csw = nil 279 | } 280 | case SERVICE_ACTION: 281 | switch cmd[1] { 282 | case READ_CAPACITY_16: 283 | data, err = readCapacity16(card, length) 284 | default: 285 | err = fmt.Errorf("unsupported service action %#x %+v", op, cbw) 286 | } 287 | case TEST_UNIT_READY, PREVENT_ALLOW_MEDIUM_REMOVAL: 288 | // ignored events 289 | default: 290 | err = fmt.Errorf("unsupported CDB Operation Code %#x %+v", op, cbw) 291 | } 292 | 293 | return 294 | } 295 | 296 | func handleWrite(buf []byte) (err error) { 297 | if len(buf) != dataPending.size { 298 | return fmt.Errorf("len(buf) != size (%d != %d)", len(buf), dataPending.size) 299 | } 300 | 301 | return write(cards[dataPending.lun], dataPending.lba, buf) 302 | } 303 | --------------------------------------------------------------------------------