Kryptering i python, är jag säker?

Permalänk
Medlem

Kryptering i python, är jag säker?

Hej,

jag håller på lära mig Python och försöker hitta på diverse projekt. Det här är mitt försök på något som krypterar text givet en nyckel.
Är det någon som har synpunkter på koden och eller hur säkert man kan säga att ett sådant här krypto är, nu tänker jag ju att det går välja en mer komplicerad nyckel än "12"

import random def cipher(txt, key): text_as_number = [] text_cipher = [] random.seed(key) cipher_list = random.sample(range(500), len(txt)) for letter in txt: text_as_number.append(ord(letter)) for number in [a + b for a, b in zip(text_as_number, cipher_list)]: text_cipher.append(chr(number)) return ''.join(text_cipher) def decrypt(txt, key): text_as_number = [] text_cipher = [] random.seed(key) cipher_list = random.sample(range(500), len(txt)) for letter in txt: text_as_number.append(ord(letter)) for number in [a - b for a, b in zip(text_as_number, cipher_list)]: text_cipher.append(chr(number)) return ''.join(text_cipher) text_krypterad = cipher("Hej på er alla, hur säkert är det här egentligen?", 12) text_dekrypterad = decrypt(text_krypterad, 12) print (text_krypterad) print (text_dekrypterad)

Visa signatur

Intel Core i5-3570K Processor || AMD Radeon HD 7970 || MSI Z77A-G45 (MS-7752)
3DMark06 30 781, SM2.0 Score - 11982 - HDR/SM3.0 Score 15632 - CPU Score 7631

Permalänk
Medlem

@MrMacho:

Om du har tänkt att faktiskt använda det för skydda något så är det nog bäst att inte rekommendera det. Det finns fina exempel på företag som har använt proprietary ciphers som de trott vara säkra, som sedan blivit knäckta.

Permalänk
Medlem

Det du har är en simpel stream cipher med de sårbarheter som är vanliga i stream ciphers.

import random funktionerna i python är inte kryptografisk säkra. random.sample använder ej en cryptographically secure pseudorandom number generator (CSPRNG). Låt oss säga att en attacker kan gissa att meddelandet börjar med "Hej på er alla". Attackern kan då beräkna vissa av värdena i cipher_list som genererades mha random.sample. Med de värdena skulle det kunna vara möjligt att köra random.sample algoritmen baklänges och få reda på vilket värde som användes som seed. Med seed värdet går det beräkna alla värden i cipher_list och dekryptera hela meddelandet. En CSPRNG algoritm är designad så att det är omöjligt att köra beräkningen baklänges och få reda på mer output än vad attackern redan har. Att gissa delar av ett krypterat meddelande är i praktiken ganska enkelt för de flesta krypterade meddelanden följer något protokoll (handshake) som är standardiserat.

Ett annat potentiellt problem är hur mycket entropi random.seed använder. Om du använder en lång krånglig nyckel på 512-bit men random.seed bara har ett internt state på 32-bit är algoritmen ändå trivial att bruteforcea. Jag vet inte hur mycket state pythons seed har men eftersom den inte är till för kryptografi skulle jag inte bli förvånad om det är 32-bit. Som exempel tar C funktionen srand() en unsigned int som vanligtvis bara är 32-bit.

Ett annat problem med stream ciphers är att man aldrig kan använda samma nyckel flera gånger. Låt oss säga att vi krypterar två meddelande med samma nyckel K. Plaintext är P1 och P2 och ciphertext C1 och C2.
kryptering:
P1 + K = C1
P2 + K = C2
avkryptering:
C1 - K = (P1 + K) - K = P1
C2 - K = (P2 + K) - K = P2
En attacker har tillgång till både C1 och C2.
C1 - C2 = (P1 + K) - (P2 + K) = P1 - P2
Attackern kan alltså strippa av K och få ut P1 - P2. Det är inte plaintext men det går ofta gissa vad plaintext är. Många protokoll har null bytes och om P2 är 0 får man direkt ut P1. Här är ett bildexempel som visar saken bättre https://i.imgur.com/PKYvfrX.jpg
Anledningen till att jag tar upp återanvändning av nycklar är att om seed bara är 32-bit så finns det som max bara 2^32 = 4294967296 interna state i PRNGn och cipher_list kommer börja upprepa sig efter det. Med korta meddelanden är det ingen problem men krypterar man några gig så upprepas värdena i cipher_list och det är enkelt att knäcka kryptot.

Dessa problem är generella för stream cipher men din implementation är lite annorlunda eftersom du använder plus och minus i stället för xor vilket är vanligare. Xor används oftare eftersom man inte behöver oroa sig för carry om additionen är för stor och med xor är kryptering och avkryptering samma algoritm eftersom a xor b = c, c xor b = a.

Var fick du siffran 500 i range(500) ifrån? chr() och ord() arbetar med unicode med värden i range 0..1114111 (ca 20-bit) men du adderar bara med värden i range 0..500 (knappt 9-bit). Det försvagar krypteringen enormt. Låt oss säga att en ciphertext har tecknet "俴". ord("俴") = 20468 så vi vet att det motsvarande plaintext tecknet har ett unicode värde i spannet 19968..20468. I det spannet finns 501 kinesiska/japanska tecken så man kan vara säker på att meddelandet inte är på svenska. Du måste se till så att alla tecken i spannet som chr() / ord() arbetar med förekommer i ciphertexten med lika stor sannolikhet. Även om du bara använder standard ascii tecken i meddelandet så försvagas ändå krypteringen av att du använder range(500). Standard ascii är 7-bit (0..127) så ciphertexten borde då har värden i spannet 0..127 i lika stor sannolikhet. Normalt vis arbetar kryptografiska algoritmer på bytes, inte characters eftersom det är enklare.

Min pyton-fu är vek så det finns troligen många andra problem jag inte ser direkt.

Permalänk
Medlem
Skrivet av Emaku:

Det du har är en simpel stream cipher med de sårbarheter som är vanliga i stream ciphers.

import random funktionerna i python är inte kryptografisk säkra. random.sample använder ej en cryptographically secure pseudorandom number generator (CSPRNG). Låt oss säga att en attacker kan gissa att meddelandet börjar med "Hej på er alla". Attackern kan då beräkna vissa av värdena i cipher_list som genererades mha random.sample. Med de värdena skulle det kunna vara möjligt att köra random.sample algoritmen baklänges och få reda på vilket värde som användes som seed. Med seed värdet går det beräkna alla värden i cipher_list och dekryptera hela meddelandet. En CSPRNG algoritm är designad så att det är omöjligt att köra beräkningen baklänges och få reda på mer output än vad attackern redan har. Att gissa delar av ett krypterat meddelande är i praktiken ganska enkelt för de flesta krypterade meddelanden följer något protokoll (handshake) som är standardiserat.

Ett annat potentiellt problem är hur mycket entropi random.seed använder. Om du använder en lång krånglig nyckel på 512-bit men random.seed bara har ett internt state på 32-bit är algoritmen ändå trivial att bruteforcea. Jag vet inte hur mycket state pythons seed har men eftersom den inte är till för kryptografi skulle jag inte bli förvånad om det är 32-bit. Som exempel tar C funktionen srand() en unsigned int som vanligtvis bara är 32-bit.

Ett annat problem med stream ciphers är att man aldrig kan använda samma nyckel flera gånger. Låt oss säga att vi krypterar två meddelande med samma nyckel K. Plaintext är P1 och P2 och ciphertext C1 och C2.
kryptering:
P1 + K = C1
P2 + K = C2
avkryptering:
C1 - K = (P1 + K) - K = P1
C2 - K = (P2 + K) - K = P2
En attacker har tillgång till både C1 och C2.
C1 - C2 = (P1 + K) - (P2 + K) = P1 - P2
Attackern kan alltså strippa av K och få ut P1 - P2. Det är inte plaintext men det går ofta gissa vad plaintext är. Många protokoll har null bytes och om P2 är 0 får man direkt ut P1. Här är ett bildexempel som visar saken bättre https://i.imgur.com/PKYvfrX.jpg
Anledningen till att jag tar upp återanvändning av nycklar är att om seed bara är 32-bit så finns det som max bara 2^32 = 4294967296 interna state i PRNGn och cipher_list kommer börja upprepa sig efter det. Med korta meddelanden är det ingen problem men krypterar man några gig så upprepas värdena i cipher_list och det är enkelt att knäcka kryptot.

Dessa problem är generella för stream cipher men din implementation är lite annorlunda eftersom du använder plus och minus i stället för xor vilket är vanligare. Xor används oftare eftersom man inte behöver oroa sig för carry om additionen är för stor och med xor är kryptering och avkryptering samma algoritm eftersom a xor b = c, c xor b = a.

Var fick du siffran 500 i range(500) ifrån? chr() och ord() arbetar med unicode med värden i range 0..1114111 (ca 20-bit) men du adderar bara med värden i range 0..500 (knappt 9-bit). Det försvagar krypteringen enormt. Låt oss säga att en ciphertext har tecknet "俴". ord("俴") = 20468 så vi vet att det motsvarande plaintext tecknet har ett unicode värde i spannet 19968..20468. I det spannet finns 501 kinesiska/japanska tecken så man kan vara säker på att meddelandet inte är på svenska. Du måste se till så att alla tecken i spannet som chr() / ord() arbetar med förekommer i ciphertexten med lika stor sannolikhet. Även om du bara använder standard ascii tecken i meddelandet så försvagas ändå krypteringen av att du använder range(500). Standard ascii är 7-bit (0..127) så ciphertexten borde då har värden i spannet 0..127 i lika stor sannolikhet. Normalt vis arbetar kryptografiska algoritmer på bytes, inte characters eftersom det är enklare.

Min pyton-fu är vek så det finns troligen många andra problem jag inte ser direkt.

Tack för era svar NoToes och Emaku.

Jag förstod att det inte skulle vara säkert-säkert, men det verkar ju som att det inte ens är säkert

Nästa steg var att jag tänkte göra ett program som håller reda på alla inloggningar jag har spridda runt om på nätet och tyckte det verkade lite osäkert att samla allt i vanlig plain-text. Jag har ännu inte tillräckliga kunskaper i Python för att kunna anropa något externt krypteringsprogram för att sköta krypteringen. Det får nog bli så att jag gör allting annat klart först och kanske har jag plockat upp tillräckligt med kunskap under tiden för att göra den implementeringen.

Eller finns det någon variant av krypto som en hemmasnickrare som mig kan använda sig av?

edit: angående att jag använde mig av range(500) var att jag fick några felmeddelanden och under felsökningen valde jag att dra ner på storleken så att jag enklare skulle kunna se vad som hände. Men tanken från början var att använda full range av unicode.

Visa signatur

Intel Core i5-3570K Processor || AMD Radeon HD 7970 || MSI Z77A-G45 (MS-7752)
3DMark06 30 781, SM2.0 Score - 11982 - HDR/SM3.0 Score 15632 - CPU Score 7631

Permalänk
Medlem

@MrMacho: Just vid kryptering, använd alltid något som är beprövat. Nu vet jag inte hur väl beprövat just pycrypto är, men där används vanliga krypteringsalgoritmer som AES och RSA.

Visa signatur

Jag är en optimist; det är aldrig så dåligt så att det inte kan bli sämre.

Permalänk
Medlem
Skrivet av zyberzero:

@MrMacho: Just vid kryptering, använd alltid något som är beprövat. Nu vet jag inte hur väl beprövat just pycrypto är, men där används vanliga krypteringsalgoritmer som AES och RSA.

Ja det där verkar ju onekligen enklare, tackar

Visa signatur

Intel Core i5-3570K Processor || AMD Radeon HD 7970 || MSI Z77A-G45 (MS-7752)
3DMark06 30 781, SM2.0 Score - 11982 - HDR/SM3.0 Score 15632 - CPU Score 7631

Permalänk
Hedersmedlem

Slänger in en generell notis gällande listomfattningar (som jag alldeles nyss bestämde var en bra svensk översättning av "list comprehensions" ): när man i Python ser kodbitar som initierar en tom vektor och sedan fyller på den elementvis så kan man nästan garanterat ersätta det med en listomfattning, och bli gladare på kuppen (garanterat!). Det ger mindre kod som tydligare beskriver intentionen.

Exempelvis kan kodbiten:

Skrivet av MrMacho:

text_as_number = [] for letter in txt: text_as_number.append(ord(letter))

mer Pythonskt skrivas som:

text_as_number = [ord(letter) for letter in txt]

vilket visar poängen med konstruktionen.

På samma sätt så ser vi att du senare använder en listomfattning att skapa en temporär lista att iterera över för att sedan fylla på en tomt initierad vektor:

Skrivet av MrMacho:

text_cipher = [] … for number in [a + b for a, b in zip(text_as_number, cipher_list)]: text_cipher.append(chr(number))

där du skulle kunna hoppa över ett steg och skriva:

text_cipher = [chr(a + b) for a, b in zip(text_as_number, cipher_list)]

Här ser vi också att enda användningen för text_as_number är att iterera över den en gång, varpå vi snarare borde valt ett generatoruttryck till att börja med. På samma sätt ser vi att cipher_list också bara används för att låta strängens join-metod iterera över den en gång, så med detta i åtanke kan funktionen i sin helhet skrivas:

def cipher(txt, key): text_as_number = (ord(letter) for letter in txt) random.seed(key) cipher_list = random.sample(range(500), len(txt)) text_cipher = (chr(a + b) for a, b in zip(text_as_number, cipher_list)) return ''.join(text_cipher)

Skulle du av någon anledning behöva mata in stora datamängder (men man kan notera att du i nuvarande form får ett ValueError-undantag för strängar längre än 500 tecken) så kommer detta både gå fortare och spara minne, men prestanda är en sidonotis: framför allt så uttrycker det vad du försöker göra mer koncist.

En allmän utflykt om läsbarhet och prestanda: det förstnämnda ska i första hand premieras före prestanda (förrän man vet att det behövs), men vad som är mest "läsbart" bygger ofta (inte alltid) på vad som också är bäst i prestandasynpunkt, eftersom dessa lösningar har en förmåga att bli praxis. En vettig definition på "läsbar kod" är kod som inte ska överraska någon som är van vid språket i fråga. Eftersom någon som är van vid Python bör ha listomfattningar och generatoruttryck i ryggmärgen så är de att föredra framför den mindre koncisa och mer resurskrävande metoden med att lägga till element för element i en loop. Någon som är van vid Python och ser en initialisering av en tom lista tänker automatiskt: "ojdå, här händer nog något märkligt snart eftersom kodaren valde denna metod" vilket gör koden tyngre att läsa (speciellt när det visar sig att inget vidare märkligt händer).

Mycket snack om en liten detalj, kan tyckas, men den är inte så liten som den ser ut, och dessutom ansamlas små detaljer lätt till större härken av svårtydd kod.


När vill man inte skriva listomfattningar? Oftast när elementoperationen är mer komplex än att den går att uttrycka i ett eller ett par korta funktionsanrop, men i mina ögon bör man då definiera en separat funktion som beskriver denna elementoperation och likväl ha tilldelningen som en listomfattning eller ett generatoruttryck. Om operationen är komplex nog för att inte gå att sammanfatta enkelt så bör den lyftas ut.

Ett annat alternativ som kan kännas mer bekant för någon som kommer från andra språk är map, vilket skulle användas som:

text_as_number = map(ord, txt)

Att hävda att det relativt sett är "ovanligt" att se detta i Python är måhända ett starkt ställningstagande, men jag ställer mig på Alex Martellis axlar och hävdar detta likväl. Listomfattningar mer flexibla och vanligare, och därmed enligt någon definition mer "läsbara".


Det är ofta bra att med tydliga variabelnamn skriva koden så att den "förklarar sig själv", vilket nuvarande kod gör bättre, men känns saker väldigt självklara så skulle man kunna överväga en variant som:

def cipher(txt, key): random.seed(key) cipher_list = random.sample(range(500), len(txt)) text_cipher = (chr(ord(a) + b) for a, b in zip(txt, cipher_list)) return ''.join(text_cipher)

eller kanske till och med bara:

def cipher(txt, key): random.seed(key) cipher_list = random.sample(range(500), len(txt)) return ''.join(chr(ord(a) + b) for a, b in zip(txt, cipher_list))

Att baka in även cipher_list-konstruktionen direkt i listomfattningen vore nog att gå väl långt, men kort kod kan vara bra så länge som den fortsätter vara uttrycksfull.


Vill man gå mer överbord så kan man känna hur det kliar i DRY-nerven när det enda som skiljer decrypt och cipher (som väl borde heta encrypt) åt är ett enstaka operatoranrop och titta på förslagsvis operator och functools.partial för att lösa detta.

Visa signatur

Nu med kortare användarnamn, men fortfarande bedövande långa inlägg.

Permalänk
Medlem
Skrivet av MrMacho:

Tack för era svar NoToes och Emaku.

Jag förstod att det inte skulle vara säkert-säkert, men det verkar ju som att det inte ens är säkert

Nästa steg var att jag tänkte göra ett program som håller reda på alla inloggningar jag har spridda runt om på nätet och tyckte det verkade lite osäkert att samla allt i vanlig plain-text. Jag har ännu inte tillräckliga kunskaper i Python för att kunna anropa något externt krypteringsprogram för att sköta krypteringen. Det får nog bli så att jag gör allting annat klart först och kanske har jag plockat upp tillräckligt med kunskap under tiden för att göra den implementeringen.

Eller finns det någon variant av krypto som en hemmasnickrare som mig kan använda sig av?

edit: angående att jag använde mig av range(500) var att jag fick några felmeddelanden och under felsökningen valde jag att dra ner på storleken så att jag enklare skulle kunna se vad som hände. Men tanken från början var att använda full range av unicode.

vill du kryptera text är det bara använda gpg. Där kan du spara varje lösenord som en textfil för att sedan bara kryptera det med gpg. Det du skulle kunna göra är sedan att hålla reda på och sortera dessa för användaren. Dock finns det redan projekt på detta
http://www.passwordstore.org/

Visa signatur

Arch - Makepkg, not war -||- Gigabyte X570 Aorus Master -||- GSkill 64GiB DDR4 14-14-15-35-1T 3600Mhz -||- AMD 5900x-||- Gigabyte RX6900XT -||- 2x Adata XPG sx8200 Pro 1TB -||- EVGA G2 750W -||- Corsair 570x -||- O2+ODAC-||- Sennheiser HD-650 -|| Boycott EA,2K,Activision,Ubisoft,WB,EGS
Arch Linux, one hell of a distribution.

Permalänk
Medlem

Hacking Secret Ciphers with Python om du vill läsa mer om lite kryptering i python.

Permalänk
Medlem
Skrivet av phz:

Slänger in en generell notis gällande listomfattningar (som jag alldeles nyss bestämde var en bra svensk översättning av "list comprehensions" ): när man i Python ser kodbitar som initierar en tom vektor och sedan fyller på den elementvis så kan man nästan garanterat ersätta det med en listomfattning, och bli gladare på kuppen (garanterat!). Det ger mindre kod som tydligare beskriver intentionen.

Exempelvis kan kodbiten:
mer Pythonskt skrivas som:

text_as_number = [ord(letter) for letter in txt]

vilket visar poängen med konstruktionen.

På samma sätt så ser vi att du senare använder en listomfattning att skapa en temporär lista att iterera över för att sedan fylla på en tomt initierad vektor:
där du skulle kunna hoppa över ett steg och skriva:

text_cipher = [chr(a + b) for a, b in zip(text_as_number, cipher_list)]

Här ser vi också att enda användningen för text_as_number är att iterera över den en gång, varpå vi snarare borde valt ett generatoruttryck till att börja med. På samma sätt ser vi att cipher_list också bara används för att låta strängens join-metod iterera över den en gång, så med detta i åtanke kan funktionen i sin helhet skrivas:

def cipher(txt, key): text_as_number = (ord(letter) for letter in txt) random.seed(key) cipher_list = random.sample(range(500), len(txt)) text_cipher = (chr(a + b) for a, b in zip(text_as_number, cipher_list)) return ''.join(text_cipher)

Skulle du av någon anledning behöva mata in stora datamängder (men man kan notera att du i nuvarande form får ett ValueError-undantag för strängar längre än 500 tecken) så kommer detta både gå fortare och spara minne, men prestanda är en sidonotis: framför allt så uttrycker det vad du försöker göra mer koncist.

En allmän utflykt om läsbarhet och prestanda: det förstnämnda ska i första hand premieras före prestanda (förrän man vet att det behövs), men vad som är mest "läsbart" bygger ofta (inte alltid) på vad som också är bäst i prestandasynpunkt, eftersom dessa lösningar har en förmåga att bli praxis. En vettig definition på "läsbar kod" är kod som inte ska överraska någon som är van vid språket i fråga. Eftersom någon som är van vid Python bör ha listomfattningar och generatoruttryck i ryggmärgen så är de att föredra framför den mindre koncisa och mer resurskrävande metoden med att lägga till element för element i en loop. Någon som är van vid Python och ser en initialisering av en tom lista tänker automatiskt: "ojdå, här händer nog något märkligt snart eftersom kodaren valde denna metod" vilket gör koden tyngre att läsa (speciellt när det visar sig att inget vidare märkligt händer).

Mycket snack om en liten detalj, kan tyckas, men den är inte så liten som den ser ut, och dessutom ansamlas små detaljer lätt till större härken av svårtydd kod.


När vill man inte skriva listomfattningar? Oftast när elementoperationen är mer komplex än att den går att uttrycka i ett eller ett par korta funktionsanrop, men i mina ögon bör man då definiera en separat funktion som beskriver denna elementoperation och likväl ha tilldelningen som en listomfattning eller ett generatoruttryck. Om operationen är komplex nog för att inte gå att sammanfatta enkelt så bör den lyftas ut.

Ett annat alternativ som kan kännas mer bekant för någon som kommer från andra språk är map, vilket skulle användas som:

text_as_number = map(ord, txt)

Att hävda att det relativt sett är "ovanligt" att se detta i Python är måhända ett starkt ställningstagande, men jag ställer mig på Alex Martellis axlar och hävdar detta likväl. Listomfattningar mer flexibla och vanligare, och därmed enligt någon definition mer "läsbara".


Det är ofta bra att med tydliga variabelnamn skriva koden så att den "förklarar sig själv", vilket nuvarande kod gör bättre, men känns saker väldigt självklara så skulle man kunna överväga en variant som:

def cipher(txt, key): random.seed(key) cipher_list = random.sample(range(500), len(txt)) text_cipher = (chr(ord(a) + b) for a, b in zip(txt, cipher_list)) return ''.join(text_cipher)

eller kanske till och med bara:

def cipher(txt, key): random.seed(key) cipher_list = random.sample(range(500), len(txt)) return ''.join(chr(ord(a) + b) for a, b in zip(txt, cipher_list))

Att baka in även cipher_list-konstruktionen direkt i listomfattningen vore nog att gå väl långt, men kort kod kan vara bra så länge som den fortsätter vara uttrycksfull.


Vill man gå mer överbord så kan man känna hur det kliar i DRY-nerven när det enda som skiljer decrypt och cipher (som väl borde heta encrypt) åt är ett enstaka operatoranrop och titta på förslagsvis operator och functools.partial för att lösa detta.

Tack!

Den där posten hjälpte mig verkligen på flera sätt

Skrivet av Commander:

vill du kryptera text är det bara använda gpg. Där kan du spara varje lösenord som en textfil för att sedan bara kryptera det med gpg. Det du skulle kunna göra är sedan att hålla reda på och sortera dessa för användaren. Dock finns det redan projekt på detta
http://www.passwordstore.org/

Det där var ju nästan precis det jag tänkte göra

Nu fick jag lite idéer om att ta det till nästa nivå. Skapa ett program och binda det till typ "F1", programmet startar och känner av vilken sida man är på och kopierar in användarnamn och tabbar till nästa fält och skriver in rätt lösenord som det har hämtat genom Passwordstore du länkade till

Det är det här som både inspirerar och avskräcker mig från programmering, att man alltid känner sig otillräcklig

Visa signatur

Intel Core i5-3570K Processor || AMD Radeon HD 7970 || MSI Z77A-G45 (MS-7752)
3DMark06 30 781, SM2.0 Score - 11982 - HDR/SM3.0 Score 15632 - CPU Score 7631