/**
* Copyright (c) 2015 unfoldingWord
* http://creativecommons.org/licenses/MIT/
* See LICENSE file for details.
* Contributors:
* PJ Fechner <pj@actsmedia.com>
*/
package signing;
import android.util.Base64;
import android.util.Log;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.security.PublicKey;
import java.util.Arrays;
import java.util.Date;
/**
* A Signing Identity represents an organization that has been registered to sign content.
*
*/
public class SigningEntity {
private static final String TAG = "SigningEntity";
private final PublicKey mCAPublicKey;
private final PublicKey mPublicKey;
public final Organization organization;
private final byte[] mData;
private final byte[] mSignature;
private Status mStatus;
/**
* Creates a new Signing Entity
* @param caPublicKey The Certificate Authority's public key
* @param publicKey The Signing Entity's public key
* @param organization The entity's organization info
* @param keyOrgData The concated public key and organization data
* @param keyOrgSig The signature of the entity's concated public key and organization
*/
public SigningEntity(PublicKey caPublicKey, PublicKey publicKey, Organization organization, byte[] keyOrgData, byte[] keyOrgSig) {
mCAPublicKey = caPublicKey;
mPublicKey = publicKey;
mSignature = keyOrgSig;
// this techncially duplicates the key and org data, but we pass it along so we don't convert everything to bytes again
// and possibly introduce additional points of error
mData = keyOrgData;
this.organization = organization;
}
/**
* Checks the validation status of this Signing Entity
* @return
*/
public Status status() {
if(mStatus == null) {
mStatus = Crypto.verifyECDSASignature(mCAPublicKey, mSignature, mData);
if(mStatus == Status.VERIFIED) {
// check if expired
if(new Date().after(organization.expiresAt)) {
mStatus = Status.EXPIRED;
}
}
}
return mStatus;
}
/**
* Checks the validation status of the signed content.
* @param signature The signature of hte data as signed by the Se
* @param data The data that will be validated against the signature (the source translation)
* @return
*/
public Status verifyContent(String signature, byte[] data) {
if(signature == null){
Log.e(TAG, "signature Error");
return Status.ERROR;
}
byte[] sig = Base64.decode(signature, Base64.NO_WRAP);
Status contentStatus = verifyContent(sig, data);
// always return the most severe status
if(contentStatus.weight() > status().weight()) {
return contentStatus;
} else {
return status();
}
}
/**
* Checks the validation status of the signed content.
* @param signature The signature of the data as signed by the SE
* @param data The data that will be validated against the signature (the source translation)
* @return
*/
public Status verifyContent(byte[] signature, byte[] data) {
Status contentStatus = Crypto.verifyECDSASignature(mPublicKey, signature, data);
// always return the most severe status
if(contentStatus.weight() > status().weight()) {
return contentStatus;
} else {
return status();
}
}
/**
* Generates a new signing entity from the signing identity
* @param caPublicKey The The Certificate Authority's public key
* @param signingIdentity An input stream to the Signing Identity
* @return
*/
public static SigningEntity generateFromIdentity(PublicKey caPublicKey, InputStream signingIdentity) {
BufferedReader br = new BufferedReader(new InputStreamReader(signingIdentity));
StringBuilder pkBuilder = new StringBuilder();
StringBuilder orgBuilder = new StringBuilder();
StringBuilder sigBuilder = new StringBuilder();
StringBuilder dataBuilder = new StringBuilder();
// read Signing Identity
try {
String section = null;
String line;
while((line = br.readLine()) != null) {
if(line.startsWith("-----")) {
// start/end section
section = line;
} else if(!line.trim().isEmpty()){
// build sections
if(section.equals("-----BEGIN PUBLIC KEY-----")) {
pkBuilder.append(line.trim());
} else if(section.equals("-----BEGIN ORG INFO-----")) {
orgBuilder.append(line.trim());
} else if(section.equals("-----BEGIN SIG-----")) {
sigBuilder.append(line.trim());
}
}
// store everything but the signature for verification
if(!section.equals("-----BEGIN SIG-----") && !section.equals("-----END SIG-----")) {
// TRICKY: we intentionally close with a trailing new line
dataBuilder.append(line + "\n");
}
}
} catch (IOException e) {
e.printStackTrace();
return null;
}
// Assemble Signing Entity
if(dataBuilder.length() > 0 && pkBuilder.length() > 0 && orgBuilder.length() > 0 && sigBuilder.length() > 0) {
byte[] keyOrgData;
try {
keyOrgData = dataBuilder.toString().getBytes("UTF-8");
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
byte[] keyBytes;
try {
keyBytes = Base64.decode(pkBuilder.toString().getBytes("UTF-8"), Base64.DEFAULT);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
return null;
}
PublicKey key = Crypto.loadPublicECDSAKey(keyBytes);
byte[] signature = Base64.decode(sigBuilder.toString(), Base64.NO_WRAP);
String orgJsonString;
try {
orgJsonString = new String(Base64.decode(orgBuilder.toString(), Base64.NO_WRAP));
} catch (Exception e) {
e.printStackTrace();
return null;
}
Organization org = Organization.generate(orgJsonString);
if(org != null) {
return new SigningEntity(caPublicKey, key, org, keyOrgData, signature);
}
}
return null;
}
@Override
public String toString() {
return "SigningEntity{" +
"mCAPublicKey=" + mCAPublicKey +
", mPublicKey=" + mPublicKey +
", organization=" + organization +
", mData=" + Arrays.toString(mData) +
", mSignature=" + Arrays.toString(mSignature) +
", mStatus=" + mStatus +
'}';
}
}