/**
* personium.io
* Copyright 2014 FUJITSU LIMITED
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fujitsu.dc.common.auth.token;
import java.io.UnsupportedEncodingException;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.apache.commons.lang.CharEncoding;
import org.apache.commons.lang.StringUtils;
import com.fujitsu.dc.common.utils.DcCoreUtils;
/**
* Cell Local Token の生成・パースを行うクラス.
*/
public abstract class LocalToken extends AbstractOAuth2Token {
/**
* AES/CBC/PKCS5Padding.
*/
public static final String AES_CBC_PKCS5_PADDING = "AES/CBC/PKCS5Padding";
private static final String SEPARATOR = "\t";
static final int IV_BYTE_LENGTH = 16;
private static byte[] keyBytes;
private static SecretKey aesKey;
/**
* Key文字列を設定します。
* @param keyString キー文字列.
*/
public static void setKeyString(String keyString) {
keyBytes = keyString.getBytes(); // 16/24/32バイトの鍵バイト列
aesKey = new SecretKeySpec(keyBytes, "AES");
}
/**
* 明示的な有効期間を設定してトークンを生成する.
* @param issuedAt 発行時刻(epochからのミリ秒)
* @param lifespan 有効時間(ミリ秒)
* @param issuer 発行者
* @param subject 主体
* @param schema スキーマ
*/
public LocalToken(final long issuedAt, final long lifespan, final String issuer,
final String subject, final String schema) {
this.issuedAt = issuedAt;
this.lifespan = lifespan;
this.issuer = issuer;
this.subject = subject;
this.schema = schema;
}
final String doCreateTokenString(final String[] contents) {
StringBuilder raw = new StringBuilder();
// 発行時刻のEpochからのミリ秒を逆順にした文字列が先頭から入るため、推測しづらい。
String iaS = Long.toString(this.issuedAt);
String iaSr = StringUtils.reverse(iaS);
raw.append(iaSr);
raw.append(SEPARATOR);
raw.append(Long.toString(this.lifespan));
raw.append(SEPARATOR);
raw.append(this.subject);
raw.append(SEPARATOR);
if (this.schema != null) {
raw.append(this.schema);
}
if (contents != null) {
for (String cont : contents) {
raw.append(SEPARATOR);
if (cont != null) {
raw.append(cont);
}
}
}
raw.append(SEPARATOR);
raw.append(this.issuer);
return encode(raw.toString(), getIvBytes(issuer));
}
/**
* 指定のIssuer向けのIV(Initial Vector)を生成して返します.
* IVとしてissuerの最後の最後の16文字を逆転させた文字列を用います。 これにより、違うIssuerを想定してパースすると、パースに失敗する。
* @param issuer Issuer URL
* @return Initial Vectorの Byte配列
*/
protected static byte[] getIvBytes(final String issuer) {
try {
return StringUtils.reverse("123456789abcdefg" + issuer)
.substring(0, IV_BYTE_LENGTH).getBytes(CharEncoding.UTF_8);
} catch (UnsupportedEncodingException e) {
throw new RuntimeException(e);
}
}
/**
* パース処理.
* @param token トークン
* @param issuer 発行者
* @param numFields フィールド数
* @return パースされたトークン
* @throws AbstractOAuth2Token.TokenParseException トークン解釈に失敗したとき
*/
static String[] doParse(final String token, final String issuer,
final int numFields) throws AbstractOAuth2Token.TokenParseException {
String tokenDecoded = decode(token, getIvBytes(issuer));
String[] frag = tokenDecoded.split(SEPARATOR);
// 正常な形式でなければパース失敗とする
if (frag.length != numFields || !issuer.equals(frag[numFields - 1])) {
throw AbstractOAuth2Token.PARSE_EXCEPTION;
}
return frag;
}
/**
* 文字列を暗号化する.
* @param in 入力文字列
* @param ivBytes イニシャルベクトル
* @return 暗号化された文字列
*/
public static String encode(final String in, final byte[] ivBytes) {
// IVに、発行CELLのURL逆順を入れることで、より短いトークンに。
Cipher cipher;
try {
cipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING);
cipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(ivBytes));
byte[] cipherBytes = cipher.doFinal(in.getBytes(CharEncoding.UTF_8));
return DcCoreUtils.encodeBase64Url(cipherBytes);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* 復号する.
* @param in 暗号化文字列
* @param ivBytes イニシャルベクトル
* @return 復号された文字列
* @throws AbstractOAuth2Token.TokenParseException 例外
*/
public static String decode(final String in, final byte[] ivBytes) throws AbstractOAuth2Token.TokenParseException {
byte[] inBytes = DcCoreUtils.decodeBase64Url(in);
Cipher cipher;
try {
cipher = Cipher.getInstance(AES_CBC_PKCS5_PADDING);
} catch (NoSuchAlgorithmException e) {
throw AbstractOAuth2Token.PARSE_EXCEPTION;
} catch (NoSuchPaddingException e) {
throw AbstractOAuth2Token.PARSE_EXCEPTION;
}
try {
cipher.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(ivBytes));
} catch (InvalidKeyException e) {
throw AbstractOAuth2Token.PARSE_EXCEPTION;
} catch (InvalidAlgorithmParameterException e) {
throw AbstractOAuth2Token.PARSE_EXCEPTION;
}
byte[] plainBytes;
try {
plainBytes = cipher.doFinal(inBytes);
} catch (IllegalBlockSizeException e) {
throw AbstractOAuth2Token.PARSE_EXCEPTION;
} catch (BadPaddingException e) {
throw AbstractOAuth2Token.PARSE_EXCEPTION;
}
try {
return new String(plainBytes, CharEncoding.UTF_8);
} catch (UnsupportedEncodingException e) {
throw AbstractOAuth2Token.PARSE_EXCEPTION;
}
}
}