iT邦幫忙

2017 iT 邦幫忙鐵人賽
DAY 2
0
Security

安全地寫 Java 的「基本功」系列 第 2

安全地寫 Java 的 「基本功」- Day 1

前言

孔子在周易繫辭篇,說到一句非常有名的話,「是故形而上者謂之道,形而下者謂之器」,形而上的是事物運行的原理,形而下的是事物運行的方法論。這是一個很偉大的見解,但當然,我們不是談哲學。

資訊安全是一門很大的學門,從 ISO 的標準來看,它是一種管理系統,需要一整個企業體投入配合,就像品質管理一樣。從 ISC^2 的觀點來看,他們不談那麼大的範圍,而是從軟體的生命週期著手,切分成數個領域來討論,有種分而治之的概念。然而再到了 EC-Concil,則形成了各個語言、技術領域的真槍實戰內容。

但其實我並非專精這門的人,在 30 天的日子裡,我們也很難談完百家爭鳴的各個概念與工法。然而,在那麼大的領域裡,要達成 CIA 的特性,總會有一些方法。最重要的,就是認證(Authentication)、授權(Authorization)、稽核究責(Auditing/Accountability)。

認證(Authentication)

認證是最常見的一種安全控管機制,當各位透過帳號密碼登入 IT 邦幫忙或 Facebook、Gmail 時,其實就是在經過認證的機制了。但認證更不只如此。例如你上 Amazon 買電子書時,它需要認出你是那個正確的會員,同樣的,你也必須認出它是真正的 Amazon。因此我們使用 SSL/TLS 機制來進行對 Serevr 端的認證。

除了這類作法,其實,當你回到家,用鑰匙打開門時,就是一種認證機制了。根據你所知道的帳密,你可以登入到這個平台來,根據你所擁有的鑰匙,你可以回到你的家。除此之外,你還可以根據你的指紋,解鎖手上的 iPhone,這就是根據生物特徵的認證機制。

授權(Authorization)

授權常常是伴隨著成功的認證機制而來的,當然,你若認證不成功,小則乾瞪眼,大則被抓走。認證完了之後,就是給予適當權限的時候了。例如你在 Facebook 上發文,可以決定這是要給路人看的,還是要給朋友看的,又或者只有某些密友能看。其實隱私的管理,也是一種廣義的授權。

授權常用的方法,連我自己都常在用的,是矩陣式權限配置搭配上 rule based 的權限設定。程式在實作上時,或許免不了使用到 SpringSecurity 這個大框架,但我們還是需要知道背後的原理。

稽核究責(Auditing/Accountability)

稽核的動作,其實像對帳、查帳一樣,例如到這個月底發現口袋空了,只好把這個月的發票拿出來,調查看看到底是那一次失心瘋買了不該買的東西。這就是稽核。但巧婦難為無米之炊,大會計師難為無發票稽核,沒有發票,沒有軌跡,就沒辦法稽核。

有人在管 Linux/BSD Server 的,可能常常要三不五時去 /var/log/ 下找 log,系統的 log 就是最重要的稽核軌跡。同樣的,程式的 log 就是你的程式背著你偷偷幹了哪些事的明證。

起手式

不知道各位平常怎樣取自己的密碼,第一個程式,我們先別談太高深的密碼學原理,因為我也不會。也別談太高層次的設計理論,有興趣歡迎來我們公司。我們談談怎樣取密碼。

Test First

我們先來寫第一個測試,這個測試很簡單,呼叫 PasswordGenerator 來為我們產生一個密碼:

package javaxx.security.password;

import org.junit.Assert;
import org.junit.Test;

/**
 * PasswordGeneratorTest
 * cchuang, 2016/12/2.
 */
public class PasswordGeneratorTest {
    private static final String TEST_TEXT = "guessOrGiveUp";

    @Test
    public void testPasswordGenerator() throws Exception {
        PasswordGenerator pwdGen = PasswordGenerator.getInstance();

        String password = pwdGen.generatePasswordFromText(TEST_TEXT);

        Assert.assertNotEquals(TEST_TEXT, password);

        System.out.println("generated password: " + password);
    }
}

原理

既然是密碼產生器,就是產生讓人猜不到的密碼。然而,CMU 先前有研究過,定期更換密碼的政策,容易讓使用者傾向取更簡單的密碼,反而更容易造成資訊安全風險。那麼,我們若要產生足夠強度的密碼,又要好記,有什麼辦法呢?

我們可以使用平常習慣的一串字,也許是自己的 ID,或用一串你認為比較好記的字。然後,我們可以進行轉義(Transition),或代換(Substitution)。

例如,我的 ID cchuang0425,可以進行以下處理:

  • c -> c 或 C
  • h -> h 或 H
  • u -> u 或 U
  • a -> a 或 A
  • n -> n 或 n
  • g -> g 或 G
  • 0 -> 0 或 ) 或 o 或 O
  • 4 -> 4 或 A 或 $
  • 2 -> 2 或 @
  • 5 -> 5 或 %

當然,你可以找到更多規則,建立自己的密碼轉換規則。

主要程式

以下是一個簡單的密碼轉換程式:

package javaxx.security.password;

import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;

/**
 * PasswordGenerator
 * cchuang, 2016/12/2.
 */
public class PasswordGenerator {
    private static PasswordGenerator instance;

    public static PasswordGenerator getInstance() {
        if (null == instance) {
            instance = new PasswordGenerator();
        }

        return instance;
    }

    private Map<Character, String> charMap;

    private PasswordGenerator() {
        initCharMap();
    }

    private void initCharMap() {
        charMap = new HashMap<Character, String>();

        charMap.put('a', "aA4");
        charMap.put('b', "bB");
        charMap.put('c', "cC");
        charMap.put('d', "dD");
        charMap.put('e', "eE3");
        charMap.put('f', "fF");
        charMap.put('g', "gG9");
        charMap.put('h', "hH");
        charMap.put('i', "iIl");
        charMap.put('j', "jJ");
        charMap.put('k', "kK");
        charMap.put('l', "lLI");
        charMap.put('m', "mM");
        charMap.put('n', "nN");
        charMap.put('o', "oO0");
        charMap.put('p', "pP");
        charMap.put('q', "qQ");
        charMap.put('r', "rR");
        charMap.put('s', "sS");
        charMap.put('t', "tT");
        charMap.put('u', "uU");
        charMap.put('v', "vV");
        charMap.put('w', "wW");
        charMap.put('x', "xX");
        charMap.put('y', "yY");
        charMap.put('z', "zZ");
        charMap.put('1', "1!");
        charMap.put('2', "2@");
        charMap.put('3', "3#E");
        charMap.put('4', "4$A");
        charMap.put('5', "5%");
        charMap.put('6', "6^");
        charMap.put('7', "7&");
        charMap.put('8', "8*");
        charMap.put('9', "9(");
        charMap.put('0', "0)");
        charMap.put('-', "-_");
        charMap.put('=', "=+");
        charMap.put('[', "[{");
        charMap.put(']', "]}");
        charMap.put(',', ",<");
        charMap.put('.', ".>");
        charMap.put('/', "/?");
        charMap.put('`', "`~");
        charMap.put('\'', "\'\"");
        charMap.put(' ', " ");
    }

    public String generatePasswordFromText(String origText) {

        String trimed = trimOrigTest(origText);

        char[] origChars = trimed.toCharArray();

        String generated = "";

        do {
            generated = generatePassword(origChars);
        } while (trimed.equals(generated));

        return generated;
    }

    private String trimOrigTest(String origText) {
        return null == origText ? "" : origText.trim().toLowerCase();
    }

    private String generatePassword(char[] origChars) {
        if (0 == origChars.length) {
            return "";
        }

        StringBuffer passwd = new StringBuffer();

        for (int i = 0; i < origChars.length; i++) {
            passwd.append(pickChar(origChars[i]));
        }

        return passwd.toString();
    }

    private char pickChar(char origChar) {
        if (origChar < 32) {
            return ' ';
        }

        String charBase = charMap.get(origChar);

        int rnd = random(charBase.length());

        return charBase.charAt(rnd);
    }

    private int random(int charBaseLength) {
        try {
            return SecureRandom.getInstance("SHA1PRNG").nextInt(charBaseLength);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace(System.err);
            return 0;
        }
    }
}

簡單說明

  • 首先我們會做一段繁瑣的資料初始化動作,當然,你可以用更 smart 的方式,我只是讓程式比較可讀
  • 其次,在主程式區塊中,需要先處理字串,將頭尾空白去掉,然後轉小寫,當然你若喜歡密碼前後有空白,可以只轉成小寫
  • 再來,比較麻煩,一個一個字元取出,然後在剛剛做出來的編碼表中取出隨機的字碼
  • 我們使用了 Java 的 SecureRandom 機制,經測試,使用 java.util.Randomjava.security.SecureRandom 的差別,在於執行個幾遍,觀察字碼的重複次數,SecureRandom 重複次數較低
  • 最後用 StringBuffer 組合起來

小結

其實這個程式很簡單,但由此開始,我們會更深入地思考關於其他更廣泛的資訊安全相關實作議題。本程式可參考 GitHub 專案的
PasswordGenerator 分支。


上一篇
安全地寫 Java 的 「基本功」- Day 0
下一篇
安全地寫 Java 的 「基本功」- Day 2
系列文
安全地寫 Java 的「基本功」14

尚未有邦友留言

立即登入留言