April 4, 2021

Luca App Source Code Audit IV: Die Sache mit dem geklautem Source Code

Trigger Warnung: handzahmer Artikel

Es steht der Vorwurf im Raum, culture4life hätte Code geklaut, die Lizenzinformation entfernt und dann mit fremder Leistung dick abkassiert. Ärgerlicherweise schweigt sich die allgemeine Berichterstattung weitestgehend dazu aus, was denn nun genau geklaut wurde, von wem und wer die Primärquelle für die Meldung ist.

Ich versuch das mal hier einzuordnen.

Der Code, um den es geht ist dieser hier (mittlerweile mit wiederhergestelltem Copyright - Achtung, in voller Länge):

package de.culture4life.luca.util;
  
/*
* Copyright (c) 2015, Bubelich Mykola (bubelich.com)
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*/

import java.nio.ByteBuffer;
import java.util.Arrays;

public final class Z85 {

    private final static char[] _ALPHA = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ.-:+=^!/*?&<>()[]{}@%$#".toCharArray();

    private final static int[] _RALPHA = {
            68, 0, 84, 83, 82, 72, 0, 75, 76, 70, 65, 0, 63, 62,
            69, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 64, 0, 73, 66, 74, 71,
            81, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
            48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61,
            77, 0, 78, 67, 0, 0, 10, 11, 12, 13, 14, 15, 16, 17, 18,
            19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32,
            33, 34, 35, 79, 0, 80};

    private final static int _RALSHIFT = 33;

    public static String encode(byte[] input) throws RuntimeException {
        if (input == null || input.length == 0) {
            throw new IllegalArgumentException("Input is wrong");
        }

        int length = input.length;
        int index = 0;
        byte[] buff = new byte[4];

        StringBuilder sb = new StringBuilder((input.length * 5 / 4) + 1);

        while (length >= 4) {
            buff[3] = input[index++];
            buff[2] = input[index++];
            buff[1] = input[index++];
            buff[0] = input[index++];
            sb.append(encodeQuarter(buff));
            length -= 4;
        }

        if (length > 0) {
            buff = new byte[length];
            for (int i = length - 1; i >= 0; i--) {
                buff[i] = input[index++];
            }
            sb.append(encodePadding(buff));
        }

        return sb.toString();
    }

    public static byte[] decode(String input) throws RuntimeException {
        if (input == null || input.length() == 0) {
            throw new IllegalArgumentException("Input is wrong");
        }
        int length = input.length();
        int index = 0;
        char[] buff = new char[5];
        ByteBuffer bytebuff = ByteBuffer.allocate((length * 4 / 5));
        while (length >= 5) {
            buff[0] = input.charAt(index++);
            buff[1] = input.charAt(index++);
            buff[2] = input.charAt(index++);
            buff[3] = input.charAt(index++);
            buff[4] = input.charAt(index++);
            bytebuff.put(decodeQuarter(buff));
            length -= 5;
        }

        if (length > 0) {
            char[] padding = new char[length];
            for (int i = 0; i < length; i++) {
                padding[i] = input.charAt(index++);
            }
            bytebuff.put(decodePadding(padding));
        }

        bytebuff.flip();
        if (bytebuff.limit() == 0) {
            throw new RuntimeException("Output is empty");
        }

        return Arrays.copyOf(bytebuff.array(), bytebuff.limit());
    }

    private static char[] encodeQuarter(byte[] data) {
        long value = (data[0] & 0x00000000000000FFL) |
                ((data[1] & 0x00000000000000FFL) << 8) |
                ((data[2] & 0x00000000000000FFL) << 16) |
                ((data[3] & 0x00000000000000FFL) << 24);

        char[] out = new char[5];
        out[0] = _ALPHA[(int) ((value / 0x31C84B1L) % 85)];
        out[1] = _ALPHA[(int) ((value / 0x95EEDL) % 85)];
        out[2] = _ALPHA[(int) ((value / 0x1C39L) % 85)];
        out[3] = _ALPHA[(int) ((value / 0x55L) % 85)];
        out[4] = _ALPHA[(int) ((value) % 85)];
        return out;
    }

    /**
     * Encode padding scheme
     *
     * @param data byte[] Array of length = 4 of data
     * @return char[] Encoded padding
     */
    private static char[] encodePadding(byte[] data) {
        long value = 0;
        int length = (data.length * 5 / 4) + 1;
        char[] out = new char[length];

        switch (data.length) {
            case 3:
                value |= (data[2] & 0x00000000000000FFL) << 16;
            case 2:
                value |= (data[1] & 0x00000000000000FFL) << 8;
        }

        value |= (data[0] & 0x00000000000000FFL);

        switch (data.length) {
            case 3:
                out[3] = _ALPHA[(int) ((value / 0x95EEDL) % 85)];
            case 2:
                out[2] = _ALPHA[(int) ((value / 0x1C39L) % 85)];
        }

        out[1] = _ALPHA[(int) ((value / 0x55L) % 85)];
        out[0] = _ALPHA[(int) ((value) % 85)];

        return out;
    }

    private static byte[] decodeQuarter(char[] data) {
        long value = 0;
        value += _RALPHA[data[0] - _RALSHIFT] * 0x31C84B1L;
        value += _RALPHA[data[1] - _RALSHIFT] * 0x95EEDL;
        value += _RALPHA[data[2] - _RALSHIFT] * 0x1C39L;
        value += _RALPHA[data[3] - _RALSHIFT] * 0x55L;
        value += _RALPHA[data[4] - _RALSHIFT];

        return new byte[]{
                (byte) (value >>> 24),
                (byte) (value >>> 16),
                (byte) (value >>> 8),
                (byte) (value)
        };
    }

    private static byte[] decodePadding(char[] data) {
        long value = 0;
        int length = data.length * 4 / 5;

        switch (data.length) {
            case 4:
                value += _RALPHA[data[3] - _RALSHIFT] * 0x95EEDL;
            case 3:
                value += _RALPHA[data[2] - _RALSHIFT] * 0x1C39L;
            case 2:
                value += _RALPHA[data[1] - _RALSHIFT] * 0x55L;
        }

        value += _RALPHA[data[0] - _RALSHIFT];

        byte[] buff = new byte[length];
        for (int i = length - 1; i >= 0; i--) {
            buff[length - i - 1] = (byte) (value >>> 8 * i);
        }

        return buff;
    }

}

Bei Veröffentlichung fehlte der Copyright Kommentar am Anfang. Die schadhafte Version wurde depubliziert.

Was genau macht der Code?

Nichts spektakuläres. Er codiert 4 Byte binär- in 5 Byte Textdaten. Gebraucht wird das zur Generierung der QR Codes, damit diese nur druckbare ASCII Zeichen enthalten. Unbedingt notwendig wäre das nicht, denn QR Code kann Binärdaten auch direkt speichern, bietet aber den Vorteil, dass die erstellten QR Codes dann leicht, sowohl als Bild, als auch als Text, z.B. per Fax, verschickt werden können.

Wie wichtig ist Z85 für die Funktion von Luca?

Praktisch irrelevant. Der Code sitzt zwar an einer zentralen Stelle, ist aber im Prinzip völlig austauschbar. Dieselbe Funktionalität hätte man genauso gut mit einer Standard Base64 Kodierung umsetzen (oder auch komplett weglassen) können.

Warum wurde dann nicht einfach Base64 genommen?

Hier kann man nur spekulieren.

  • Z85 verspricht eine (leicht) bessere Packrate als Base64, weil ein größerer Zeichensatz (siehe _ALPHA verwendet wird (Base64 verwendet nur Buchstaben, Zahlen, “+” und “/”, sowie “=” als Füllsymbol).
  • Base64 erzeugt (hauptsächlich durch durch das Füllsymbol) ein sehr charakteristisches Muster in den kodierten Daten.

Die bessere Komprimierung ist das schwächere Argument, denn die Übertragung der Bytes in einem QR Code kostet nichts. Es muss immer das gesamte Bild gescannt werden, egal wie hoch sein “Füllstand” ist. Lediglich der “Detailgrad” ändert sich.

Sollte Z85 gewählt worden sein, um die Kodierung zu verschleiern, dann wäre die Absicht den Zugriff auf die Rohdaten zu verhindern, z.B. um Reverse Engineering zu erschweren. Dies wäre ein Fall von Security by Obscurity, verdächtig, und ein Grund, hier nochmal genauer hinzuschauen.

Wo kommt der Code her und was genau ist eigentlich der Vorwurf?

Zunächst einmal, es ist praktisch unmöglich, Apps zu schreiben, ohne auf Open Source Komponenten zurückzugreifen. Irgendwas braucht man immer, was man nicht selbst entwickelt hat oder selbst entwickeln kann. Das ist im Allgemeinen auch kein Problem, den die Idee, unter einer OS Lizenz zu veröffentlichen ist ja gerade, das Andere die eigene Arbeit nutzen können (und hoffentlich auch etwas zurück geben). Das Einbinden kann auf zwei Arten geschehen:

  • Wenn die Komponente vom Entwickler als Artefakt angeboten wird, dann kann sie automatisch über das Buildsystem importiert werden.
  • “Loser” Quellcode muss per Copy&Paste übernommen werden.

Die Z85 Komponente wird nicht direkt als Artefakt für das Buildsystem angeboten, besteht aber aus nur einer einzelnen Datei, weswegen sie von culture4life per Copy&Paste übernommen wurde. Dies ist gängige Praxis und auch so vom Autor (Bubelich Mykola) vorgesehen. Der “große” Fehler besteht nun in zwei “kleinen” Schlampereien:

  1. Die Datei wurde im falschen Pfad (🗋 de/culture4life/luca/util statt 🗋 com/bubelich ) abgelegt, wodurch auch der packagename in der Datei geändert werden musste (von com.bubelich zu com.culture4life.luca.util ).
  2. Der Hinweis auf die Urheberschaft ging verloren (mehr dazu weiter unten).

Damit hat sich culture4life fälschlich als Urheber des Codes ausgegeben (dies entbehrt nicht einer gewissen Ironie, denn mit Smudo hat Luca einen prominenten Fürsprecher, der sich eigentlich mit Urheberrecht auskennen sollte - zumindest lebt er davon).

Wichtig ist an dieser Stelle ganz klar zu sagen, dass culture4life den Z85 Codec in in Luca nutzen darf (mit Segen des Autors) und auch nicht verpflichtet ist, dafür Lizenzzahlungen zu leisten. Die Verfehlung bestand “lediglich” darin, den Autor nicht zu nennen. Die moralische und rechtliche Bewertung werde ich hier nicht kommentieren.

Anmerkung: Der Rechtsbruch ist, streng genommen, nicht damit behoben, eine neue Datei mit intakten Coprightnotice zu veröffentlichen. Die BSD Lizenz fordert:

Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

Die aktuell auf Google Play verfügbare APK Datei erfüllt diese Forderung nicht.

Wie konnte es dazu kommen?

Wenn man sich den von culture4life veröffentlichten Sourcecode anschaut, fallen ein paar Dinge auf:

  • Der früheste Stand in der Versionsgeschichte ist vom 30. März 2021 und bezieht sich auf Version 1.6.1 der App. Wurde da vorher etwa ohne Versionsverwaltung gearbeitet (Spoiler: nein)?!
  • Jede vernünftige IDE fügt normalerweise automatisch einen Kommentar mit den Lizenzbestimmungen am Anfang jeder neuen Quelltextdatei ein. Im Luca Source fehlen diese aber seltsamerweise vollständig.
  • Überhaupt fehlen praktisch überall die Kommentare. Bei keiner einzigen Java Klasse ist beschrieben, was sie eigentlich machen soll.
  • Besonders interessant ist, wenn man die Version von Z85 im Original mit der Version im Lucasource (siehe oben) vergleicht. Im Original finden sich die üblichen Javadoc Kommentare.

Lange Rede, kurzer Sinn, hier ist, was ich glaube, was passiert ist: der Luca Source stand Anfangs unter einer proprietären Lizenz (und hatte entsprechende Copyright Hinweise). Geplant war, lediglich ausgewählten Kreisen Zugang mit Knebelvertrag zu geben, damit die das Abnicken und vielleicht ein (werbewirksames) Prüfsiegel drauf pappen können. Ideal wäre eine Adelung vom CCC gewesen, aber der vergibt nunmal keine Prüfsiegel (sondern bestenfalls nur keinen Daumen runter) und mit NDA läuft da schonmal gar nichts. Also wurde, widerwillig, der Sourcecode aus dem privaten Versionscontrolsystem in ein öffentliches GIT Repository gekippt und dabei drei Fehler gemacht:

  1. Es wurde nur und ausschließlich der Versionsstand 1.6.1 veröffentlicht. Auf Google Play wird aber aktuell noch die ältere Version 1.4.12 angeboten (das Versionsschema legt zwei bedeutende Änderungen nahe). Damit ist ein Code Audit eigentlich schon nicht mehr sinnvoll möglich, da das Ansichtsexemplar nicht dem Verkaufsgegenstand entspricht (aus genau diesem Grund kauft Stiftung Warentest zu testende Produkte anonym ein). Würdigt man noch die weiteren Umstände, wie die “temporäre Lizenz” und das Fehlen des Backendcodes, dann sieht das hier nach einem Täuschungsversuch, bzw. Ablenkungsmanöver aus.
  2. Beim Export von Version 1.6.1 musste die ursprüngliche, proprietäre Lizenzinformation aus den Quelltexten entfernt werden. Dazu wurde anscheinend ein Werkzeug eingesetzt, das einfach rigoros (fast) alle Kommentare gelöscht hat (inklusive Javadoc). Die Tatsache, dass undokumentierter Code schwerer zu lesen ist wurde dabei entweder billigend in Kauf genommen oder es wurde nicht mehr überprüft, dass mehr gelöscht wurde als notwendig war.
  3. Es wurde vergessen, dass sich unter den 97 Quelltext Dateien der App eine Fremddatei befindet, deren Copyright nicht hätte angetastet werden dürfen.

Mit anderen Worten, die Veröffentlichung macht einen eher lieblosen Eindruck und wirkt eher so, als sei es nur darum gegangen, formalen Anforderungen zu genügen.

Fazit

Der Codeklau an sich war ein Versehen von geringem Umfang, eine lässliche Sünde, aber ein gefundenes Fressen für Plagiatsjäger. Wirklich interessant ist erst, was dahinter zum Vorschein kommt.