diff --git a/imsi-change/.gitignore b/imsi-change/.gitignore new file mode 100644 index 0000000..5150de5 --- /dev/null +++ b/imsi-change/.gitignore @@ -0,0 +1,3 @@ +build/ +test/ +.sim-keys diff --git a/imsi-change/LICENSE b/imsi-change/LICENSE new file mode 100644 index 0000000..d645695 --- /dev/null +++ b/imsi-change/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/imsi-change/Makefile b/imsi-change/Makefile new file mode 100644 index 0000000..9656b05 --- /dev/null +++ b/imsi-change/Makefile @@ -0,0 +1,71 @@ +SIMTOOLS_DIR = ../../sim-tools + +APPLET_AID = 0xd0:0x70:0x02:0xca:0x44:0x90:0x01:0x01 +APPLET_NAME = org.osmocom.IMSIChange.IMSIChange +PACKAGE_AID = 0xd0:0x70:0x02:0xCA:0x44:0x90:0x01 +PACKAGE_NAME = org.osmocom.IMSIChange +PACKAGE_VERSION = 1.0 + +SOURCES = \ + src/org/osmocom/IMSIChange/Bytes.java \ + src/org/osmocom/IMSIChange/MobileIdentity.java \ + src/org/osmocom/IMSIChange/IMSIChange.java \ + $(NULL) + +CAP_FILE = build/javacard/org/osmocom/IMSIChange/javacard/IMSIChange.cap + +include ./applet-project.mk + +.PHONY: flash +flash: classes + $(eval MODULE_AID := $(shell echo $(APPLET_AID) | sed 's/0x//g' | sed 's/\://g')) + $(eval INSTANCE_AID := $(shell echo $(APPLET_AID) | sed 's/0x//g' | sed 's/\://g')) + . $$PWD/.sim-keys && $(SIMTOOLS_DIR)/bin/shadysim \ + --pcsc \ + -l $(CAP_FILE) \ + -i $(CAP_FILE) \ + --enable-sim-toolkit \ + --access-domain=00 \ + --module-aid $(MODULE_AID) \ + --instance-aid $(INSTANCE_AID) \ + --nonvolatile-memory-required 0100 \ + --volatile-memory-for-install 0100 \ + --max-menu-entry-text 21 \ + --max-menu-entries 01 \ + --kic "$$KIC1" \ + --kid "$$KID1" + +.PHONY: remove +remove: + . $$PWD/.sim-keys && $(SIMTOOLS_DIR)/bin/shadysim \ + --pcsc \ + -d "$$(echo $(PACKAGE_AID) | sed 's/0x//g' | sed 's/\://g')" \ + --kic "$$KIC1" \ + --kid "$$KID1" + +.PHONY: list +list: + . $$PWD/.sim-keys && $(SIMTOOLS_DIR)/bin/shadysim \ + --pcsc \ + --list-applets \ + --kic "$$KIC1" \ + --kid "$$KID1" + +.PHONY: delete +delete: remove + +.PHONY: reflash +reflash: + $(MAKE) remove + $(MAKE) flash + +.PHONY: test +test: + mkdir -p ./test/classes + javac -target 1.1 -source 1.3 -classpath test/classes -g -d ./test/classes src/org/osmocom/IMSIChange/Bytes.java + javac -target 1.1 -source 1.3 -classpath test/classes -g -d ./test/classes src/org/osmocom/IMSIChange/MobileIdentity.java + javac -target 1.1 -source 1.3 -classpath test/classes -g -d ./test/classes src/org/osmocom/IMSIChange/Test.java + java -classpath test/classes org.osmocom.IMSIChange.Test + +.PHONY: check +check: test diff --git a/imsi-change/README.md b/imsi-change/README.md new file mode 100644 index 0000000..99d1a55 --- /dev/null +++ b/imsi-change/README.md @@ -0,0 +1,26 @@ +# IMSI change SIM applet + +Display and change the IMSI of the SIM. This is a standalone version of a debug +feature in the more complex IMSI Pseudonymization applet. To be used as example +code to build other applets. + +### How to flash + +``` +$ cp .sim-keys.example .sim-keys +$ nvim .sim-keys # adjust KIC1, KID1 +$ make flash +``` + +Before flashing a second time, remove the sim applet: + +``` +$ make remove +``` + +### Related + +* [IMSI Pseudonymization](https://osmocom.org/projects/imsi-pseudo/wiki) +* [Shadysimply in Osmocom wiki](https://osmocom.org/projects/cellular-infrastructure/wiki/Shadysimpy) + + diff --git a/imsi-change/applet-project.mk b/imsi-change/applet-project.mk new file mode 100644 index 0000000..982e768 --- /dev/null +++ b/imsi-change/applet-project.mk @@ -0,0 +1,51 @@ +BUILD_DIR = ./build +BUILD_CLASSES_DIR = $(BUILD_DIR)/classes +BUILD_JAVACARD_DIR = $(BUILD_DIR)/javacard +JAVACARD_SDK_DIR ?= $(SIMTOOLS_DIR)/javacard +JAVACARD_EXPORT_DIR ?= $(JAVACARD_SDK_DIR)/api21_export_files +ifdef COMSPEC + CLASSPATH = $(JAVACARD_SDK_DIR)/lib/api21.jar;$(JAVACARD_SDK_DIR)/lib/sim.jar +else + CLASSPATH = $(JAVACARD_SDK_DIR)/lib/api21.jar:$(JAVACARD_SDK_DIR)/lib/sim.jar +endif +JFLAGS = -target 1.1 -source 1.3 -g -d $(BUILD_CLASSES_DIR) -classpath "$(BUILD_CLASSES_DIR):$(CLASSPATH)" +JAVA ?= java +JC ?= javac + +.SUFFIXES: .java .class +.java.class: + @mkdir -p $(BUILD_CLASSES_DIR) + @mkdir -p $(BUILD_JAVACARD_DIR) + $(JC) $(JFLAGS) $*.java + +.PHONY: jar +jar: classes + $(JAVA) -jar $(JAVACARD_SDK_DIR)/bin/converter.jar \ + -d $(BUILD_JAVACARD_DIR) \ + -classdir $(BUILD_CLASSES_DIR) \ + -exportpath $(JAVACARD_EXPORT_DIR) \ + -applet $(APPLET_AID) $(APPLET_NAME) \ + $(PACKAGE_NAME) $(PACKAGE_AID) $(PACKAGE_VERSION) + +default: jar + +classes: $(SOURCES:.java=.class) + +clean: + $(RM) -rf $(BUILD_DIR) + +install: + $(eval CAP_FILE := $(shell find $(BUILD_JAVACARD_DIR) -name *.cap)) + $(eval MODULE_AID := $(shell echo $(APPLET_AID) | sed 's/0x//g' | sed 's/\://g')) + $(eval INSTANCE_AID := $(shell echo $(APPLET_AID) | sed 's/0x//g' | sed 's/\://g')) + $(SIMTOOLS_DIR)/bin/shadysim \ + $(SHADYSIM_OPTIONS) \ + -l $(CAP_FILE) \ + -i $(CAP_FILE) \ + --enable-sim-toolkit \ + --module-aid $(MODULE_AID) \ + --instance-aid $(INSTANCE_AID) \ + --nonvolatile-memory-required 0100 \ + --volatile-memory-for-install 0100 \ + --max-menu-entry-text 10 \ + --max-menu-entries 01 diff --git a/imsi-change/src/org/osmocom/IMSIChange/Bytes.java b/imsi-change/src/org/osmocom/IMSIChange/Bytes.java new file mode 100644 index 0000000..f4f2505 --- /dev/null +++ b/imsi-change/src/org/osmocom/IMSIChange/Bytes.java @@ -0,0 +1,82 @@ +/* Copyright 2020 sysmocom s.f.m.c. GmbH + * SPDX-License-Identifier: Apache-2.0 */ +package org.osmocom.IMSIChange; + +public class Bytes { + public static byte nibble2hex(byte nibble) + { + nibble = (byte)(nibble & 0xf); + if (nibble < 0xa) + return (byte)('0' + nibble); + else + return (byte)('a' + nibble - 0xa); + } + + public static byte[] hexdump(byte data[]) + { + byte res[] = new byte[(byte)(data.length*2)]; + for (byte i = 0; i < data.length; i++) { + res[(byte)(i*2)] = nibble2hex((byte)(data[i] >> 4)); + res[(byte)(i*2 + 1)] = nibble2hex(data[i]); + } + return res; + } + + public static boolean equals(byte a[], byte b[]) + { + if (a.length != b.length) + return false; + for (short i = 0; i < (short)a.length; i++) { + if (a[i] != b[i]) + return false; + } + return true; + } + + public static boolean isDigit(byte digits[]) + { + for (short i = 0; i < (short)digits.length; i++) { + if (digits[i] < '0' || digits[i] > '9') + return false; + } + return true; + } + + public static byte[] toStr(byte byte_nr) + { + byte str[]; + short nr = byte_nr; + byte d; + byte l = 0; + if (nr < 0) { + l = 1; + nr = (short)-nr; + } + + if (nr > 99) { + l += 3; + d = 100; + } + else if (nr > 9) { + l += 2; + d = 10; + } + else { + str = new byte[1]; + l += 1; + d = 1; + } + + byte i = 0; + str = new byte[l]; + if (byte_nr < 0) + str[i++] = '-'; + + while (d > 0) { + str[i++] = (byte)('0' + (nr / d)); + nr %= d; + d /= 10; + } + return str; + } +} diff --git a/imsi-change/src/org/osmocom/IMSIChange/IMSIChange.java b/imsi-change/src/org/osmocom/IMSIChange/IMSIChange.java new file mode 100755 index 0000000..6135596 --- /dev/null +++ b/imsi-change/src/org/osmocom/IMSIChange/IMSIChange.java @@ -0,0 +1,176 @@ +/* Copyright 2020 sysmocom s.f.m.c. GmbH + * SPDX-License-Identifier: Apache-2.0 */ +package org.osmocom.IMSIChange; +import org.osmocom.IMSIChange.MobileIdentity; + +import sim.access.*; +import sim.toolkit.*; +import javacard.framework.*; + +public class IMSIChange extends Applet implements ToolkitInterface, ToolkitConstants { + // DON'T DECLARE USELESS INSTANCE VARIABLES! They get saved to the EEPROM, + // which has a limited number of write cycles. + + private byte STKServicesMenuId; + private SIMView gsmFile; + + /* Main menu */ + private static final byte[] changeIMSI = {'C', 'h', 'a', 'n', 'g', 'e', ' ', 'I', 'M', 'S', 'I'}; + private static final byte[] invalidIMSI = {'I', 'n', 'v', 'a', 'l', 'i', 'd', ' ', 'I', 'M', 'S', 'I'}; + private static final byte[] noChange = {'N', 'o', ' ', 'c', 'h', 'a', 'n', 'g', 'e'}; + private static final byte[] changed = {'I', 'M', 'S', 'I', ' ', 'c', 'h', 'a', 'n', 'g', 'e', 'd', '!'}; + + private IMSIChange() { + gsmFile = SIMSystem.getTheSIMView(); + + ToolkitRegistry reg = ToolkitRegistry.getEntry(); + STKServicesMenuId = reg.initMenuEntry(changeIMSI, (short)0, (short)changeIMSI.length, + PRO_CMD_SELECT_ITEM, false, (byte)0, (short)0); + } + + public static void install(byte[] bArray, short bOffset, byte bLength) { + IMSIChange applet = new IMSIChange(); + applet.register(); + } + + public void process(APDU arg0) throws ISOException { + if (selectingApplet()) + return; + } + + public void processToolkit(byte event) throws ToolkitException { + EnvelopeHandler envHdlr = EnvelopeHandler.getTheHandler(); + + if (event == EVENT_MENU_SELECTION) { + byte selectedItemId = envHdlr.getItemIdentifier(); + + if (selectedItemId == STKServicesMenuId) { + byte prevIMSI_mi[] = readIMSI(); + byte prevIMSI_str[] = MobileIdentity.mi2str(prevIMSI_mi); + promptIMSI(prevIMSI_str); + } + } + } + + private void showMsg(byte[] msg) { + ProactiveHandler proHdlr = ProactiveHandler.getTheHandler(); + proHdlr.initDisplayText((byte)0, DCS_8_BIT_DATA, msg, (short)0, (short)(msg.length)); + proHdlr.send(); + } + + private byte[] getResponse() + { + ProactiveResponseHandler rspHdlr = ProactiveResponseHandler.getTheHandler(); + byte[] resp = new byte[rspHdlr.getTextStringLength()]; + rspHdlr.copyTextString(resp, (short)0); + return resp; + } + + private byte[] prompt(byte[] msg, byte[] prefillVal, short minLen, short maxLen) { + /* if maxLen < 1, the applet crashes */ + if (maxLen < 1) + maxLen = 1; + + ProactiveHandler proHdlr = ProactiveHandler.getTheHandler(); + proHdlr.initGetInput((byte)0, DCS_8_BIT_DATA, msg, (short)0, (short)(msg.length), minLen, maxLen); + if (prefillVal != null && prefillVal.length > 0) { + /* appendTLV() expects the first byte to be some header before the actual text. + * At first I thought it was the value's length, but turned out to only work for lengths under 8... + * In the end I reversed the value 4 from the first byte read by rspHdlr.copyValue() for + * TAG_TEXT_STRING fields. As long as we write 4 into the first byte, things just work out, + * apparently. + * This is the appendTLV() variant that writes one byte ahead of writing an array: */ + proHdlr.appendTLV((byte)(TAG_DEFAULT_TEXT), (byte)4, prefillVal, (short)0, + (short)(prefillVal.length)); + } + proHdlr.send(); + + return getResponse(); + } + + private void showError(short code) { + byte[] msg = {'E', '?', '?'}; + msg[1] = (byte)('0' + code / 10); + msg[2] = (byte)('0' + code % 10); + showMsg(msg); + } + + private void promptIMSI(byte prevIMSI_str[]) + { + byte newIMSI_str[] = prevIMSI_str; + + try { + newIMSI_str = prompt(changeIMSI, newIMSI_str, (short)0, (short)15); + } catch (Exception e) { + showError((short)40); + return; + } + + if (newIMSI_str.length < 6 || newIMSI_str.length > 15 + || !Bytes.isDigit(newIMSI_str)) { + showMsg(invalidIMSI); + return; + } + + if (Bytes.equals(newIMSI_str, prevIMSI_str)) { + showMsg(noChange); + return; + } + + byte mi[]; + try { + /* The IMSI file should be 9 bytes long, even if the IMSI is shorter */ + mi = MobileIdentity.str2mi(newIMSI_str, MobileIdentity.MI_IMSI, (byte)9); + writeIMSI(mi); + showMsg(changed); + refreshIMSI(); + } catch (Exception e) { + showError((short)42); + } + } + + private byte[] readIMSI() + { + gsmFile.select((short) SIMView.FID_DF_GSM); + gsmFile.select((short) SIMView.FID_EF_IMSI); + byte[] IMSI = new byte[9]; + gsmFile.readBinary((short)0, IMSI, (short)0, (short)9); + return IMSI; + } + + private void writeIMSI(byte mi[]) throws Exception + { + if (mi.length != 9) + throw new Exception(); + gsmFile.select((short) SIMView.FID_DF_GSM); + gsmFile.select((short) SIMView.FID_EF_IMSI); + gsmFile.updateBinary((short)0, mi, (short)0, (short)mi.length); + } + + /* + * - command qualifiers for REFRESH, + * ETSI TS 101 267 / 3GPP TS 11.14 chapter 12.6 "Command details": + * '00' =SIM Initialization and Full File Change Notification; + * '01' = File Change Notification; + * '02' = SIM Initialization and File Change Notification; + * '03' = SIM Initialization; + * '04' = SIM Reset; + * '05' to 'FF' = reserved values. + */ + public static final byte SIM_REFRESH_SIM_INIT_FULL_FILE_CHANGE = 0x00; + public static final byte SIM_REFRESH_FILE_CHANGE = 0x01; + public static final byte SIM_REFRESH_SIM_INIT_FILE_CHANGE = 0x02; + public static final byte SIM_REFRESH_SIM_INIT = 0x03; + public static final byte SIM_REFRESH_SIM_RESET = 0x04; + + /* Run the Proactive SIM REFRESH command for the FID_EF_IMSI. */ + private void refreshIMSI() + { + /* See ETSI TS 101 267 / 3GPP TS 11.14 section 6.4.7.1 "EF IMSI changing procedure": + * Valid qualifiers are SIM_REFRESH_SIM_INIT_FILE_CHANGE and SIM_REFRESH_SIM_INIT_FULL_FILE_CHANGE. + */ + ProactiveHandler proHdlr = ProactiveHandler.getTheHandler(); + proHdlr.init((byte)PRO_CMD_REFRESH, SIM_REFRESH_SIM_INIT_FULL_FILE_CHANGE, DEV_ID_ME); + proHdlr.send(); + } +} diff --git a/imsi-change/src/org/osmocom/IMSIChange/MobileIdentity.java b/imsi-change/src/org/osmocom/IMSIChange/MobileIdentity.java new file mode 100644 index 0000000..54fa2b0 --- /dev/null +++ b/imsi-change/src/org/osmocom/IMSIChange/MobileIdentity.java @@ -0,0 +1,152 @@ +/* Copyright 2020 sysmocom s.f.m.c. GmbH + * SPDX-License-Identifier: Apache-2.0 */ +package org.osmocom.IMSIChange; + +public class MobileIdentity { + public static final byte MI_IMSI = 1; + + /* Convert BCD-encoded digit into printable character + * \param[in] bcd A single BCD-encoded digit + * \returns single printable character + */ + public static byte bcd2char(byte bcd) + { + if (bcd < 0xa) + return (byte)('0' + bcd); + else + return (byte)('A' + (bcd - 0xa)); + } + + /* Convert BCD to string. + * The given nibble offsets are interpreted in BCD order, i.e. nibble 0 is bcd[0] & 0xf, nibble 1 is bcd[0] >> 4, nibble + * 3 is bcd[1] & 0xf, etc.. + * \param[out] dst Output byte array. + * \param[in] dst_ofs Where to start writing in dst. + * \param[in] dst_len How many bytes are available at dst_ofs. + * \param[in] bcd Binary coded data buffer. + * \param[in] start_nibble Offset to start from, in nibbles. + * \param[in] end_nibble Offset to stop before, in nibbles. + * \param[in] allow_hex If false, return false if there are digits other than 0-9. + * \returns true on success, false otherwise + */ + public static boolean bcd2str(byte dst[], byte dst_ofs, byte dst_len, + byte bcd[], byte start_nibble, byte end_nibble, boolean allow_hex) + { + byte nibble_i; + byte dst_i = dst_ofs; + byte dst_end = (byte)(dst_ofs + dst_len); + boolean rc = true; + + for (nibble_i = start_nibble; nibble_i < end_nibble && dst_i < dst_end; nibble_i++, dst_i++) { + byte nibble = bcd[(byte)nibble_i >> 1]; + if ((nibble_i & 1) != 0) + nibble >>= 4; + nibble &= 0xf; + + if (!allow_hex && nibble > 9) + rc = false; + + dst[dst_i] = bcd2char(nibble); + } + + return rc; + } + + public static byte[] mi2str(byte mi[]) + { + /* The IMSI byte array by example: + * 08 99 10 07 00 00 10 74 90 + * + * This is encoded according to 3GPP TS 24.008 10.5.1.4 Mobile + * Identity, short the Mobile Identity IEI: + * + * 08 length for the following MI, in bytes. + * 9 = 0b1001 + * 1 = odd nr of digits + * 001 = MI type = IMSI + * 9 first IMSI digit (BCD) + * 0 second digit + * 1 third + * ... + * 0 14th digit + * 9 15th and last digit + * + * If the IMSI had an even number of digits: + * + * 08 98 10 07 00 00 10 74 f0 + * + * 08 length for the following MI, in bytes. + * 8 = 0b0001 + * 0 = even nr of digits + * 001 = MI type = IMSI + * 9 first IMSI digit + * 0 second digit + * 1 third + * ... + * 0 14th and last digit + * f filler + */ + byte bytelen = mi[0]; + byte mi_type = (byte)(mi[1] & 0xf); + boolean odd_nr_of_digits = ((mi_type & 0x08) != 0); + byte start_nibble = 2 + 1; // 2 to skip the bytelen, 1 to skip the mi_type + byte end_nibble = (byte)(2 + bytelen * 2 - (odd_nr_of_digits ? 0 : 1)); + byte str[] = new byte[end_nibble - start_nibble]; + bcd2str(str, (byte)0, (byte)str.length, mi, start_nibble, end_nibble, true); + return str; + } + + public static byte char2bcd(byte c) + { + if (c >= '0' && c <= '9') + return (byte)(c - '0'); + else if (c >= 'A' && c <= 'F') + return (byte)(0xa + (c - 'A')); + else if (c >= 'a' && c <= 'f') + return (byte)(0xa + (c - 'a')); + else + return 0; + } + + public static byte[] str2mi(byte str[], byte mi_type, byte min_buflen) + { + boolean odd_digits = ((str.length & 1) != 0); + /* 1 nibble of mi_type. + * str.length nibbles of MI BCD. + */ + byte mi_nibbles = (byte)(1 + str.length); + byte mi_bytes = (byte)(mi_nibbles / 2 + ((mi_nibbles & 1) != 0? 1 : 0)); + /* 1 byte of total MI length in bytes, plus the MI nibbles */ + byte buflen = (byte)(1 + mi_bytes); + /* Fill up with 0xff to the requested buffer size */ + if (buflen < min_buflen) + buflen = min_buflen; + byte buf[] = new byte[buflen]; + + for (byte i = 0; i < buf.length; i++) + buf[i] = (byte)0xff; + + /* 1 byte of following MI length in bytes */ + buf[0] = mi_bytes; + + /* first MI byte: low nibble has the MI type and odd/even indicator bit, + * high nibble has the first BCD digit. + */ + mi_type = (byte)(mi_type & 0x07); + if (odd_digits) + mi_type |= 0x08; + buf[1] = (byte)((char2bcd(str[0]) << 4) + mi_type); + + /* fill in the remaining MI nibbles */ + byte str_i = 1; + for (byte mi_i = 1; mi_i < mi_bytes; mi_i++) { + byte data = char2bcd(str[str_i++]); + if (str_i < str.length) + data |= char2bcd(str[str_i++]) << 4; + else + data |= 0xf0; + buf[1 + mi_i] = data; + } + return buf; + } +} diff --git a/imsi-change/src/org/osmocom/IMSIChange/Test.java b/imsi-change/src/org/osmocom/IMSIChange/Test.java new file mode 100644 index 0000000..89905b8 --- /dev/null +++ b/imsi-change/src/org/osmocom/IMSIChange/Test.java @@ -0,0 +1,75 @@ +/* Copyright 2020 sysmocom s.f.m.c. GmbH + * SPDX-License-Identifier: Apache-2.0 */ +package org.osmocom.IMSIChange; +import org.osmocom.IMSIChange.*; + +public class Test { + private static byte nibble2hex(byte nibble) + { + nibble = (byte)(nibble & 0xf); + if (nibble < 0xa) + return (byte)('0' + nibble); + else + return (byte)('a' + nibble - 0xa); + } + + private static byte[] hexdump(byte data[]) + { + byte res[] = new byte[(byte)(data.length*2)]; + for (byte i = 0; i < data.length; i++) { + res[(byte)(i*2)] = nibble2hex((byte)(data[i] >> 4)); + res[(byte)(i*2 + 1)] = nibble2hex(data[i]); + } + return res; + } + + private static String hexdumpStr(byte data[]) + { + return new String(hexdump(data)); + } + + private static final String[] imsis = { + "123456", + "1234567", + "12345678", + "123456789", + "1234567890", + "12345678901", + "123456789012", + "1234567890123", + "12345678901234", + "123456789012345", + "1234567890123456", + }; + + private static void test_str2mi2str() + { + for (int i = 0; i < imsis.length; i++) { + byte str[] = imsis[i].getBytes(); + byte mi[] = MobileIdentity.str2mi(str, MobileIdentity.MI_IMSI, (byte)9); + byte str_from_mi[] = MobileIdentity.mi2str(mi); + System.out.print("IMSI " + new String(str) + " --> MI " + hexdumpStr(mi) + " --> IMSI " + + new String(str_from_mi)); + if (Bytes.equals(str, str_from_mi)) + System.out.println(" (ok)"); + else + System.out.println(" ERROR!"); + } + } + + private static void test_toStr() + { + byte nr = -128; + while (true) { + System.out.println("" + nr + " = '" + new String(Bytes.toStr(nr)) + "'"); + if (nr == 127) + break; + nr++; + } + } + + public static void main(String args[]){ + test_str2mi2str(); + test_toStr(); + } +}