孔子在周易繫辭篇,說到一句非常有名的話,「是故形而上者謂之道,形而下者謂之器」,形而上的是事物運行的原理,形而下的是事物運行的方法論。這是一個很偉大的見解,但當然,我們不是談哲學。
資訊安全是一門很大的學門,從 ISO 的標準來看,它是一種管理系統,需要一整個企業體投入配合,就像品質管理一樣。從 ISC^2 的觀點來看,他們不談那麼大的範圍,而是從軟體的生命週期著手,切分成數個領域來討論,有種分而治之的概念。然而再到了 EC-Concil,則形成了各個語言、技術領域的真槍實戰內容。
但其實我並非專精這門的人,在 30 天的日子裡,我們也很難談完百家爭鳴的各個概念與工法。然而,在那麼大的領域裡,要達成 CIA 的特性,總會有一些方法。最重要的,就是認證(Authentication)、授權(Authorization)、稽核究責(Auditing/Accountability)。
認證是最常見的一種安全控管機制,當各位透過帳號密碼登入 IT 邦幫忙或 Facebook、Gmail 時,其實就是在經過認證的機制了。但認證更不只如此。例如你上 Amazon 買電子書時,它需要認出你是那個正確的會員,同樣的,你也必須認出它是真正的 Amazon。因此我們使用 SSL/TLS 機制來進行對 Serevr 端的認證。
除了這類作法,其實,當你回到家,用鑰匙打開門時,就是一種認證機制了。根據你所知道的帳密,你可以登入到這個平台來,根據你所擁有的鑰匙,你可以回到你的家。除此之外,你還可以根據你的指紋,解鎖手上的 iPhone,這就是根據生物特徵的認證機制。
授權常常是伴隨著成功的認證機制而來的,當然,你若認證不成功,小則乾瞪眼,大則被抓走。認證完了之後,就是給予適當權限的時候了。例如你在 Facebook 上發文,可以決定這是要給路人看的,還是要給朋友看的,又或者只有某些密友能看。其實隱私的管理,也是一種廣義的授權。
授權常用的方法,連我自己都常在用的,是矩陣式權限配置搭配上 rule based 的權限設定。程式在實作上時,或許免不了使用到 SpringSecurity 這個大框架,但我們還是需要知道背後的原理。
稽核的動作,其實像對帳、查帳一樣,例如到這個月底發現口袋空了,只好把這個月的發票拿出來,調查看看到底是那一次失心瘋買了不該買的東西。這就是稽核。但巧婦難為無米之炊,大會計師難為無發票稽核,沒有發票,沒有軌跡,就沒辦法稽核。
有人在管 Linux/BSD Server 的,可能常常要三不五時去 /var/log/
下找 log,系統的 log 就是最重要的稽核軌跡。同樣的,程式的 log 就是你的程式背著你偷偷幹了哪些事的明證。
不知道各位平常怎樣取自己的密碼,第一個程式,我們先別談太高深的密碼學原理,因為我也不會。也別談太高層次的設計理論,有興趣歡迎來我們公司。我們談談怎樣取密碼。
我們先來寫第一個測試,這個測試很簡單,呼叫 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,可以進行以下處理:
當然,你可以找到更多規則,建立自己的密碼轉換規則。
以下是一個簡單的密碼轉換程式:
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;
}
}
}
SecureRandom
機制,經測試,使用 java.util.Random
與 java.security.SecureRandom
的差別,在於執行個幾遍,觀察字碼的重複次數,SecureRandom 重複次數較低其實這個程式很簡單,但由此開始,我們會更深入地思考關於其他更廣泛的資訊安全相關實作議題。本程式可參考 GitHub 專案的
PasswordGenerator 分支。