美文网首页
安卓设备标识

安卓设备标识

作者: 八两技术 | 来源:发表于2021-12-17 00:39 被阅读0次

    鉴于国家对设备标识相关越来越规范,以及Android 10以及更高版本的系统限制。

    对于游戏发行SDK,特别是广告分发相关的SDK开发带来了一定的难度,八两也对 Android 设备标识相关的参数进行了一些整理,标题说是分析,其实没什么技术含量,但是比较全。

    至于如何应对越来越严苛的系统限制, 后续八两有时间了另开篇幅唠唠

    Android ID:

    标识说明: Android ID 是随着安卓设备出厂生成的唯一设备ID

    稳定性: 可以认为在设备生命周期内不会改变,除非恢复出厂设置或者用户刷ROOM.且几乎所有安卓设备都可稳定获取.

    唯一性: 每台设备出厂时随机生成,有 16 位随机字符串,每位取值10位数字加26位小写字母,理论上有 36的16次方种组合,具有较好的唯一性

    额外权限: 无需申请任何权限

    版本变化: Android 8 以及以上版本,会根据当前设备用户,APK安卓签名,设备,确定作用域,任一不同作用域均会获取到不同的Android ID. 即 Android ID 不再能标识唯一设备

    Java实现:

    public static String getAndroidId(Context context) {
        String id = Settings.System.getString(context.getContentResolver(), Settings.Secure.ANDROID_ID);
        return TextUtils.isEmpty(id) ? "" : id;
    }
    

    OAID

    标识说明: 移动安全联盟倡导的通用ID, 可以被用户手动重置,无需任何权限即可获取。美中不足是OAID是异步获取。

    稳定性: 在设备厂商按照标准的情况下,OAID 具有很好的稳定性,但非正常情况下,如虚拟机等,可伪造篡改OAID

    唯一性: OAID 由 32 位字符组成,不易出现重复

    额外权限: 无,但在部分设备上,OAID是异步获取。

    版本变化: 根据厂商不同而不同,通常 Android 10.0 以后,才能获取OAID. 且并非所有厂商均支持 OAID 获取

    获取方式: 需申请集成移动安全联盟的SDK,或根据设备自己实现,不赘述

    IMEI

    标识说明: International Mobile Equipment Identity, 国际移动设备识别码,共有15位数字,前6位(TAC)是型號核准號碼,代表手機類型。接著2位(FAC)是最後裝配號,代表產地。後6位(SNR)是串號,代表生產順序號。最後1位(SP)一般為0,是檢驗碼,備用

    稳定性: 在设备厂商按照标准的情况下,IMEI 具有很好的稳定性,但非正常情况下,如虚拟机等,可伪造篡改IMEI

    唯一性: IMEI 由 15 位数字组成,不易出现重复

    额外权限: 需要申明 android.permission.READ_PHONE_STATE 权限

    版本变化: Android 6.0 以后, android.permission.READ_PHONE_STATE 需要用户手动授权, Android 10 以后不再支持获取 IMEI, 尝试获取均返回空

    Java实现:

    @SuppressLint("MissingPermission")
    public static String getIMEI(Context context) {
        try {
            if (PermissionUtils.hasAndroidPermission(context, Manifest.permission.READ_PHONE_STATE)) {
                TelephonyManager telephonyManager = (TelephonyManager) context.getSystemService(Activity.TELEPHONY_SERVICE);
                if (telephonyManager != null) return TextUtils.isEmpty(telephonyManager.getDeviceId()) ? "" : telephonyManager.getDeviceId();
            } else {
                Logger.warning(Logger.TOOLS_TAG, "Can not get IMEI since no READ_PHONE_STATE permission");
            }
        } catch (Exception e) {
            Logger.warning(Logger.TOOLS_TAG, "Can not get IMEI because of Exception, this is expected in Android 10 and above ", e);
        }
        return "";
    }
    

    Mac 地址

    标识说明: 设备唯一网络标识,也叫网卡地址,长度 48 位,通常表示成类似 00-16-EA-AE-3C-40

    稳定性: 因为是唯一网络标识,可基本认为稳定,不过若设备从未联网,以及部分情况下产商的限制,可能出现获取不到或者获取到某固定值的情况

    唯一性: 48 位二进制数,理论上有 2 ^ 48(281474976710656) 次方种可能

    额外权限: 需要声明 android.permission.ACCESS_WIFI_STATEandroid.permission.INTERNET 权限

    版本变化: Android 6.0 以上获取方式略有修改, Android 11 以及以上,每次设备联网都会生成新的随机 Mac 地址,故其不再具有唯一性

    Java实现:

    public static String getMacAddress(Context context) {
        String mac = "";
        if (Util.getCurrentAndroidVersion() >= Build.VERSION_CODES.M) {
            try {
                Enumeration<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces();
                if(interfaces == null) return mac;
                List<NetworkInterface> all = Collections.list(interfaces);
                for (NetworkInterface nif : all) {
                    if (nif.getName().equalsIgnoreCase("wlan0")) {
                        byte[] macBytes = nif.getHardwareAddress();
                        if (macBytes == null) {
                            break;
                        }
                        StringBuilder sb = new StringBuilder();
                        for (byte b : macBytes) {
                            sb.append(String.format("%02X:", b));
                        }
                        if (sb.length() > 0) sb.deleteCharAt(sb.length() - 1);
                        mac = sb.toString();
                        break;
                    }
                }
            } catch (SocketException e) {
                Logger.warning(Logger.ERR_TAG, "Get Mac address via NetworkInterface exception", e);
            }
        } else {
            WifiManager wifiManager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
            if(wifiManager != null) {
                WifiInfo wifiInfo = wifiManager.getConnectionInfo();
                if(wifiInfo != null) mac = wifiInfo.getMacAddress();
            }
        }
        return (TextUtils.isEmpty(mac) || "02:00:00:00:00:00".equals(mac)) ? "" : mac;
    }
    

    Android 序列号

    标识说明: Android 序列号,Android 硬件序列号,与硬件信息强相关

    稳定性: Android 序列号与硬件信息强相关,硬件设备无修改则此值不会发生变化

    唯一性: 由于各硬件厂商有自己的序列号生成规则,故不同厂商之间,有序列号重复的可能,但重复可能性很低。序列号本身有 16 位,每位是26个字母中的随机值

    额外权限: Android 7.1 以前无权限,7.0以后需要申明 android.permission.READ_PHONE_STATE 权限

    版本变化: Android 7.1 以前可直接通过 Build.SERIAL 获取, Android 10 以前可通过申请 android.permission.READ_PHONE_STATE 权限以反射方式获取, Android 10 以后需要 android.Manifest.permission.READ_PRIVILEGED_PHONE_STATE 权限,理论上只有系统应用或者设备厂商才能获取

    Java实现:

    /**
     * Trying to get SERIAL number in different way.<br>
     * As test, this method work until <b>Android 9 and below</b>.
     * @return Serial Number , return "" if unable to get Serial Number or Serial Number is "unknown"
     */
    public static String getSerial() {
        if(!TextUtils.isEmpty(Build.SERIAL) && !"unknown".equalsIgnoreCase(Build.SERIAL)) {
            return Build.SERIAL;
        }
        try {
            Class<?> claz = Class.forName("android.os.Build");
            Method method = claz.getMethod("getSerial");
            String serial = (String) method.invoke(null);
            return TextUtils.isEmpty(serial) || "unknown".equalsIgnoreCase(serial) ? "" : serial;
        } catch (Exception e) {
            Logger.warning(Logger.EXP_TAG, "Get serial Reflection exception", e);
            return "";
        }
    }
    

    出厂时间

    标识说明: 记录在 Android 设备上的出厂时间信息

    稳定性: 由于每台 Android 设备均有固定的出厂时间,故此值通常情况下不会变化,除非以下情况:恢复出厂设置,刷 room。而实际由于各个厂商的实现不同,部分情况会无法获取出厂时间。

    唯一性: 出厂时间理论上可精确到毫秒,实际中大多数只精确到秒,然而每秒出厂的手机有限,依旧有较好的唯一性,单独使用不建议,配合其他信息使用也可有较好的唯一性

    额外权限: 无需申请额外权限

    版本变化: Android 10 前是否能获取到与设备型号、有较大的关系,Android 11 以后由于权限限制,目前实现也无法获取,需调研其他获取方式

    Java实现:

    /**
     * Trying to get device first factory time, this "factory time" will not change until factory reset.
     * @return Factory Access Time with digital, record in /cache if could get it, otherwise return "" if any exception happen.
     * e.g: 202007141154500800
     */
    public static String getFactoryTime() {
        StringBuilder result = new StringBuilder();
        ProcessBuilder cmd;
        try {
            String[] args = {"/system/bin/stat", "/cache"};
            cmd = new ProcessBuilder(args);
            Process process = cmd.start();
            InputStream in = process.getInputStream();
            byte[] re = new byte[24];
            while (in.read(re) != -1) {
                result.append(new String(re));
            }
            in.close();
        } catch (Exception e) {
            Logger.warning("Exception happen to get factory time ", e);
        }
        String[] lines = result.toString().split("\n");
        StringBuilder time = new StringBuilder();
        for (String line: lines) {
            if(line.contains("Access:") && !line.contains("Uid:")) {
                for (char s: line.toCharArray()) {
                    if(Character.isDigit(s)){
                        time.append(s);
                    }
                }
            }
        }
        return time.toString().equals("19700101080000000000000") ? "" : time.toString().replace("000000000", "");
    }
    

    Media Drm ID

    标识说明: MediaDrm 是数字音频版权框架,其被安卓在安卓架构中原生支持。Media Drm ID 是数字音频用于追踪,保护版权所需的唯一设备ID.

    稳定性: 其在框架中被原生支持,且用来追踪数字音频版权,故具有较好的稳定性,通常情况不会变化,且由框架加密,不易伪造和篡改。目前尚未测试刷机,恢复出厂设置等场景

    唯一性: 根据不同数字版权提供商算法,可获取不同的 MediaDrm ID, 但都基于 64 位 16 进制数字计算而来,故有 16^64次方种组合,不易重复

    额外权限:无

    版本变化:Android 4.3 以上才可正常获取,Android 8 以及以后,Media Drm ID 会根据底层包名不同而不同,与安卓签名无关

    Java实现:
    加密 WIDEVINE 获取方式

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
    public static String getWidevineId() {
        String sRet = "";
    
        UUID WIDEVINE_UUID = new UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L);
        MediaDrm mediaDrm = null;
    
        try {
            mediaDrm = new MediaDrm(WIDEVINE_UUID);
            byte[] widevineId = mediaDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
    
            MessageDigest md = MessageDigest.getInstance("SHA-256");
            md.update(widevineId);
    
            sRet = bytesToHex(md.digest()); //we convert byte[] to hex for our purposes
        } catch (Exception e) {
            //WIDEVINE is not available
            Logger.error("getWidevineSN.WIDEVINE is not available", e);
        } finally {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
                if(null!=mediaDrm) {
                    mediaDrm.close();
                }
            } else {
                if(null!=mediaDrm) {
                    mediaDrm.release();
                }
            }
        }
    
        return sRet;
    }
    
    private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray();
    public static String bytesToHex(byte[] bytes) {
        char[] hexChars = new char[bytes.length * 2];
        for (int j = 0; j < bytes.length; j++) {
            int v = bytes[j] & 0xFF;
            hexChars[j * 2] = HEX_ARRAY[v >>> 4];
            hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F];
        }
        return new String(hexChars);
    }
    

    WIDEVINE ID 获取方式,返回 bytes 数组

    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
    public static String getWidevineBytes() {
        UUID wideVineUuid = new UUID(-0x121074568629b532L, -0x5c37d8232ae2de13L);
        try {
            MediaDrm wvDrm = new MediaDrm(wideVineUuid);
            byte[] wideVineId = wvDrm.getPropertyByteArray(MediaDrm.PROPERTY_DEVICE_UNIQUE_ID);
            return Arrays.toString(wideVineId);
        } catch (Exception e) {
            // Inspect exception
            return null;
        }
        // Close resources with close() or release() depending on platform API
        // Use ARM on Android P platform or higher, where MediaDrm has the close() method
    }
    

    IP

    标识说明: 设备IP地址

    稳定性: 较无稳定性,随着用户网络条件与物理位置的不同,IP地址也会变化

    唯一性: 相同地区的 IP 可能相同

    额外权限: 需要声明 android.permission.ACCESS_WIFI_STATEandroid.permission.INTERNET 权限

    版本变化: 无

    Java实现:

    /**
     * <p>
     * As it said, simply get IP address,
     * <ol>
     * First it will try to get IP address from {@link NetworkInterface}
     * </ol>
     * <ol>
     * Then it will try to get IP address from {@link android.net.wifi.WifiInfo#getIpAddress()}
     * </ol>
     * <ol>
     * Return empty string if any exception happen.
     * </ol>
     * </p>
     *
     * @param context activity context
     * @return IP Address, support both IPV4 & IPV6 address, support mobile network & WIFI
     * "" if exception happen
     */
    public static String getIpAddress(Context context) {
        Enumeration<NetworkInterface> en = null;
        try {
            en = NetworkInterface.getNetworkInterfaces();
        } catch (SocketException e) {
            Logger.warning(Logger.ERR_TAG, "Create NetworkInterface occur Exception", e);
        }
        if (en != null) {
            while (en.hasMoreElements()) {
                NetworkInterface intf = en.nextElement();
                Enumeration<InetAddress> enumIpAddr = intf.getInetAddresses();
                while (enumIpAddr.hasMoreElements()) {
                    InetAddress inetAddress = enumIpAddr.nextElement();
                    if (!inetAddress.isLoopbackAddress() && !inetAddress.isAnyLocalAddress() && !inetAddress.isLinkLocalAddress()) {
                        return inetAddress.getHostAddress().toUpperCase(Locale.US);
                    }
                }
            }
        }
    
        // Can not get IP address in network interface, use wifi ip address
        try {
            WifiManager manager = (WifiManager) context.getApplicationContext().getSystemService(Context.WIFI_SERVICE);
            if(manager == null) return "";
            int ipAddress = manager.getConnectionInfo().getIpAddress();
            return String.format(Locale.getDefault(), "%d.%d.%d.%d", (ipAddress & 0xff), (ipAddress >> 8 & 0xff), (ipAddress >> 16 & 0xff), (ipAddress >> 24 & 0xff));
        } catch (Exception e) {
            // Exception could happen if wifi doesn't connect
            Logger.warning(Logger.ERR_TAG, "Get IP address from wifi manager exception ", e);
            return "";
        }
    
    }
    

    UUID

    标识说明: 随机序列码

    稳定性: 无任何稳定性,每次均生成不同值

    唯一性: UUID由32位 16 进制字符构成,每个字符可有16种可能,有较好唯一性

    额外权限: 无

    版本变化: 无

    Java实现:

    public static String getUUID() {
        return UUID.randomUUID().toString();
    }
    

    其他调研参数

    以下参数,无特别说明,均无版本变化,无需申请权限,但有较高局限性,绝大多数均无法判别唯一设备,但可加入唯一设备标识的计算

    参数获取基础代码:

    private static String getProperty(String prop) {
        try {
            Class<?> cls = Class.forName("android.os.SystemProperties");
            Method get = cls.getDeclaredMethod("get", new Class<?>[]{String.class, String.class});
            String value = (String) get.invoke(null, new Object[]{prop, ""});
            return TextUtils.isEmpty(value) ? "" : value;
        } catch (Exception e) {
            Logger.warning(Logger.EXP_TAG, "Get property %s Reflection exception", prop, e);
            return "";
        }
    }
    

    开机时间

    标识说明: 系统运行到现在的开机时间

    稳定性: 与用户习惯强相关,取决于用户开关机频率

    唯一性: Unix 时间戳,精确到毫秒,有一定的唯一性

    Java实现:

    public static String getFirstBoot() {
        return getProperty("ro.runtime.firstboot");
    }
    

    品牌

    标识说明: 硬件手机品牌

    稳定性: 理论上一定稳定,均可获得定值

    唯一性: 可筛选区分不同品牌的设备,然同一品牌设备众多,故几乎无唯一性

    Java实现:

    public static String getDeviceBrand() {
        return TextUtils.isEmpty(Build.BRAND) ? "unknown" : Build.BRAND;
    }
    

    型号

    标识说明: 与品牌略有不同,是其品牌下的细分型号。

    public static String getDeviceModel() {
        return TextUtils.isEmpty(Build.MODEL) ? "unknown" : Build.MODEL;
    }
    

    制造厂商

    标识说明: 与品牌类似,返回设备具体制造厂商,通常此值与品牌相同,若出现手机代工等情况,其值可能不同

    public static String getDeviceManufacture() {
        return TextUtils.isEmpty(Build.MANUFACTURER) ? "unknown " : Build.MANUFACTURER;
    }
    

    相关文章

      网友评论

          本文标题:安卓设备标识

          本文链接:https://www.haomeiwen.com/subject/jevbfrtx.html