安卓设备标识

鉴于国家对设备标识相关越来越规范,以及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 interfaces = NetworkInterface.getNetworkInterfaces();
            if(interfaces == null) return mac;
            List 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.
* As test, this method work until Android 9 and below. * @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实现:

/**
 * 

* As it said, simply get IP address, *

    * First it will try to get IP address from {@link NetworkInterface} *
*
    * Then it will try to get IP address from {@link android.net.wifi.WifiInfo#getIpAddress()} *
*
    * Return empty string if any exception happen. *
*

* * @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 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 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;
}

你可能感兴趣的:(安卓设备标识)