/* * * * Copyright (C) 2014 Open Whisper Systems * * 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, either version 3 of the License, or * (at your option) any later version. * * 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, see <http://www.gnu.org/licenses/>. * / */ package org.anhonesteffort.flock.sync.calendar; import android.util.Log; import org.anhonesteffort.flock.util.guava.Optional; import org.anhonesteffort.flock.sync.DecryptedMultiStatusResult; import org.anhonesteffort.flock.sync.InvalidLocalComponentException; import org.anhonesteffort.flock.sync.InvalidRemoteComponentException; import org.anhonesteffort.flock.webdav.MultiStatusResult; import org.anhonesteffort.flock.webdav.caldav.CalDavConstants; import net.fortuna.ical4j.data.CalendarBuilder; import net.fortuna.ical4j.data.CalendarOutputter; import net.fortuna.ical4j.data.ParserException; import net.fortuna.ical4j.model.Calendar; import net.fortuna.ical4j.model.ConstraintViolationException; import net.fortuna.ical4j.model.Date; import net.fortuna.ical4j.model.ValidationException; import net.fortuna.ical4j.model.component.VEvent; import net.fortuna.ical4j.model.component.VToDo; import net.fortuna.ical4j.model.property.CalScale; import net.fortuna.ical4j.model.property.DtEnd; import net.fortuna.ical4j.model.property.ProdId; import net.fortuna.ical4j.model.property.Uid; import net.fortuna.ical4j.model.property.Version; import net.fortuna.ical4j.model.property.XProperty; import net.fortuna.ical4j.util.Calendars; import org.anhonesteffort.flock.crypto.InvalidMacException; import org.anhonesteffort.flock.crypto.MasterCipher; import org.anhonesteffort.flock.sync.HidingDavCollection; import org.anhonesteffort.flock.sync.HidingDavCollectionMixin; import org.anhonesteffort.flock.crypto.HidingUtil; import org.anhonesteffort.flock.sync.OwsWebDav; import org.anhonesteffort.flock.webdav.ComponentETagPair; import org.anhonesteffort.flock.webdav.InvalidComponentException; import org.anhonesteffort.flock.webdav.PropertyParseException; import org.anhonesteffort.flock.webdav.caldav.CalDavCollection; import org.anhonesteffort.flock.webdav.caldav.CalDavStore; import org.apache.jackrabbit.webdav.DavException; import org.apache.jackrabbit.webdav.property.DavPropertyName; import org.apache.jackrabbit.webdav.property.DavPropertyNameSet; import org.apache.jackrabbit.webdav.property.DavPropertySet; import org.apache.jackrabbit.webdav.property.DefaultDavProperty; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.StringReader; import java.security.GeneralSecurityException; import java.util.LinkedList; import java.util.List; /** * Programmer: rhodey */ public class HidingCalDavCollection extends CalDavCollection implements HidingDavCollection<Calendar> { private static final String TAG = "org.anhonesteffort.flock.sync.calendar.HidingCalDavCollection"; protected static final String PROPERTY_NAME_FLOCK_HIDDEN_CALENDAR = "X-FLOCK-HIDDEN-CALENDAR"; protected static final String PROPERTY_NAME_HIDDEN_COLOR = "X-FLOCK-HIDDEN-CALENDAR-COLOR"; protected static final DavPropertyName PROPERTY_HIDDEN_COLOR = DavPropertyName.create( PROPERTY_NAME_HIDDEN_COLOR, OwsWebDav.NAMESPACE ); private MasterCipher masterCipher; private HidingDavCollectionMixin delegate; protected HidingCalDavCollection(CalDavStore calDavStore, String path, MasterCipher masterCipher) { super(calDavStore, path, new DavPropertySet()); this.masterCipher = masterCipher; this.delegate = new HidingDavCollectionMixin(this, masterCipher); } protected HidingCalDavCollection(CalDavCollection calDavCollection, MasterCipher masterCipher) { super((CalDavStore) calDavCollection.getStore(), calDavCollection.getPath(), calDavCollection.getProperties()); this.masterCipher = masterCipher; this.delegate = new HidingDavCollectionMixin(this, masterCipher); } @Override protected DavPropertyNameSet getPropertyNamesForFetch() { DavPropertyNameSet calendarProps = super.getPropertyNamesForFetch(); DavPropertyNameSet hidingProps = delegate.getPropertyNamesForFetch(); calendarProps.addAll(hidingProps); calendarProps.add(PROPERTY_HIDDEN_COLOR); return calendarProps; } @Override public boolean isFlockCollection() throws PropertyParseException { return delegate.isFlockCollection(); } @Override public void makeFlockCollection(String displayName) throws DavException, IOException, GeneralSecurityException { DavPropertyNameSet removeNameSet = new DavPropertyNameSet(); removeNameSet.add(CalDavConstants.PROPERTY_NAME_CALENDAR_COLOR); delegate.makeFlockCollection(displayName, removeNameSet); } @Override public Optional<String> getHiddenDisplayName() throws PropertyParseException, InvalidMacException, GeneralSecurityException, IOException { return delegate.getHiddenDisplayName(); } @Override public void setHiddenDisplayName(String displayName) throws DavException, IOException, GeneralSecurityException { delegate.setHiddenDisplayName(displayName); } public Optional<Integer> getHiddenColor() throws PropertyParseException, InvalidMacException, GeneralSecurityException, IOException { Optional<String> hiddenColor = getProperty(PROPERTY_HIDDEN_COLOR, String.class); if (!hiddenColor.isPresent()) return getColor(); String hiddenColorString = HidingUtil.decodeAndDecryptIfNecessary(masterCipher, hiddenColor.get()); return Optional.of(Integer.valueOf(hiddenColorString)); } public void setHiddenColor(Integer color) throws DavException, IOException, GeneralSecurityException { String hiddenColorString = HidingUtil.encryptEncodeAndPrefix(masterCipher, color.toString()); DavPropertySet updateProperties = new DavPropertySet(); updateProperties.add(new DefaultDavProperty<String>(PROPERTY_HIDDEN_COLOR, hiddenColorString)); patchProperties(updateProperties, new DavPropertyNameSet()); } protected ComponentETagPair<Calendar> getHiddenComponent(ComponentETagPair<Calendar> exposedComponentPair) throws InvalidRemoteComponentException, InvalidMacException, GeneralSecurityException, IOException { Calendar exposedComponent = exposedComponentPair.getComponent(); XProperty protectedComponent = (XProperty) exposedComponent.getProperty(PROPERTY_NAME_FLOCK_HIDDEN_CALENDAR); if (protectedComponent == null) return exposedComponentPair; String recoveredComponentText = HidingUtil.decodeAndDecryptIfNecessary(masterCipher, protectedComponent.getValue()); StringReader stringReader = new StringReader(recoveredComponentText); CalendarBuilder calendarBuilder = new CalendarBuilder(); try { Calendar recoveredComponent = calendarBuilder.build(stringReader); return new ComponentETagPair<Calendar>(recoveredComponent, exposedComponentPair.getETag()); } catch (ParserException e) { Log.e(TAG, "caught exception while trying to build from hidden component", e); try { Uid uid = Calendars.getUid(exposedComponent); if (uid != null) { throw new InvalidRemoteComponentException("caught exception while trying to build from hidden component", CalDavConstants.CALDAV_NAMESPACE, getPath(), uid.getValue(), e); } } catch (ConstraintViolationException ex) { } throw new InvalidRemoteComponentException("caught exception while trying to build from hidden component", CalDavConstants.CALDAV_NAMESPACE, getPath(), e); } } @Override public Optional<ComponentETagPair<Calendar>> getHiddenComponent(String uid) throws InvalidRemoteComponentException, DavException, InvalidMacException, GeneralSecurityException, IOException { try { Optional<ComponentETagPair<Calendar>> originalComponentPair = super.getComponent(uid); if (!originalComponentPair.isPresent()) return Optional.absent(); return Optional.of(getHiddenComponent(originalComponentPair.get())); } catch (InvalidComponentException e) { throw new InvalidRemoteComponentException(e); } } @Override public DecryptedMultiStatusResult<Calendar> getHiddenComponents(List<String> uids) throws DavException, GeneralSecurityException, IOException { MultiStatusResult<Calendar> exposedComponentPairs = super.getComponents(uids); DecryptedMultiStatusResult<Calendar> decryptedComponentPairs = new DecryptedMultiStatusResult<Calendar>( new LinkedList<ComponentETagPair<Calendar>>(), exposedComponentPairs.getInvalidComponentExceptions(), new LinkedList<InvalidMacException>() ); for (ComponentETagPair<Calendar> exposedComponentPair : exposedComponentPairs.getComponentETagPairs()) { try { decryptedComponentPairs.getComponentETagPairs().add(getHiddenComponent(exposedComponentPair)); } catch (InvalidRemoteComponentException e) { decryptedComponentPairs.getInvalidComponentExceptions().add(e); } catch (InvalidMacException e) { decryptedComponentPairs.getInvalidMacExceptions().add(e); } } return decryptedComponentPairs; } @Override public DecryptedMultiStatusResult<Calendar> getHiddenComponents() throws DavException, GeneralSecurityException, IOException { MultiStatusResult<Calendar> exposedComponentPairs = super.getComponents(); DecryptedMultiStatusResult<Calendar> decryptedComponentPairs = new DecryptedMultiStatusResult<Calendar>( new LinkedList<ComponentETagPair<Calendar>>(), exposedComponentPairs.getInvalidComponentExceptions(), new LinkedList<InvalidMacException>() ); for (ComponentETagPair<Calendar> exposedComponentPair : exposedComponentPairs.getComponentETagPairs()) { try { decryptedComponentPairs.getComponentETagPairs().add(getHiddenComponent(exposedComponentPair)); } catch (InvalidRemoteComponentException e) { decryptedComponentPairs.getInvalidComponentExceptions().add(e); } catch (InvalidMacException e) { decryptedComponentPairs.getInvalidMacExceptions().add(e); } } return decryptedComponentPairs; } // NOTICE: All events starting within a given month will appear to start on the first day // NOTICE... of the month and end on the last day of this month... is this acceptable??? protected void putHiddenComponentToServer(Calendar exposedComponent, Optional<String> ifMatchETag) throws InvalidLocalComponentException, GeneralSecurityException, IOException, DavException { exposedComponent.getProperties().remove(ProdId.PRODID); exposedComponent.getProperties().add(new ProdId(((CalDavStore)getStore()).getProductId())); Calendar protectedComponent = new Calendar(); protectedComponent.getProperties().add(Version.VERSION_2_0); protectedComponent.getProperties().add(CalScale.GREGORIAN); java.util.Calendar calendar = java.util.Calendar.getInstance(); VEvent exposedEvent = (VEvent) exposedComponent.getComponent(VEvent.VEVENT); VToDo exposedToDo = (VToDo ) exposedComponent.getComponent(VEvent.VTODO); if (exposedEvent != null) { if (exposedEvent.getUid() == null || exposedEvent.getUid().getValue() == null) { Log.e(TAG, "was given a VEVENT with no UID"); throw new InvalidLocalComponentException("Cannot put an iCal to server without UID!", CalDavConstants.CALDAV_NAMESPACE, getPath()); } Date startDate = exposedEvent.getStartDate().getDate(); calendar.setTime(new java.util.Date(startDate.getTime())); calendar.set(java.util.Calendar.DAY_OF_MONTH, 1); calendar.set(java.util.Calendar.HOUR, 1); calendar.set(java.util.Calendar.MINUTE, 1); calendar.set(java.util.Calendar.SECOND, 1); calendar.set(java.util.Calendar.MILLISECOND, 1); Date approximateEndDate = new Date(calendar.getActualMaximum(java.util.Calendar.DAY_OF_MONTH)); DtEnd approximateDtEnd = new DtEnd(approximateEndDate); VEvent protectedEvent = new VEvent(new Date(calendar.getTime()), "Open Whisper Systems - Flock"); protectedEvent.getProperties().add(approximateDtEnd); protectedEvent.getProperties().add(exposedEvent.getUid()); protectedComponent.getComponents().add(protectedEvent); } else if (exposedToDo != null) { if (exposedToDo.getUid() == null || exposedToDo.getUid().getValue() == null) { Log.e(TAG, "was given a VTODO with no UID"); throw new InvalidLocalComponentException("Cannot put an iCal to server without UID!", CalDavConstants.CALDAV_NAMESPACE, getPath()); } Date startDate = exposedToDo.getStartDate().getDate(); calendar.setTime(new java.util.Date(startDate.getTime())); calendar.set(java.util.Calendar.DAY_OF_MONTH, 1); calendar.set(java.util.Calendar.HOUR, 1); calendar.set(java.util.Calendar.MINUTE, 1); calendar.set(java.util.Calendar.SECOND, 1); calendar.set(java.util.Calendar.MILLISECOND, 1); Date approximateEndDate = new Date(calendar.getActualMaximum(java.util.Calendar.DAY_OF_MONTH)); DtEnd approximateDtEnd = new DtEnd(approximateEndDate); VToDo protectedToDo = new VToDo(new Date(calendar.getTime()), "Open Whisper Systems - Flock"); protectedToDo.getProperties().add(approximateDtEnd); protectedToDo.getProperties().add(exposedToDo.getUid()); protectedComponent.getComponents().add(protectedToDo); } else { Log.e(TAG, "was given an calendar component containing neither VEVENT or VTODO"); throw new InvalidLocalComponentException("was given an calendar component containing neither VEVENT or VTODO", CalDavConstants.CALDAV_NAMESPACE, getPath()); } CalendarOutputter calendarOutputter = new CalendarOutputter(); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); try { calendarOutputter.output(exposedComponent, byteStream); String protectedComponentData = HidingUtil.encryptEncodeAndPrefix(masterCipher, byteStream.toString()); XProperty xSecureSyncHiddenProp = new XProperty(PROPERTY_NAME_FLOCK_HIDDEN_CALENDAR, protectedComponentData); protectedComponent.getProperties().add(xSecureSyncHiddenProp); super.putComponentToServer(protectedComponent, ifMatchETag); } catch (InvalidComponentException e) { throw new InvalidLocalComponentException(e); } catch (ValidationException e) { Log.e(TAG, "caught exception while trying to output component to byte stream", e); throw new InvalidLocalComponentException("Caught exception while trying to output component to byte stream", CalDavConstants.CALDAV_NAMESPACE, getPath(), e); } } @Override public void addHiddenComponent(Calendar component) throws InvalidLocalComponentException, DavException, GeneralSecurityException, IOException { putHiddenComponentToServer(component, Optional.<String>absent()); } @Override public void updateHiddenComponent(ComponentETagPair<Calendar> component) throws InvalidLocalComponentException, DavException, GeneralSecurityException, IOException { putHiddenComponentToServer(component.getComponent(), component.getETag()); } @Override public void closeHttpConnection() { getStore().closeHttpConnection(); } }