# Quarantäne Aufgabe: MD5 Funktion in SCL



## Maagic7 (29 März 2020)

Ich verusch mich gerade an einem MD5 Hash in SCL.
Konkretes Ziel ist Temperaturmesstabellen mit einem Fingerprint zu versehen!

Die Beschreibung des 128Bit MD5 Algorithmus ist hier zu finden

https://de.wikipedia.org/wiki/Message-Digest_Algorithm_5


Die Hasthabelle und die Hauptschleife hab ich bereits integriert. Wird zumindest fehlerfrei compiliert!
Wer mitmachen möchte, hier der bisherige Code

Code Update 14.04.2020

```
FUNCTION_BLOCK m7_MD5_128Bit
    TITLE = 'Calculates the 128Bit MD5 Fingerprint of a DataBlock'

// ==============================================================================
//  128Bit MD5 Fingerprint calculation
// ==============================================================================
//
// AUTHOR: S. Maag 
// DATE: 4/2020
//
// CHANGELOG:
// ------------------------------------------------------------------------------
// DATE         NAME        DESCRIPTION
// ------------------------------------------------------------------------------
//   
// ------------------------------------------------------------------------------

    VERSION : '0.2'
    AUTHOR  : 'S.Maag'
    FAMILY  : 'Maagic7'

VAR_INPUT
    Start : BOOL;
    inDataANY : ANY;      // Data/Message to encode
END_VAR

VAR_OUTPUT
    Busy : BOOL;
    Ready : BOOL;
    BlockCount : DINT;

    MD5_a0 : DWORD;
    MD5_b0 : DWORD;
    MD5_c0 : DWORD;
    MD5_d0 : DWORD;
END_VAR

VAR_TEMP

    A,  B,  C,  D  : DWORD;
    F              : DWORD;
    g              : DINT;
    
    gg,kk : DINT;
    ss : INT;
    
    MsgLengthBit   : DINT;      // Länge unser Daten in Bits = Anzahl Bytes *8
 
    pMsgLengthBit AT MsgLengthBit : STRUCT
        HiHiByte : BYTE;
        HiByte   : BYTE;
        LoByte   : BYTE;
        LoLoByte : BYTE;
    END_STRUCT;
    
    I, J : DINT;               // Schleifenvariablen
    dwTmp : DWORD;
    dwMem: DWORD;
    
    buf : ARRAY[0..15] OF DWORD;            // 512-Bit Buffer for Message
    pB_buf AT buf : ARRAY[0..63]  OF BYTE;      // *buf[] : BYTE;
 
    BlkAny : ANY;           // ANY für max 512 Bit /64 Byte aus inDataANY Speicherbereich kopieren, unsere Datenblocks
    pBlkAny AT BlkAny : STRUCT
       ID  : WORD;  // ID für ANY (1002 hex = es wird mit Bytes gearbeitet)
       REP : WORD;   // Wiederholungsfaktor
       DBNo: INT;   // DB der im ANY Pointer benutzt wird ANY
       PTR : DWORD; // Pointer Doppelwort um den Angfang des Datenbereichs und den Datentyp zu definieren (84hex = DB Datentyp)
    END_STRUCT;

    iBytesToCopy : INT;
    iRET        : INT;
    
    xPause : BOOL;    

END_VAR

VAR
    a0, b0, c0, d0 : DWORD;
    diRestBytes  : DINT;
    BlockPointer : DWORD;

    s : ARRAY[0..63] OF INT;
    k : ARRAY[0..63] OF DWORD;
  
    xInit : BOOL;
  
    xAddHiBit   : BOOL;
    xHiBitAdded : BOOL;
    xLastBlock  : BOOL;
    oldStart    : BOOL;
END_VAR

CONST
    cst_BlocksPerCycle := 10;        // Anzahl der zu berechnenden Blocks pro SPS Zyklus
END_CONST


BEGIN

// allein die Initialisierung der beiden ARRAYs sind 1kB an Code
// die Arrays könnte man noch in einen DB auslagern und an den FC übergeben

    // ======================================================================
    //    Initialisierungen
    // ======================================================================

    IF NOT xInit THEN
        // ROL ARRAY, Anzahl der zu rotierenden Bits
        s[0]:= 7; s[4]:= 7;  s[8]:= 7; s[12]:= 7;
        s[1]:=12; s[5]:=12;  s[9]:=12; s[13]:=12;
        s[2]:=17; s[6]:=17; s[10]:=17; s[14]:=17;
        s[3]:=22; s[7]:=22; s[11]:=22; s[15]:=22;
        
        s[16]:= 5; s[20]:= 5; s[24]:= 5; s[28]:= 5;
        s[17]:= 9; s[21]:= 9; s[25]:= 9; s[29]:= 9;
        s[18]:=14; s[22]:=14; s[26]:=14; s[20]:=14;
        s[19]:=20; s[23]:=20; s[27]:=20; s[31]:=20;
        
        s[32]:= 4; s[36]:= 4; s[40]:= 4; s[44]:= 4;
        s[33]:=11; s[37]:=11; s[41]:=11; s[45]:=11;
        s[34]:=16; s[38]:=16; s[42]:=16; s[46]:=16;
        s[35]:=23; s[39]:=23; s[43]:=23; s[47]:=23;
        
        s[48]:= 6; s[52]:= 6; s[56]:= 6; s[60]:= 6;
        s[49]:=10; s[53]:=10; s[57]:=10; s[61]:=10;
        s[50]:=15; s[54]:=15; s[58]:=15; s[62]:=15;
        s[51]:=21; s[54]:=21; s[59]:=21; s[63]:=21;
    
        k[0] := DW#16#D76AA478; k[1] := DW#16#E8C7B756; k[2] := DW#16#242070DB; k[3] := DW#16#C1BDCEEE;
        k[4] := DW#16#F57C0FAF; k[5] := DW#16#4787C62A; k[6] := DW#16#A8304613; k[7] := DW#16#FD469501;
        k[8] := DW#16#698098D8; k[9] := DW#16#8B44F7AF; k[10]:= DW#16#FFFF5BB1; k[11]:= DW#16#895CD7BE;
        k[12]:= DW#16#6B901122; k[13]:= DW#16#FD987193; k[14]:= DW#16#A679438E; k[15]:= DW#16#49B40821;
        k[16]:= DW#16#F61E2562; k[17]:= DW#16#C040B340; k[18]:= DW#16#265E5A51; k[19]:= DW#16#E9B6C7AA;
        k[20]:= DW#16#D62F105D; k[21]:= DW#16#02441453; k[22]:= DW#16#D8A1E681; k[23]:= DW#16#E7D3FBC8;
        k[24]:= DW#16#21E1CDE6; k[25]:= DW#16#C33707D6; k[26]:= DW#16#F4D50D87; k[27]:= DW#16#455A14ED;
        k[28]:= DW#16#A9E3E905; k[29]:= DW#16#FCEFA3F8; k[30]:= DW#16#676F02D9; k[31]:= DW#16#8D2A4C8A;
        k[32]:= DW#16#FFFA3942; k[33]:= DW#16#8771F681; k[34]:= DW#16#6D9D6122; k[35]:= DW#16#FDE5380C;
        k[36]:= DW#16#A4BEEA44; k[37]:= DW#16#4BDECFA9; k[38]:= DW#16#F6BB4B60; k[39]:= DW#16#BEBFBC70;
        k[40]:= DW#16#289B7EC6; k[41]:= DW#16#EAA127FA; k[42]:= DW#16#D4EF3085; k[43]:= DW#16#04881D05;
        k[44]:= DW#16#D9D4D039; k[45]:= DW#16#E6DB99E5; k[46]:= DW#16#1FA27CF8; k[47]:= DW#16#C4AC5665;
        k[48]:= DW#16#F4292244; k[49]:= DW#16#432AFF97; k[50]:= DW#16#AB9423A7; k[51]:= DW#16#FC93A039;
        k[52]:= DW#16#655B59C3; k[53]:= DW#16#8F0CCC92; k[54]:= DW#16#FFEFF47D; k[55]:= DW#16#85845DD1;
        k[56]:= DW#16#6FA87E4F; k[57]:= DW#16#FE2CE6E0; k[58]:= DW#16#A3014314; k[59]:= DW#16#4E0811A1;
        k[60]:= DW#16#F7537E82; k[61]:= DW#16#BD3AF235; k[62]:= DW#16#2AD7D2BB; k[63]:= DW#16#EB86D391;
        
        xInit := TRUE;
    END_IF;

    BlkAny := inDataANY;    // Eingang ANY-Pointer auf Datenquelle => TEMP

    IF Start AND (NOT Busy) AND (NOT Ready) THEN
        // Initialisiere die Variablen: (lt. RFC 1321)
        a0 := DW#16#67452301;
        b0 := DW#16#EFCDAB89;
        c0 := DW#16#98BADCFE;
        d0 := DW#16#10325476;
    
        // ======================================================================
        //    Vorberechnungen
        // ======================================================================
    
        // wir müssen die Daten 512-Bit/64 Byte weise in unseren buf[] kopieren
        // der letzte Block muss ein Hi-Bit angehängt werden, dann so weit erweitert dass,
        // 64 Bit für die Blocklänge in Bits überig bleiben. Evtl. muss ein leerer
        // Block angehängt werden      
            
        // für alle 512-Bit Block von message, buf
    
        diRestBytes := WORD_TO_DINT(pBlkAny.REP);       // REP gibt den Widerholungsfaktor an, da ANY immer BYTE ist = Anzahl Bytes
        BlockPointer := pBlkAny.PTR;
        BlockCount := 0;

        MsgLengthBit := diRestBytes*8;
        
        xLastBlock := FALSE;
        xAddHiBit := FALSE;   // Hi-Bit muss noch an MSG angehängt werden
        xHiBitAdded := FALSE;
        Busy := TRUE;

    ELSIF NOT Start AND Busy THEN
        Busy := FALSE;
    ELSIF Start AND NOT oldSTART THEN   // RTrig(START)
        Ready := FALSE;
    ELSIF NOT Start AND Ready THEN  
        Ready := FALSE;
    END_IF;
   
    oldStart := START;
    
    // ======================================================================
    //    Hauptschleife
    // ======================================================================
    
    IF Busy THEN
 
        REPEAT
            BlockCount := BlockCount + 1;
  
            // Prüfen wieviele Bytes noch kopiert werden können
            IF diRestBytes > 64 THEN         // kompletter 64 Byte Block
                iBytesToCopy := 64;
            ELSIF diRestBytes > 59 THEN      // > 59 Bytes, dann reicht Block nicht um unsere 5 Byte hinten anzuhängen 
                iBytesToCopy := DINT_TO_INT(diRestBytes);
                xAddHiBit := TRUE;          // Hi-Bit am Ende anfügen und Bufffer[] zuerst löschen
            ELSE                            // Bkock ist komplett leer oder es reicht unsere 5 Byte Daten anzuhängen
                xLastBlock := TRUE;
                iBytesToCopy := DINT_TO_INT(diRestBytes);    // das kann minimal 0 werden! (würde aber auch funktioneren falls <0)
                xAddHiBit := TRUE;             // Achtung: muss unbedingt hier nochmal Hi gesetzt werden, da benutzt für Buffer[]:=0
            END_IF;
      
            diRestBytes := diRestBytes - INT_TO_DINT(iBytesToCopy);       // verbleibende Bytes -64  (wird minimal 0)
          
            IF xAddHiBit THEN       // Wenn letzter oder vorletzter Block, dann sind evtl. nicht alle 64 Bytes belegt => zuerst löschen
                FOR J:=0 TO 15 DO
                     buf[J] := 0;    // Buffer vorher löschen, da er nicht mehr vollständig mit Daten belegt wird
                END_FOR;
            END_IF;
            
            // Daten von Quelle die mit inDataANY spezifiziert sind nach buf[] kopieren
            IF iBytesToCopy >0 THEN
                pBlkAny.REP := INT_TO_WORD(iBytesToCopy);          // Wiederholungsfaktor im Quell-ANY
                pBlkAny.PTR := BlockPointer;
                iRET:= BLKMOV(SRCBLK := BlkAny, DSTBLK := buf); // SFC20 BLKMOV
                BlockPointer := DINT_TO_DWORD(DWORD_TO_DINT(BlockPointer) + (iBytesToCopy*8));  // BlockPointer um 64 Byte weiter
            END_IF;
    
            IF xAddHiBit AND NOT xHiBitAdded THEN
                pB_buf[iBytesToCopy] := 1;
                xAddHiBit := TRUE;
                xHiBitAdded := TRUE;
            END_IF;
    
            IF xLastBlock THEN
                // MsgLengthBit in Buffer letzte 64 bit eintragen ACHTUNG little Endian (Intel Format)
                // wir haben nur 32 Bit. Der Rest bleibt 0
                pB_buf[63]:=pMsgLengthBit.HiHiByte;  // gedrehte ByteReihenfolge verwenden
                pB_buf[62]:=pMsgLengthBit.HiByte;
                pB_buf[61]:=pMsgLengthBit.LoByte;
                pB_buf[60]:=pMsgLengthBit.LoLoByte;
            END_IF;
        
        // Initialisiere den Hash-Wert für diesen Block:
            A := a0;
            B := b0;
            C := c0;
            D := d0;
        
        // Hauptschleife:
        // not Operator entspricht dem Einerkomplement
        // für alle i von 0 bis 63
            FOR I:=0 TO 63 DO
    
                IF I < 16 THEN          // 0..15
                    F := (B AND C) OR ((NOT B) AND D);
                //    F := D XOR (B AND (C XOR D));  // Alternativ, soll schneller sein. Stimmt aber nicht, ist tats. 2-3ms langsamer
                  //    g := I;
                     gg :=DWORD_TO_DINT(buf[I]);
      
                ELSIF I < 32 THEN       // 16..31
                     F := (B AND D) OR (C AND (NOT D));
                  //  g := (5*I + 1) MOD 16; // MOD 16 müsste durch AND DW#16#F zu erstzen sein
    
                  //   F := C XOR (D AND (B XOR C));             // Alternativ, weil schneller. Auch knapp 1ms langsamer
                     g := DWORD_TO_DINT(DINT_TO_DWORD(5*I + 1) AND DW#16#F); 
                    gg :=DWORD_TO_DINT(buf[g]);
    
                ELSIF I < 48 THEN       // 32..47
                    F := B XOR C XOR D;
                 //   g := (3*I + 5) MOD 16;
                    g := DWORD_TO_DINT(DINT_TO_DWORD(3*I + 5) AND DW#16#F);
                    gg :=DWORD_TO_DINT(buf[g]);
     
                ELSE                    // 48..63
                    F := C XOR (B OR (NOT D));
                   // g := (7*I) MOD 16;
                    g := DWORD_TO_DINT(DINT_TO_DWORD(7*I) AND DW#16#F);
                    gg :=DWORD_TO_DINT(buf[g]);  // gg in allen ELSE-Zweigen berechnen ist schneller als 1x ausserhalb
                END_IF;
                
                // die Werte für die Linksrotoation vorberechnen ist schneller als das direkt im ROL Befehl einzubinden
                kk := gg + DWORD_TO_DINT(k[I]) + DWORD_TO_DINT(A) + DWORD_TO_DINT(F);
                ss := s[I];  // Anzahl Bits schieben
                
                dwTmp := D;
                D := C;
                C := B;
    
            //  B := B + linksrotation((A + F + K[i] + buf[g]), s[i])
     //           dwMem := ROL (IN:= DINT_TO_DWORD(DWORD_TO_DINT(A)+DWORD_TO_DINT(F)+DWORD_TO_DINT(k[I])+DWORD_TO_DINT(buf[g])), N:=s[I]);
                dwMem := ROL (IN:= DINT_TO_DWORD(kk), N:=ss);
     
                B := DINT_TO_DWORD( DWORD_TO_DINT(B) + DWORD_TO_DINT(dwMem) );
    
                A := dwTmp;
     
            END_FOR;
        
            // Addiere den Hash-Wert des Blocks zur Summe der vorherigen Hashes:
            a0 := DINT_TO_DWORD(DWORD_TO_DINT(a0) + DWORD_TO_DINT(A));
            b0 := DINT_TO_DWORD(DWORD_TO_DINT(b0) + DWORD_TO_DINT(B));
            c0 := DINT_TO_DWORD(DWORD_TO_DINT(c0) + DWORD_TO_DINT(C));
            d0 := DINT_TO_DWORD(DWORD_TO_DINT(d0) + DWORD_TO_DINT(D));
            
            xPause := (BlockCount MOD cst_BlocksPerCycle) = 0 ;
        //    xPause := FALSE;
        UNTIL xLastBlock OR xPause END_REPEAT;
  
    END_IF;  // IF Busy

    // ======================================================================
    //   Rückgabewerte
    // ======================================================================
    IF xLastBlock THEN
        xLastBlock := FALSE;
        Ready := TRUE;
        Busy := FALSE;
        MD5_a0 := a0;
        MD5_b0 := b0;
        MD5_c0 := c0;
        MD5_d0 := d0;
    END_IF;

END_FUNCTION_BLOCK
```


----------



## Heinileini (30 März 2020)

> g := (5*I + 1) MOD 16; // MOD 16 müsste durch AND DW#16#F zu ersetzen sein
> //  g := DWORD_TO_DINT(DINT_TO_DWORD(5*I + 1) AND DW#16#F);


Das sehe ich auch so.
Die Berechnungen von g könnte man durch ein Array mit 64 Konstanten ersetzen - ist vielleicht "performanter"?


> ELSIF I < 64 THEN


Einfach nur ELSE tut's auch, da 0 <= I <= *63*.


----------



## PN/DP (30 März 2020)

Ich würde den Code in einen FB packen und mindestens die Arrays s und t in die Instanz (Static) legen.
Die Abarbeitung des Codes wird möglicherweise auf mehrere SPS-Zyklen aufgeteilt werden müssen - ein FB ist besser zum Merken von Zwischenergebnissen.
In Static ist die Initialisierung der Arrays bei Deklaration als Liste möglich.
Für einige CPUs ist Lokaldatenspeicher-Bedarf > 500 Byte schon heftig. Der kann als FB stark verringert werden.

Harald


----------



## Maagic7 (30 März 2020)

An einen FB hab ich auch schon gedacht, hab das dann aber erstmal verworfen, da das eigentlich eine Funktion ist.
Dass man das aber evtl. auf mehrere Zyklen aufteilen könnte darauf bin ich noch gar nicht gekommen!

Man muss mal sehen wie lange das dann dauert, wenn man mal ein 16K DB hat.
Da bin ich echt gespannt!


----------



## Maagic7 (14 April 2020)

Mittlerweile hab ich das weiterentwickelt und es läuft schon mal.
Leider hab ich im Moment noch keine Ahnung wie ich das auf Richtigkeit testen soll/kann.

Die Laufzeit bei langen DB's ist kritisch! Als FC hat das 112ms für 64kB DB auf einer VIPA SLIO 015 gebracht.
Der gleiche Code nur als FB war noch langsamer! (um 17ms glaub ich)

Deshalb hab ich das jetzt auf einen FB geändert und immer nur 10 Blocks a' 64 Byte in einem Zyklus bearbeitet.
Laufzeit ca 112us pro SPS Zyklus. Für die vollen 64kB sind dann 1025 Durchlaeufe erforderlich.

Hier das Step7 Classic Testporjekt. Vielleicht kann mal jemand überprüfen, ob der MD5 Wert korekt ist!
Ich bekomme für eine 64k Datenblock, welcher mit 1..32767 als 16-Bit Int gefüllt ist

MD5_a0 : DW#16# DF A0 0B 46
MD5_b0 : DW#16# 65 23 A6 41
MD5_c0  : DW#16# 37 0E 41 96
MD5_d0 : DW#16# 54 72 96 A4

Bei S7 ist das Datenformat big-Endian, bei PC's little-Endian d.h. schon deswegen kann ich das
so wahrscheinlich nicht verlgeichen, wenn ich gleiches auf irgendeinem PC Program durchrechne.

Anhang anzeigen Md5.zip


----------



## Thomas_v2.1 (14 April 2020)

Maagic7 schrieb:


> Leider hab ich im Moment noch keine Ahnung wie ich das auf Richtigkeit testen soll/kann.



Am Ende der zugehörigen RFC 1321 sind unter "A.5 Test suite" ein paar Testfälle die du prüfen kannst:
https://tools.ietf.org/html/rfc1321


----------



## PN/DP (14 April 2020)

Die Windows PowerShell ab V4.0 kann MD5 berechnen mit get-filehash


> For security reasons, MD5 and SHA1, which are no longer considered secure, should only be used for simple change validation, and should not be used to generate hash values for files that require protection from attack or tampering.


Wie bestimmen Sie die Prüfsumme einer Datei?

Der Gockel findet auch jede Menge Tools zum "md5 berechnen" (sogar Code mit Excel VBA), z.B. WinMD5Free

Harald


----------

