/* * Created on Jul 15, 2008 * Created by Paul Gardner * * Copyright 2008 Vuze, Inc. All rights reserved. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 2 of the License only. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307, USA. */ package com.aelitis.azureus.core.subs.impl; import java.io.File; import java.io.IOException; import java.security.Signature; import java.util.*; import org.gudy.azureus2.core3.util.BEncoder; import org.gudy.azureus2.core3.util.Debug; import org.gudy.azureus2.core3.util.SHA1Simple; import com.aelitis.azureus.core.security.CryptoECCUtils; import com.aelitis.azureus.core.subs.Subscription; import com.aelitis.azureus.core.subs.SubscriptionException; import com.aelitis.azureus.core.vuzefile.VuzeFile; import com.aelitis.azureus.core.vuzefile.VuzeFileComponent; import com.aelitis.azureus.core.vuzefile.VuzeFileHandler; public class SubscriptionBodyImpl { private static final int SIMPLE_ID_LENGTH = 10; protected static byte[] encode( byte[] hash, int version, int size ) { int hash_len = hash.length; byte[] result = new byte[ hash_len + 4 + 4 ]; System.arraycopy( hash, 0, result, 0, hash_len ); System.arraycopy( SubscriptionImpl.intToBytes(version), 0, result, hash_len, 4 ); System.arraycopy( SubscriptionImpl.intToBytes(size), 0, result, hash_len+4, 4 ); return( result ); } protected static byte[] sign( byte[] private_key, byte[] hash, int version, int size ) throws Exception { Signature signature = CryptoECCUtils.getSignature( CryptoECCUtils.rawdataToPrivkey( private_key )); // key for signature is hash + version + size so we have some // control over auto-update process and prevent people from injecting // potentially huge bogus updates signature.update( encode( hash, version, size )); return( signature.sign()); } protected static boolean verify( byte[] public_key, byte[] hash, int version, int size, byte[] sig ) { try{ Signature signature = CryptoECCUtils.getSignature( CryptoECCUtils.rawdataToPubkey( public_key )); signature.update( encode( hash, version, size )); return( signature.verify( sig )); }catch( Throwable e ){ Debug.out( e ); return( false ); } } protected static byte[] deriveShortID( byte[] public_key, Map singleton_details ) { if ( singleton_details != null ){ return( deriveSingletonShortID( singleton_details )); }else{ byte[] hash = new SHA1Simple().calculateHash( public_key ); byte[] short_id = new byte[SIMPLE_ID_LENGTH]; System.arraycopy( hash, 0, short_id, 0, SIMPLE_ID_LENGTH ); return( short_id ); } } protected static byte[] deriveSingletonShortID( Map singleton_details ) { byte[] short_id = new byte[SIMPLE_ID_LENGTH]; byte[] explicit_sid = new SHA1Simple().calculateHash((byte[])singleton_details.get( "key" )); System.arraycopy( explicit_sid, 0, short_id, 0, SIMPLE_ID_LENGTH ); return( short_id ); } private SubscriptionManagerImpl manager; private String name; private boolean is_public; private byte[] public_key; private int version; private int az_version; private String json; private Map singleton_details; private byte[] hash; private byte[] sig; private int sig_data_size; private Map map; // load constructor protected SubscriptionBodyImpl( SubscriptionManagerImpl _manager, SubscriptionImpl _subs ) throws SubscriptionException { manager = _manager; try{ File vuze_file = manager.getVuzeFile( _subs ); VuzeFile vf = VuzeFileHandler.getSingleton().loadVuzeFile( vuze_file.getAbsolutePath()); if ( vf == null ){ throw( new IOException( "Failed to load vuze file '" + vuze_file + "'" )); } load( vf.getComponents()[0].getContent(), false ); }catch( Throwable e ){ rethrow( e ); } } // import constructor protected SubscriptionBodyImpl( SubscriptionManagerImpl _manager, Map _map ) throws IOException { manager = _manager; load( _map, true ); } protected void load( Map _map, boolean _verify ) throws IOException { map = _map; hash = (byte[])map.get( "hash" ); sig = (byte[])map.get( "sig" ); Long l_size = (Long)map.get( "size" ); Map details = (Map)map.get( "details" ); if ( details == null || hash == null || sig == null || l_size == null ){ throw( new IOException( "Invalid subscription - details missing" )); } sig_data_size = l_size.intValue(); name = new String((byte[])details.get( "name" ), "UTF-8" ); public_key = (byte[])details.get( "public_key" ); version = ((Long)details.get( "version" )).intValue(); is_public = ((Long)details.get( "is_public" )).intValue()==1; json = new String((byte[])details.get( "json"), "UTF-8" ); singleton_details = (Map)details.get( "sin_details" ); Long l_az_version = (Long)details.get( "az_version" ); az_version = l_az_version==null?Subscription.AZ_VERSION:l_az_version.intValue(); if ( _verify ){ // verify byte[] contents = BEncoder.encode( details ); byte[] actual_hash = new SHA1Simple().calculateHash( contents ); if ( !Arrays.equals( actual_hash, hash )){ // backwards compat for pre-az_version Map details_copy = new HashMap( details ); details_copy.remove( "az_version" ); contents = BEncoder.encode( details_copy ); actual_hash = new SHA1Simple().calculateHash( contents ); } if ( !Arrays.equals( actual_hash, hash )){ throw( new IOException( "Hash mismatch" )); } if ( sig_data_size != contents.length ){ throw( new IOException( "Signature data length mismatch" )); } if ( !verify( public_key, hash, version, sig_data_size, sig )){ throw( new IOException( "Signature verification failed" )); } } } // create constructor protected SubscriptionBodyImpl( SubscriptionManagerImpl _manager, String _name, boolean _is_public, String _json_content, byte[] _public_key, int _version, int _az_version, Map _singleton_details ) throws IOException { manager = _manager; name = _name; is_public = _is_public; public_key = _public_key; version = _version; az_version = _az_version; json = _json_content; singleton_details = _singleton_details; map = new HashMap(); Map details = new HashMap(); map.put( "details", details ); details.put( "name", name.getBytes( "UTF-8" )); details.put( "is_public", new Long( is_public?1:0 )); details.put( "public_key", public_key ); details.put( "version", new Long( version )); details.put( "az_version", new Long( az_version )); details.put( "json", _json_content.getBytes( "UTF-8" )); if ( singleton_details != null ){ details.put( "sin_details", singleton_details ); } } protected void updateDetails( SubscriptionImpl subs, Map details ) throws IOException { is_public = subs.isPublic(); version = subs.getVersion(); az_version = subs.getAZVersion(); name = subs.getName(false); details.put( "name",name.getBytes( "UTF-8" )); details.put( "is_public", new Long( is_public?1:0 )); details.put( "version", new Long( version )); details.put( "az_version", new Long( az_version )); if ( json != null ){ details.put( "json", json.getBytes( "UTF-8" )); } if ( singleton_details != null ){ details.put( "sin_details", singleton_details ); } } protected String getName() { return( name ); } protected byte[] getPublicKey() { return( public_key ); } public byte[] getShortID() { return( deriveShortID( public_key, singleton_details )); } protected boolean isPublic() { return( is_public ); } protected String getJSON() { return( json ); } protected Map getSingletonDetails() { return( singleton_details ); } protected void setJSON( String _json ) { json = _json; } protected int getVersion() { return( version ); } protected int getAZVersion() { return( az_version ); } // derived data protected byte[] getHash() { return( hash ); } protected byte[] getSig() { return( sig ); } protected int getSigDataSize() { return( sig_data_size ); } protected void writeVuzeFile( SubscriptionImpl subs ) throws SubscriptionException { try{ File file = manager.getVuzeFile( subs ); Map details = (Map)map.get( "details" ); updateDetails( subs, details ); byte[] contents = BEncoder.encode( details ); byte[] new_hash = new SHA1Simple().calculateHash( contents ); byte[] old_hash = (byte[])map.get( "hash" ); // backward compat from before az_version was introduced if ( old_hash != null && !Arrays.equals( old_hash, new_hash )){ Map details_copy = new HashMap( details ); details_copy.remove( "az_version" ); contents = BEncoder.encode( details_copy ); new_hash = new SHA1Simple().calculateHash( contents ); } if ( old_hash == null || !Arrays.equals( old_hash, new_hash )){ byte[] private_key = subs.getPrivateKey(); if ( private_key == null ){ throw( new SubscriptionException( "Only the originator of a subscription can modify it" )); } map.put( "size", new Long( contents.length )); try{ map.put( "hash", new_hash ); map.put( "sig", sign( private_key, new_hash, version, contents.length )); }catch( Throwable e ){ throw( new SubscriptionException( "Crypto failed: " + Debug.getNestedExceptionMessage(e))); } } File backup_file = null; if ( file.exists()){ backup_file = new File( file.getParent(), file.getName() + ".bak" ); backup_file.delete(); if ( !file.renameTo( backup_file )){ throw( new SubscriptionException( "Backup failed" )); } } try{ VuzeFile vf = VuzeFileHandler.getSingleton().create(); vf.addComponent( VuzeFileComponent.COMP_TYPE_SUBSCRIPTION, map ); vf.write( file ); hash = new_hash; sig = (byte[])map.get( "sig" ); sig_data_size = contents.length; }catch( Throwable e ){ if ( backup_file != null ){ backup_file.renameTo( file ); } throw( new SubscriptionException( "File write failed: " + Debug.getNestedExceptionMessage(e))); } }catch( Throwable e ){ rethrow( e ); } } protected void rethrow( Throwable e ) throws SubscriptionException { if ( e instanceof SubscriptionException ){ throw((SubscriptionException)e); } throw( new SubscriptionException( "Operation failed", e )); } }