TOTP 动态码算法

2018/01/20 技术

动态码算法

两次加密一次混淆,破解难度高,在加入时间参数和随机位的情况下,只会增加 5 个长度位

算法定义

TOTP  SHUFFLE(M, skey)
M = MIX(S)
S  SHUFFLE(C, skey)
C = CONCAT(K, T)
K = targetCode
T = (currentUnixTime - epochUnixTime) / stepLen

模型设计

相关函数

  • CONCAT 拼接函数,将静态码和动态码进行拼接
  • MIX 混淆函数,将静态码按随机数动态变化,但是能够进行反解析
  • SHUFFLE 对称加密函数,安全的根本,只要密钥不丢失,外界无法破解

相关参数

  • currentUnixTime 当前系统的 Unix 时间
  • epochUnixTime 双方约定的时间原点,用来计算时间差值,有利于降低动态位的长度,也算是一层简单的加密
  • stepLen 时间步长,单位为秒,这个值越大的容错度就越高,相反越小超安全
  • skey 双方约定的密钥,在默认情况下由平台帮忙生成一个指定字符集的密码本,双方必须保证一致,这是安全的根本,否由无法正确的加解密

BASE62 算法应用:四则运算、压缩

  • 码长度最大可以压缩一半的空间

    比如说数字 141802 采用 BASE62 算法转换后的值为 at8,节省了一半的空间

  • 62 进制奇偶加减运算将动态码变的更加分散

算法执行结果样本

我们以原码『ABC123』为例,时间步长为 10 秒,动态码前缀为「D」,生成 8 组的结果如下

DZCkKF5qCh3c
D2ch0xHBcwsy
D8cu07HTc3sF
Dpcd02HIc6sZ
D3mknF9qmhfc
DIjkZFhqjhUb
Dic70lHScksQ
DXzkNFjqzhSb

从结果可以看出来按这种算法,生成出来的结果是完全随机加密的,安全性较高。

使用 DEMO

@Test
public void testTotpTable() throws ParseException, InterruptedException {
    for (int i = 0; i < 128; i++) {
        TOTP totp = new TOTP();
        totp.setStepLen(10);
        //totp.setPrefix("D");
        String marshal = totp.marshal("ABC123");
        System.out.println("i = " + i + ", secret = " +  marshal + ", "
                + totp.unmarshal(marshal) + ", validate = " + totp.validate(marshal));
        TimeUnit.SECONDS.sleep(1);
    }
}

代码

BASE62 混淆算法


/**
 * Created by yunchow.zy on 18/1/18.
 */
public class SimpleMixFunction implements MixFunction {

    @Override
    public String mix(String source) {
        Objects.nonNull(source);
        final Character mixFactor62 = BASE62.singleRandom();
        TOTPDebug.info("mixFactor62 = " + mixFactor62);
        final int mixFactor10 = BASE62.parseInt("" + mixFactor62);
        StringBuilder sb = new StringBuilder(source);
        for (int i = mixFactor10 % 2; i < sb.length(); i = i + 2) {
            char c = sb.charAt(i);
            sb.setCharAt(i, BASE62.singleAdd(c, mixFactor62, c));
        }
        return mixFactor62 + sb.toString();
    }

    @Override
    public String unmix(String target) {
        if (target == null || target.length() < 2) {
            throw new IllegalArgumentException("target length illegal");
        }
        StringBuilder sb = new StringBuilder(target);
        final Character mixFactor62 = sb.charAt(0);
        final int mixFactor10 = BASE62.parseInt("" + mixFactor62);
        sb.deleteCharAt(0);
        for (int i = mixFactor10 % 2; i < sb.length(); i = i + 2) {
            char c = sb.charAt(i);
            sb.setCharAt(i, BASE62.singleSubtract(c, mixFactor62, c));
        }
        return sb.toString();
    }
}


密码表加密算法

/**
 * Created by yunchow.zy on 18/1/17.
 */
public final class SecretTable {

    // ==================================================================//
    // ================ 这里的参数请根据自己的情况进行修改 ====================//

    /**
     * 每个客户端的密码表不同,请注意保密
     */
    private String PASSWORD_TABLE_STRING = "Ef7cRHupZLQaF5BGCjKemSDWx0lkM2OPvI6Yishnbgy3t1w9Jq8rTd4UVXANoz";

    // =======================       结束       ==========================//
    // ==================================================================//

    private static final String SOURCE_ALPHABETS = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
    private static final Integer PASSWORD_TABLE_LEN = SOURCE_ALPHABETS.length();
    private static final Map<Character, Integer> SOURCE_TO_TARGET_INDEX_MAPPING = new HashMap<>(PASSWORD_TABLE_LEN);
    private static final Map<Character, Integer> TARGET_TO_SOURCE_INDEX_MAPPING = new HashMap<>(PASSWORD_TABLE_LEN);
    private static final Character[] PASSWORD_INDEX = new Character[PASSWORD_TABLE_LEN];
    private static final Character[] BASIC_SORT_INDEX = new Character[PASSWORD_TABLE_LEN];


    public SecretTable () {
        this(null);
    }

    public SecretTable (String skey) {
        if (skey != null) {
            PASSWORD_TABLE_STRING = skey;
        }
        if (PASSWORD_TABLE_STRING.length() != PASSWORD_TABLE_LEN) {
            throw new IllegalArgumentException("PASSWORD_TABLE_STRING length must be " + PASSWORD_TABLE_LEN);
        }
        char[] chars = PASSWORD_TABLE_STRING.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            SOURCE_TO_TARGET_INDEX_MAPPING.put(chars[i], i);
            PASSWORD_INDEX[i] = chars[i];
        }
        chars = SOURCE_ALPHABETS.toCharArray();
        for (int i = 0; i < chars.length; i++) {
            BASIC_SORT_INDEX[i] = chars[i];
            TARGET_TO_SOURCE_INDEX_MAPPING.put(chars[i], i);
        }
    }

    /**
     * 加密
     * @param source
     * @return
     */
    public String encode(String source) {
        if (source == null) {
            return source;
        }
        List<String> list = new ArrayList<>();
        char[] chars = source.toCharArray();
        for (Character aChar : chars) {
            Integer index = SOURCE_TO_TARGET_INDEX_MAPPING.get(aChar);
            if (index != null) {
                Character targetChar = BASIC_SORT_INDEX[index];
                list.add(String.valueOf(targetChar));
            } else {
                list.add(String.valueOf(aChar));
            }
        }
        return list.stream().collect(Collectors.joining());
    }

    /**
     * 解密
     * @param target
     * @return
     */
    public String decode(String target) {
        if (target == null) {
            return target;
        }
        List<String> list = new ArrayList<>();
        char[] chars = target.toCharArray();
        for (Character aChar : chars) {
            Integer index = TARGET_TO_SOURCE_INDEX_MAPPING.get(aChar);
            if (index != null) {
                Character targetChar = PASSWORD_INDEX[index];
                list.add(String.valueOf(targetChar));
            } else {
                list.add(String.valueOf(aChar));
            }
        }
        return list.stream().collect(Collectors.joining());
    }

    public static void main(String[] args) {
        SecretTable st = new SecretTable();
        System.out.println("1->" + st.encode("ABCDEF123=阿里巴巴@="));
        System.out.println("2->" + st.encode(st.encode("ABCDEF123")));
        System.out.println("3->" + st.encode(st.encode(st.encode("ABCDEF123"))));
        System.out.println("4->" + st.encode(st.encode(st.encode(st.encode("ABCDEF123")))));
        System.out.println("5->" + st.encode(st.encode(st.encode(st.encode(st.encode("ABCDEF123"))))));
        System.out.println("1->" + st.decode("wEGM0CjTh+阿里&"));
        System.out.println("2->" + st.decode(st.decode("k0FSPGHqc")));
        System.out.println("3->" + st.decode(st.decode(st.decode("RPCLVF5n3"))));
        System.out.println("4->" + st.decode(st.decode(st.decode(st.decode("4VG9uCDdh")))));
        System.out.println("5->" + st.decode(st.decode(st.decode(st.decode(st.decode("suFl6GMrc"))))));
    }

    /**
     * 随机生成一个新的密码表
     * @return
     */
    public static String makePasswordTable() {
        char[] chars = SOURCE_ALPHABETS.toCharArray();
        List<String> list = new ArrayList<>();
        for (char aChar : chars) {
            list.add(String.valueOf(aChar));
        }
        Collections.shuffle(list);
        return list.stream().collect(Collectors.joining());
    }
}

BASE62 算法

/**
 * Created by yunchow.zy on 18/1/17.
 */
public final class BASE62 {
    private static char[] BASE_TABLE = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz".toCharArray();
    private static final Integer SCALE = BASE_TABLE.length;
    private static final Map<Character, Integer> INDEX_MAP = new HashMap<Character, Integer>() {
        private static final long serialVersionUID = 3807502752985392746L;
        {
            for (int i = 0; i < BASE_TABLE.length; i++) {
                put(BASE_TABLE[i], i);
            }
        }
    };

    /**
     * 62进制相减运算
     * @param c1
     * @param c2
     * @param defaultC 计算失败返回的值
     * @return
     */
    public static Character singleSubtract(Character c1, Character c2, Character defaultC) {
        Integer i1 = INDEX_MAP.get(c1);
        Integer i2 = INDEX_MAP.get(c2);
        if (i1 == null || i2 == null) {
            return defaultC;
        }
        i1 = i1 < i2 ? i1 += SCALE : i1;
        int k = i1 - i2;
        if (SCALE <= k || k < 0) {
            return defaultC;
        }
        return BASE_TABLE[k];
    }

    /**
     * 62进制相加运算
     * @param c1
     * @param c2
     * @param defaultC 计算失败返回的值
     * @return
     */
    public static Character singleAdd(Character c1, Character c2, Character defaultC) {
        Integer i1 = INDEX_MAP.get(c1);
        Integer i2 = INDEX_MAP.get(c2);
        if (i1 == null || i2 == null) {
            return defaultC;
        }
        int k = i1 + i2;
        k = k >= SCALE ? k - SCALE : k;
        if (SCALE <= k || k < 0) {
            return defaultC;
        }
        return BASE_TABLE[k];
    }

    /**
     * 随机得到一个 【1 ~ 61】 之间的一个数,如果得到的是0,重新再计算一遍
     * @return
     */
    public static Character singleRandom() {
        int index = new SecureRandom().nextInt(SCALE);
        if (index == 0) {
            return singleRandom();
        }
        return BASE_TABLE[index];
    }

    /**
     * 将10进制的数转换成62进制数
     * @param num
     * @return
     */
    public static String valueOf(long num) {
        StringBuilder sb = new StringBuilder();
        long remainder = 0;
        do {
            remainder = num % SCALE;
            sb.append(BASE_TABLE[(int) remainder]);
            num = num / SCALE;
        }
        while (num > SCALE - 1);
        sb.append(BASE_TABLE[(int) num]);
        List<String> list = new ArrayList<>();
        for (int i = 0; i < sb.length(); i++) {
            list.add("" + sb.charAt(i));
        }
        Collections.reverse(list);
        return list.stream().collect(Collectors.joining());
    }

    public static int parseInt(String target) {
        return (int) parseLong(target);
    }

    /**
     * 将62进制数转换为10进制数
     * @param target
     * @return
     */
    public static long parseLong(String target) {
        long num = 0;
        int index = 0;
        for(int i = 0; i < target.length(); i++) {
            index = INDEX_MAP.get(target.charAt(i));
            num += (long)(index * (Math.pow(SCALE, target.length() - i - 1)));
        }
        return num;
    }

}

TOTP

package com.totpdemo.test.totp;

/**
 *
 * TOTP 算法定义如下
 *
 * TOTP = SHUFFLE(MIX(SHUFFLE(CONCAT(K, T), skey)), skey)
 *
 * <blockquote><pre>
 *      TOTP = SHUFFLE(M, skey)
 *      M = MIX(S)
 *      S = SHUFFLE(C, skey)
 *      C = CONCAT(K, T)
 *      K = targetCode
 *      T = (currentUnixTime - epochUnixTime) / stepLen
 * </pre></blockquote>
 *
 * 一个最基本的用法示例如下,其他用法请参考方法注释
 *
 * <blockquote><pre>
 *      TOTP totp = new TOTP();
 *      totp.setStepLen(180);
 *      totp.setPrefix("D");
 *      totp.marshal("ABC123abc321");
 *      totp.unmarshal("DShkBFDqhEPhhqDhpr");
 * </pre></blockquote>
 *
 * 例如静态码 ABC123 的时间动态码的输出如下,时间步长为 10 秒
 *
 * <blockquote><pre>
 * DZCkKF5qCh3c
 * D2ch0xHBcwsy
 * D8cu07HTc3sF
 * Dpcd02HIc6sZ
 * D3mknF9qmhfc
 * DIjkZFhqjhUb
 * Dic70lHScksQ
 * DXzkNFjqzhSb
 * </pre></blockquote>
 *
 * Created by yunchow.zy on 18/1/16.
 */
public final class TOTP {

    /**
     * 默认时间起点为:2018-01-01 08:00:00
     */
    public static final long DEFAULT_EPOCH_TIME = 1514764800;

    /**
     * 当前时间
     */
    private long currentUnixTime = System.currentTimeMillis() / 1000;

    /**
     * 时间起点
     */
    private long epochUnixTime = DEFAULT_EPOCH_TIME;

    /**
     * 秒单位步长
     */
    private int stepLen = 120;

    /**
     * 密钥
     */
    private String skey = "Ef7cRHupZLQaF5BGCjKemSDWx0lkM2OPvI6Yishnbgy3t1w9Jq8rTd4UVXANoz";

    /**
     * 容错窗口大小
     */
    private Integer faultWindowSize = 1;

    /**
     * 前缀
     */
    private String prefix = "";

    /**
     * 拼接函数,客户端可以实现自已的函数
     */
    private ConcatFunction concatFunction = new SimpleConcatFunction();

    /**
     * 加密函数,支持客户端自定义扩展
     */
    private Shuffle shuffle = new SecretTableShuffle();

    /**
     * 混淆函数,将原有的静态码随机变换,显的没有规律
     */
    private MixFunction mixFunction = new SimpleMixFunction();

    /**
     * 根据指定静态码得到一个 TOTP 动态编码
     * @param source 静态源码
     * @return
     */
    public String marshal(String source) {
        TOTPDebug.info("currentUnixTime = " + currentUnixTime);
        TOTPDebug.info("epochUnixTime = " + epochUnixTime);
        TOTPDebug.info("stepLen = " + stepLen);
        DynamicCode dynamicCode = new DynamicCode();
        dynamicCode.setStaticPart(source);
        dynamicCode.setDynamicPart("" + currentDynamicTime());
        String join = concatFunction.join(dynamicCode);
        TOTPDebug.info("concatFunctionJoin = " + join);
        String enshuff = shuffle.enshuff(join, skey); // 一次加密
        TOTPDebug.info("enshuffFirst = " + enshuff);
        String mix = mixFunction.mix(enshuff);
        TOTPDebug.info("mix = " + mix);
        enshuff = shuffle.enshuff(mix, skey); // 二次加密
        TOTPDebug.info("enshuffSecond = " + enshuff);
        return prefix + enshuff;
    }

    /**
     * 当前动态时间
     * @return
     */
    public Long currentDynamicTime() {
        Long currentDynamicTime = (currentUnixTime - epochUnixTime) / stepLen;
        TOTPDebug.info("currentDynamicTime = " + currentDynamicTime);
        return currentDynamicTime;
    }

    /**
     * 验证指定的动态码是否有效
     * @param target 动态码
     * @return
     */
    public boolean validate(String target) {
        DynamicCode unmarshal = unmarshal(target);
        return validate(unmarshal);
    }

    /**
     * 验证指定的动态码是否有效
     * @param dynamicCode 动态码对象
     * @return
     */
    public boolean validate(DynamicCode dynamicCode) {
        Integer dynamicPart = Integer.valueOf(dynamicCode.getDynamicPart());
        return validate(dynamicPart);
    }

    /**
     * 验证指定的动态码是否有效
     * @param dynamicPart 从动态码里面解析出来的时间串
     * @return
     */
    public boolean validate(Integer dynamicPart) {
        return Math.abs(currentDynamicTime() - dynamicPart) <= faultWindowSize;
    }

    /**
     * 解析一个已被加密的动态TOTP码
     * @param target
     * @param concatFunction
     * @param shuffle
     * @return
     */
    public DynamicCode unmarshal(String target) {
        if (target == null) {
            return null;
        }
        if (prefix != null && prefix.length() > 0 && target.startsWith(prefix)) {
            target = target.substring(prefix.length());
        }
        String deshuff = shuffle.deshuff(target, skey); // 一次解密
        String unmix = mixFunction.unmix(deshuff);
        deshuff = shuffle.deshuff(unmix, skey); // 二次解密
        DynamicCode dynamicCode = concatFunction.split(deshuff);
        return dynamicCode;
    }
}


打赏作者一杯

渣渣程序狗

文章内容导航