Alle Adobe AIR-toepassingen delen dezelfde lokale database-engine. Elke AIR-toepassing kan daarom verbinding maken met en gegevens lezen van en schrijven naar een niet-gecodeerd databasebestand. Vanaf Adobe AIR 1.5 is AIR ook voorzien van de mogelijkheid om gecodeerde databasebestanden te maken en hiermee verbinding te maken. Wanneer u een gecodeerde database gebruikt, moet een toepassing de correcte coderingssleutel opgeven om verbinding te kunnen maken met deze database. Als een onjuiste of geen sleutel wordt opgegeven, kan de toepassing geen verbinding maken met de database. De toepassing kan dan ook geen gegevens van de database lezen of gegevens naar de database schrijven of wijzigen.
Als u een gecodeerde database wilt gebruiken, moet u de desbetreffende gecodeerde database maken. Bij een bestaande gecodeerde database kunt u een verbinding tot stand brengen. U kunt ook de coderingssleutel van een gecodeerde database wijzigen. De methoden voor het werken met een gecodeerde database zijn bijna hetzelfde als die voor het werken met een niet-gecodeerde. Het enige verschil is dat u de gecodeerde database moet maken en er verbinding mee tot stand moet brengen. Vooral de uitvoering van SQL-instructies is hetzelfde, of de database nu wel of niet gecodeerd is.
Gebruik van gecodeerde databases
Codering is nuttig als u de toegang tot de informatie in een database wilt beperken. U kunt de databasecoderingsfunctie van Adobe AIR gebruiken voor verschillende doeleinden. Hieronder volgen enige voorbeelden van situaties waarbij u een gecodeerde database kunt gebruiken:
-
Een alleen-lezen cache van privétoepassingsgegevens die u hebt gedownload van een server
-
Een lokale opslag van privétoepassingsgegevens die zijn gesynchroniseerd met een server (gegevens worden verzonden naar en geladen van de server)
-
Gecodeerde bestanden gebruikt als bestandsindeling voor documenten die zijn gemaakt door en bewerkt door de toepassing. Dit kunnen privébestanden van een bepaalde gebruiker zijn, of ze kunnen worden gedeeld door alle gebruikers van de toepassing.
-
Elk ander gebruik van een lokale gegevensopslag, zoals beschreven in
Toepassingen voor lokale SQL-databases
, waarbij de gegevens niet toegankelijk mogen zijn voor andere gebruikers die toegang hebben tot de computer of de databasebestanden.
Inzicht in de reden waarom u een gecodeerde database wilt gebruiken kan u helpen met de besluitvorming betreffende de architectuur van de toepassing. Dit is vooral van belang voor de manier waarop uw toepassing de coderingssleutel voor de database maakt, verkrijgt en opslaat. Raadpleeg
Overwegingen bij het gebruik van codering voor een database
voor meer informatie.
Behalve een gecodeerde database kunt u ook een
gecodeerde lokale opslag
gebruiken om gevoelige gegevens privé te houden. Bij de gecodeerde lokale opslag slaat u één enkele
ByteArray
-waarde op met behulp van een tekenreekssleutel. Alleen de AIR-toepassing die de waarde heeft opgeslagen, kan deze benaderen. Dit kan ook alleen op de computer waarop deze is opgeslagen. Met de gecodeerde lokale opslag is het niet nodig om uw eigen coderingssleutel te maken. Vanwege deze redenen is de gecodeerde lokale opslag het meest geschikt voor het opslaan van één waarde of een set waarden die gemakkelijk kunnen worden gecodeerd in een ByteArray. Een gecodeerde database is het meest geschikt voor grotere gegevenssets waarbij gestructureerde gegevensopslag en het uitvoeren van query's gewenst zijn. Zie
Lokale gegevens gecodeerd opslaan
voor meer informatie over het beveiligd opslaan van lokale gegevens.
Een gecodeerde database maken
Om een gecodeerde database te gebruiken, moet het databasebestand zijn gecodeerd wanneer het wordt gemaakt. Een niet-gecodeerde database kan later niet alsnog worden gecodeerd. Ook kan de codering van een gecodeerde database later niet worden opgeheven. U kunt desgewenst de coderingssleutel van een gecodeerde database wijzigen. Zie
De coderingssleutel van een database wijzigen
voor meer informatie. Als u een bestaande database hebt die niet gecodeerd is en u gebruik wilt maken van databasecodering, kunt u een nieuwe gecodeerde database maken en de bestaande tabelstructuur en gegevens kopiëren naar die nieuwe database.
Het maken van een gecodeerde database gaat op bijna dezelfde manier als het maken van een niet-gecodeerde database; dit wordt beschreven in
Database creëren
. Eerst maakt u een
SQLConnection
-instantie die de verbinding met de database vertegenwoordigt. U maakt de database door de methode
open()
of de methode
openAsync()
van het object SQLConnection aan te roepen. Geef hierbij voor de databaselocatie een bestand op dat nog niet bestaat. Wanneer u een gecodeerde database maakt, is het enige verschil dat u een waarde opgeeft voor de parameter
encryptionKey
(de vijfde parameter van de methode
open()
en de zesde parameter van de methode
openAsync()
).
Een geldige waarde voor de parameter
encryptionKey
is een ByteArray-object dat precies 16 bytes bevat.
http://help.adobe.com/nl_NL/Flash/CS5/AS3LR/flash/utils/ByteArray.html
In de volgende voorbeelden wordt getoond hoe u een gecodeerde database kunt maken. Voor het gemak is in deze voorbeeld de coderingssleutel vastgelegd in de toepassingscode. Dit is echter sterk af te raden omdat het niet veilig is.
var conn:SQLConnection = new SQLConnection();
var encryptionKey:ByteArray = new ByteArray();
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure!
// Create an encrypted database in asynchronous mode
conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey);
// Create an encrypted database in synchronous mode
conn.open(dbFile, SQLMode.CREATE, false, 1024, encryptionKey);
Een voorbeeld van een aanbevolen manier om een coderingssleutel te genereren vindt u in
Voorbeeld: Een coderingssleutel genereren en gebruiken
.
Verbinding maken met een gecodeerde database
De procedure voor het tot stand brengen van een verbinding met een gecodeerde database is vergelijkbaar met die voor het tot stand brengen van een verbinding met een niet-gecodeerde database. Die procedure wordt uitgebreid beschreven in
Verbinden met een database
. U gebruikt de methode
open()
voor
het openen van een verbinding in de synchrone uitvoeringsmodus
en de methode
openAsync()
voor
het openen van een verbinding in de asynchrone uitvoeringsmodus
. Het enige verschil is dat u voor het openen van een gecodeerde database de correcte waarde opgeeft voor de parameter
encryptionKey
(de vijfde parameter van de methode
open()
en de zesde parameter van de methode
openAsync()
).
Als de opgegeven coderingssleutel niet correct is, treedt er een fout op. Bij de methode
open()
wordt een
SQLError
-uitzondering gegenereerd. Bij de methode
openAsync()
verzendt het object
SQLConnection
een
SQLErrorEvent
-gebeurtenis, waarvan de eigenschap
error
een
SQLError
-object bevat. In beide gevallen heeft van het object SQLError dat door de uitzondering wordt gegenereerd, de eigenschap
errorID
de waarde 3138. Die fout-ID correspondeert met het foutbericht dat het geopende bestand geen databasebestand is.
In het volgende voorbeeld wordt gedemonstreerd hoe u een gecodeerde database opent in de asynchrone uitvoeringsmodus. Voor het gemak is in dit voorbeeld de coderingssleutel hard-gecodeerd in de toepassingscode. Dit is echter sterk af te raden omdat het niet veilig is.
import flash.data.SQLConnection;
import flash.data.SQLMode;
import flash.events.SQLErrorEvent;
import flash.events.SQLEvent;
import flash.filesystem.File;
var conn:SQLConnection = new SQLConnection();
conn.addEventListener(SQLEvent.OPEN, openHandler);
conn.addEventListener(SQLErrorEvent.ERROR, errorHandler);
var dbFile:File = File.applicationStorageDirectory.resolvePath("DBSample.db");
var encryptionKey:ByteArray = new ByteArray();
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure!
conn.openAsync(dbFile, SQLMode.UPDATE, null, false, 1024, encryptionKey);
function openHandler(event:SQLEvent):void
{
trace("the database opened successfully");
}
function errorHandler(event:SQLErrorEvent):void
{
if (event.error.errorID == 3138)
{
trace("Incorrect encryption key");
}
else
{
trace("Error message:", event.error.message);
trace("Details:", event.error.details);
}
}
In het volgende voorbeeld wordt gedemonstreerd hoe u een gecodeerde database opent in de synchrone uitvoeringsmodus. Voor het gemak is in dit voorbeeld de coderingssleutel hard-gecodeerd in de toepassingscode. Dit is echter sterk af te raden omdat het niet veilig is.
import flash.data.SQLConnection;
import flash.data.SQLMode;
import flash.filesystem.File;
var conn:SQLConnection = new SQLConnection();
var dbFile:File = File.applicationStorageDirectory.resolvePath("DBSample.db");
var encryptionKey:ByteArray = new ByteArray();
encryptionKey.writeUTFBytes("Some16ByteString"); // This technique is not secure!
try
{
conn.open(dbFile, SQLMode.UPDATE, false, 1024, encryptionKey);
trace("the database was created successfully");
}
catch (error:SQLError)
{
if (error.errorID == 3138)
{
trace("Incorrect encryption key");
}
else
{
trace("Error message:", error.message);
trace("Details:", error.details);
}
}
Een voorbeeld van een aanbevolen manier om een coderingssleutel te genereren vindt u in
Voorbeeld: Een coderingssleutel genereren en gebruiken
.
De coderingssleutel van een database wijzigen
Wanneer een database gecodeerd is, kunt u de coderingssleutel ervan later wijzigen. Als u de coderingssleutel van een database wilt wijzigen, opent u eerst een verbinding met de database door een
SQLConnection
-instantie te maken en de methode
open()
of
openAsync()
ervan aan te roepen. Als de database verbonden is, roept u de methode
reencrypt()
aan en geeft u de nieuwe coderingssleutel daarbij door als argument.
Zoals bij de meeste bewerkingen op databases, varieert het gedrag van de methode
reencrypt()
; dit is ervan afhankelijk of de databaseverbinding gebruikmaakt van een synchrone dan wel een asynchrone uitvoeringsmodus. Als u de methode
open()
gebruikt om verbinding te maken met de database, worden de
reencrypt()
-bewerkingen synchroon uitgevoerd. Nadat de bewerking is voltooid, gaat de uitvoering verder vanaf de volgende regel code:
var newKey:ByteArray = new ByteArray();
// ... generate the new key and store it in newKey
conn.reencrypt(newKey);
Als de databaseverbinding echter wordt geopend met de methode
openAsync()
, is de werking van
reencrypt()
asynchroon. De verandering van de coderingssleutel begint met het aanroepen van
reencrypt()
. Wanneer deze bewerking is voltooid, verzendt het object SQLConnection een gebeurtenis
reencrypt
. U gebruikt een gebeurtenislistener om te bepalen wanneer het proces reencrypt voltooid is:
var newKey:ByteArray = new ByteArray();
// ... generate the new key and store it in newKey
conn.addEventListener(SQLEvent.REENCRYPT, reencryptHandler);
conn.reencrypt(newKey);
function reencryptHandler(event:SQLEvent):void
{
// save the fact that the key changed
}
De bewerking
reencrypt()
wordt uitgevoerd in zijn eigen transactie. Als de bewerking wordt onderbroken of mislukt (bijvoorbeeld wanneer de toepassing wordt gesloten voordat de bewerking is voltooid), wordt de transactie teruggedraaid. In dat geval is de oorspronkelijke coderingssleutel nog steeds de sleutel voor de database.
De methode
reencrypt()
kan niet worden gebruikt om de codering van een database te verwijderen. Als de waarde
null
of een coderingssleutel die niet bestaat uit een 16-byte ByteArray, wordt doorgegeven aan de methode
reencrypt()
, treedt er een fout op.
Overwegingen bij het gebruik van codering voor een database
In de sectie
Gebruik van gecodeerde databases
wordt een aantal omstandigheden beschreven waaronder u gebruik kunt maken van gecodeerde databases. De gebruikscenario's van verschillende toepassingen (inclusief deze en andere scenario's) hebben uiteraard verschillende privacyvereisten. De mate waarin de gegevens in een database privé blijven, wordt voor een groot deel bepaald door de manier waarop u gebruikmaakt van codering in uw toepassing. Als u bijvoorbeeld een gecodeerde database gebruikt om persoonlijke gegevens privé te houden, zodat zelfs andere gebruikers van dezelfde computer er niet bij kunnen, heeft de database van iedere gebruiker een eigen coderingssleutel nodig. Voor optimale beveiliging kan uw toepassing de sleutel genereren aan de hand van een wachtwoord dat door de gebruiker wordt ingevoerd. Als de coderingssleutel wordt gebaseerd op een wachtwoord, kan zelfs een andere gebruiker die in staat is zich voor te doen als gebruiker van deze account op de computer, geen toegang krijgen tot deze gegevens. Aan de andere kant van het privacyspectrum vindt u databasebestanden die kunnen worden gelezen door iedere gebruiker van uw toepassing, maar niet door andere toepassingen. In dat geval moet ieder geïnstalleerd exemplaar van de toepassing toegang hebben tot een gedeelde coderingssleutel.
U kunt het ontwerp van uw toepassing en vooral de methode die wordt gebruikt om de coderingssleutel te genereren, aanpassen aan het privacyniveau dat u wilt gebruiken voor uw toepassingsgegevens. Hieronder volgt een lijst met suggesties voor verschillende privacyniveaus voor gegevens:
-
Als u een database toegankelijk wil maken voor iedere gebruiker die toegang heeft tot de toepassing op iedere computer, gebruikt u één sleutel die beschikbaar is voor alle exemplaren van de toepassing. De eerste keer dat een toepassing wordt uitgevoerd, kan deze de gedeelde coderingssleutel bijvoorbeeld downloaden vanaf een server met gebruikmaking van een veilig protocol zoals SSL. De toepassing kan de sleutel dan opslaan in de gecodeerde lokale opslag voor toekomstig gebruik. Als alternatief kunt u de gegevens per gebruiker coderen op de computer en de gegevens synchroniseren met een externe gegevensopslag zoals een server, zodat de gegevens overdraagbaar zijn.
-
Als u een database voor slechts één gebruiker toegankelijk wilt maken op alle computers, genereert u de coderingssleutel aan de hand van een geheim dat alleen de gebruiker kent (bijvoorbeeld een wachtwoord). U moet dan absoluut niet gebruikmaken van een waarde die is gekoppeld aan een bepaalde computer (bijvoorbeeld een waarde die is opgeslagen in de gecodeerde lokale opslag) om de sleutel te genereren. Als alternatief kunt u de gegevens per gebruiker coderen op de computer en de gegevens synchroniseren met een externe gegevensopslag zoals een server, zodat de gegevens overdraagbaar zijn.
-
Als een database toegankelijk moet zijn voor één gebruiker op één computer, genereert u de sleutel aan de hand van een wachtwoord en een gegenereerde salt. Een voorbeeld van deze methode vindt u in
Voorbeeld: Een coderingssleutel genereren en gebruiken
.
Hieronder volgt een aantal extra beveiligingsoverwegingen waarmee u rekening moet houden als u een toepassing ontwerpt die gebruikmaakt van een gecodeerde database:
-
Een systeem is zo veilig als de zwakste schakel. Als u gebruikmaakt van een door de gebruiker ingevoerd wachtwoord om een coderingssleutel te genereren, is het aan te raden beperkingen met betrekking tot de lengte en de complexiteit van het wachtwoord op te geven. Een kort wachtwoord met eenvoudige tekens is gemakkelijk te raden.
-
De broncode van een AIR-toepassing wordt op de computer van een gebruiker opgeslagen als onbewerkte tekst (voor HTML-inhoud) of met een gemakkelijk te decompileren binaire indeling (voor SWF-inhoud). Omdat de broncode toegankelijk is, moet u rekening houden met de volgende twee punten:
-
Maak nooit gebruik van een hard-gecodeerde sleutel in de broncode
-
Ga er altijd van uit dat de methode die wordt gebruikt om een coderingssleutel te genereren (bijvoorbeeld een willekeurige tekengenerator of een bepaald hashalgoritme) gemakkelijk kan worden bepaald door een aanvaller
-
AIR-databasecodering maakt gebruik van de Advanced Encryption Standard (AES) met Counter with CBC-MAC (CCM)-modus. Deze codering is alleen veilig als een door de gebruiker ingevoerde sleutel wordt gecombineerd met een salt-waarde. Een voorbeeld van deze methode vindt u in
Voorbeeld: Een coderingssleutel genereren en gebruiken
.
-
Wanneer u ervoor kiest om een database te coderen, worden alle schijfbestanden gecodeerd die door de database-engine samen met die database worden gebruikt. De database-engine slaat echter bepaalde gegevens tijdelijk op in een geheugencache om de lees- en schrijfprestaties bij transacties te verbeteren. Gegevens die aanwezig zijn in het geheugen, zijn niet gecodeerd. Als een aanvaller toegang kan krijgen tot het geheugen dat wordt gebruikt door een AIR-toepassing, bijvoorbeeld door middel van een debugger, zijn de gegevens in een database die nu open en niet-gecodeerd is, beschikbaar.
Voorbeeld: Een coderingssleutel genereren en gebruiken
In deze voorbeeldtoepassing wordt een methode gedemonstreerd om een coderingssleutel te genereren. Deze toepassing is speciaal ontworpen om het maximale privacy- en beveiligingsniveau te bieden voor de gegevens van de gebruikers. Eén belangrijk aspect van het beveiligen van privégegevens is dat de gebruiker een wachtwoord moet invoeren telkens wanneer de toepassing verbinding met de database maakt. Zoals in dit voorbeeld wordt geïllustreerd, betekent dit dat een toepassing die een dergelijk privacyniveau vereist, de databasecoderingssleutel nooit rechtstreeks mag opslaan.
De toepassing bestaat uit twee delen: een ActionScript-klasse die een coderingssleutel genereert (de klasse EncryptionKeyGenerator) en een basisgebruikersinterface die demonstreert hoe die klasse moet worden gebruikt. De volledige broncode vindt u in
Complete voorbeeldcode voor het genereren en gebruiken van een coderingssleutel
.
De klasse EncryptionKeyGenerator gebruiken om een veilige coderingssleutel te verkrijgen.
U kunt de klasse EncryptionKeyGenerator gebruiken in uw toepassing zonder dat u alle details van deze klasse moet kennen. Zie
De klasse EncryptionKeyGenerator
als u wilt weten hoe de klasse een versleutelingssleutel voor een database genereert.
Ga als volgt te werk om de klasse EncryptionKeyGenerator in uw toepassing te gebruiken:
-
Download de EncryptionKeyGenerator-klasse als broncode of een gecompileerd SWC-bestand. De klasse EncryptionKeyGenerator is opgenomen in het as3corelib-project (open-source ActionScript 3.0 core library). U kunt
het as3corelib-pakket, inclusief broncode en documentatie downloaden
. U kunt de SWC- of broncodebestanden ook downloaden van de projectpagina.
-
Plaats de broncode voor de klasse EncryptionKeyGenerator (of de as3corelib-SWC) op een locatie waar de broncode van de toepassing deze kan vinden.
-
Voeg aan de broncode van uw toepassing een
import
-instructie voor de klasse EncryptionKeyGenerator toe.
import com.adobe.air.crypto.EncryptionKeyGenerator;
-
Vóór het punt waarop de code de database maakt of een verbinding met de database opent, voegt u code toe om een EncryptionKeyGenerator-instantie te maken. Daartoe roept u de
EncryptionKeyGenerator()
-constructor aan.
var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator();
-
Vraag een wachtwoord aan de gebruiker:
var password:String = passwordInput.text;
if (!keyGenerator.validateStrongPassword(password))
{
// display an error message
return;
}
De EncryptionKeyGenerator-instantie gebruikt dit wachtwoord als de basis voor de coderingssleutel (weergegeven in de volgende stap). De EncryptionKeyGenerator-instantie test het wachtwoord aan de hand van bepaalde vereisten voor sterke wachtwoorden. Als de validatie mislukt, treedt een fout op. Zoals in de voorbeeldcode wordt geïllustreerd, kunt u het wachtwoord op voorhand controleren door de methode
validateStrongPassword()
van het EncryptionKeyGenerator-object aan te roepen. Op die manier kunt u bepalen of het wachtwoord aan de minimale vereisten voor een sterk wachtwoord voldoet, en kunt u een fout vermijden.
-
Genereer de coderingssleutel vanuit het wachtwoord:
var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password);
De methode
getEncryptionKey()
genereert en retourneert de coderingssleutel (een ByteArray van 16 bytes). U kunt de coderingssleutel gebruiken om de nieuwe gecodeerde database te maken of u kunt de bestaande openen.
De methode
getEncryptionKey()
heeft één verplichte parameter, namelijk het wachtwoord dat in stap 5 is verkregen.
Opmerking:
Om het hoogste niveau voor beveiliging en privacy van de gegevens te behouden, moet een toepassing de gebruiker om het wachtwoord vragen, telkens wanneer de toepassing verbinding met de database maakt. Sla het wachtwoord of de coderingssleutel van de toepassing niet rechtstreeks op. Dit levert beveiligingsrisico's op. In plaats daarvan moet een toepassing -zoals in dit voorbeeld wordt geïllustreerd - telkens opnieuw de dezelfde techniek gebruiken om de coderingssleutel uit het wachtwoord te halen, dus zowel wanneer de gecodeerde database wordt gemaakt als wanneer later verbinding met de database wordt gemaakt.
De methode
getEncryptionKey()
accepteert ook een tweede (optionele) parameter, de parameter
overrideSaltELSKey
. De EncryptionKeyGenerator maakt een willekeurige waarde (een zogenaamde
salt
) die als deel van de coderingssleutel wordt gebruikt. Om de coderingssleutel opnieuw te kunnen maken, wordt de salt-waarde opgeslagen in de ELS (Encrypted Local Store) van de AIT-toepassing. Standaard gebruikt de klasse EncryptionKeyGenerator een bepaalde tekenreeks als de ELS-sleutel. Hoewel onwaarschijnlijk, is het mogelijk dat de sleutel een conflict kan opleveren met een andere sleutel die in de toepassing wordt gebruikt. In plaats van de standaardsleutel te gebruiken, kunt u ook uw eigen ELS-sleutel opgeven. In dat geval geeft u een aangepaste sleutel op door deze als tweede
getEncryptionKey()
-parameter door te geven, zoals hier wordt geïllustreerd:
var customKey:String = "My custom ELS salt key";
var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password, customKey);
-
De database maken of openen
Met de coderingssleutel die door de methode
getEncryptionKey()
wordt geretourneerd, kan uw code een nieuwe gecodeerde database maken of proberen om verbinding te maken met de bestaande gecodeerde database. In beide gevallen gebruikt u de methode
open()
of
openAsync()
van de klasse SQLConnection zoals wordt beschreven in
Een gecodeerde database maken
en
Verbinding maken met een gecodeerde database
.
In dit voorbeeld is de toepassing ontworpen om de database te openen in de asynchrone uitvoeringsmodus. De code stelt de juiste gebeurtenislisteners in en roept de methode
openAsync()
aan, waarbij de coderingssleutel wordt doorgegeven als het laatste argument:
conn.addEventListener(SQLEvent.OPEN, openHandler);
conn.addEventListener(SQLErrorEvent.ERROR, openError);
conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey);
In de listenermethoden verwijdert de code de gebeurtenislistener-registraties. Vervolgens wordt een statusbericht weergegeven waarin wordt aangegeven of de database is gemaakt, geopend of dat er een fout is opgetreden. Het belangrijkste gedeelte van deze gebeurtenishandlers zit in de methode
openError()
. In die methode controleert een
if
-instructie of de database bestaat (wat betekent dat er wordt geprobeerd verbinding te maken met een bestaande database) en of de fout-id overeenkomt met de constante
EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID
. Als beide voorwaarden waar zijn, betekent dit waarschijnlijk dat de gebruiker een verkeerd wachtwoord heeft ingevoerd. (Het kan ook betekenen dat het opgegeven bestand helemaal geen databasebestand is.) Hier volgt de code die de fout-id controleert:
if (!createNewDB && event.error.errorID == EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID)
{
statusMsg.text = "Incorrect password!";
}
else
{
statusMsg.text = "Error creating or opening database.";
}
Voor de volledige code van de voorbeeldgebeurtenislisteners raadpleegt u
Complete voorbeeldcode voor het genereren en gebruiken van een coderingssleutel
.
Complete voorbeeldcode voor het genereren en gebruiken van een coderingssleutel
Hier volgt de volledige code voor de voorbeeldtoepassing 'Een coderingssleutel genereren en gebruiken'. De code bestaat uit twee delen.
Het voorbeeld gebruikt de klasse EncryptionKeyGenerator om een coderingssleutel te maken op basis van een wachtwoord. De klasse EncryptionKeyGenerator is opgenomen in het as3corelib-project (open-source ActionScript 3.0 core library). U kunt
het as3corelib-pakket, inclusief broncode en documentatie downloaden
. U kunt de SWC- of broncodebestanden ook downloaden van de projectpagina.
Flex-voorbeeld
Het MXML-bestand van de toepassing bevat de broncode voor een eenvoudige toepassing die een verbinding met een gecodeerde database maakt of opent:
<?xml version="1.0" encoding="utf-8"?>
<mx:WindowedApplication xmlns:mx="http://www.adobe.com/2006/mxml" layout="vertical" creationComplete="init();">
<mx:Script>
<![CDATA[
import com.adobe.air.crypto.EncryptionKeyGenerator;
private const dbFileName:String = "encryptedDatabase.db";
private var dbFile:File;
private var createNewDB:Boolean = true;
private var conn:SQLConnection;
// ------- Event handling -------
private function init():void
{
conn = new SQLConnection();
dbFile = File.applicationStorageDirectory.resolvePath(dbFileName);
if (dbFile.exists)
{
createNewDB = false;
instructions.text = "Enter your database password to open the encrypted database.";
openButton.label = "Open Database";
}
}
private function openConnection():void
{
var password:String = passwordInput.text;
var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator();
if (password == null || password.length <= 0)
{
statusMsg.text = "Please specify a password.";
return;
}
if (!keyGenerator.validateStrongPassword(password))
{
statusMsg.text = "The password must be 8-32 characters long. It must contain at least one lowercase letter, at least one uppercase letter, and at least one number or symbol.";
return;
}
passwordInput.text = "";
passwordInput.enabled = false;
openButton.enabled = false;
var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password);
conn.addEventListener(SQLEvent.OPEN, openHandler);
conn.addEventListener(SQLErrorEvent.ERROR, openError);
conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey);
}
private function openHandler(event:SQLEvent):void
{
conn.removeEventListener(SQLEvent.OPEN, openHandler);
conn.removeEventListener(SQLErrorEvent.ERROR, openError);
statusMsg.setStyle("color", 0x009900);
if (createNewDB)
{
statusMsg.text = "The encrypted database was created successfully.";
}
else
{
statusMsg.text = "The encrypted database was opened successfully.";
}
}
private function openError(event:SQLErrorEvent):void
{
conn.removeEventListener(SQLEvent.OPEN, openHandler);
conn.removeEventListener(SQLErrorEvent.ERROR, openError);
if (!createNewDB && event.error.errorID == EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID)
{
statusMsg.text = "Incorrect password!";
}
else
{
statusMsg.text = "Error creating or opening database.";
}
}
]]>
</mx:Script>
<mx:Text id="instructions" text="Enter a password to create an encrypted database. The next time you open the application, you will need to re-enter the password to open the database again." width="75%" height="65"/>
<mx:HBox>
<mx:TextInput id="passwordInput" displayAsPassword="true"/>
<mx:Button id="openButton" label="Create Database" click="openConnection();"/>
</mx:HBox>
<mx:Text id="statusMsg" color="#990000" width="75%"/>
</mx:WindowedApplication>
Flash Professional-voorbeeld
Het FLA-bestand van de toepassing bevat de broncode voor een eenvoudige toepassing die een verbinding met een gecodeerde database maakt of opent. Het FLA-bestand heeft vier onderdelen in het werkgebied geplaatst:
Instantienaam
|
Type onderdeel
|
Beschrijving
|
instructies
|
Label
|
Bevat de instructies die aan de gebruiker worden gegeven
|
passwordInput
|
TextInput
|
Invoerveld waarin de gebruiker het wachtwoord typt
|
openButton
|
Button
|
Knop waarop de gebruiker klikt nadat het wachtwoord is ingevoerd
|
statusMsg
|
Label
|
Geeft statusberichten (geslaagd of niet geslaagd) weer
|
De code voor de toepassing wordt gedefinieerd in een hoofdframe in frame 1 van de hoofdtijdlijn. Hier volgt de code voor de toepassing:
import com.adobe.air.crypto.EncryptionKeyGenerator;
const dbFileName:String = "encryptedDatabase.db";
var dbFile:File;
var createNewDB:Boolean = true;
var conn:SQLConnection;
init();
// ------- Event handling -------
function init():void
{
passwordInput.displayAsPassword = true;
openButton.addEventListener(MouseEvent.CLICK, openConnection);
statusMsg.setStyle("textFormat", new TextFormat(null, null, 0x990000));
conn = new SQLConnection();
dbFile = File.applicationStorageDirectory.resolvePath(dbFileName);
if (dbFile.exists)
{
createNewDB = false;
instructions.text = "Enter your database password to open the encrypted database.";
openButton.label = "Open Database";
}
else
{
instructions.text = "Enter a password to create an encrypted database. The next time you open the application, you will need to re-enter the password to open the database again.";
openButton.label = "Create Database";
}
}
function openConnection(event:MouseEvent):void
{
var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator();
var password:String = passwordInput.text;
if (password == null || password.length <= 0)
{
statusMsg.text = "Please specify a password.";
return;
}
if (!keyGenerator.validateStrongPassword(password))
{
statusMsg.text = "The password must be 8-32 characters long. It must contain at least one lowercase letter, at least one uppercase letter, and at least one number or symbol.";
return;
}
passwordInput.text = "";
passwordInput.enabled = false;
openButton.enabled = false;
var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password);
conn.addEventListener(SQLEvent.OPEN, openHandler);
conn.addEventListener(SQLErrorEvent.ERROR, openError);
conn.openAsync(dbFile, SQLMode.CREATE, null, false, 1024, encryptionKey);
}
function openHandler(event:SQLEvent):void
{
conn.removeEventListener(SQLEvent.OPEN, openHandler);
conn.removeEventListener(SQLErrorEvent.ERROR, openError);
statusMsg.setStyle("textFormat", new TextFormat(null, null, 0x009900));
if (createNewDB)
{
statusMsg.text = "The encrypted database was created successfully.";
}
else
{
statusMsg.text = "The encrypted database was opened successfully.";
}
}
function openError(event:SQLErrorEvent):void
{
conn.removeEventListener(SQLEvent.OPEN, openHandler);
conn.removeEventListener(SQLErrorEvent.ERROR, openError);
if (!createNewDB && event.error.errorID == EncryptionKeyGenerator.ENCRYPTED_DB_PASSWORD_ERROR_ID)
{
statusMsg.text = "Incorrect password!";
}
else
{
statusMsg.text = "Error creating or opening database.";
}
}
De klasse EncryptionKeyGenerator
Het is niet nodig dat u precies begrijpt hoe de klasse EncryptionKeyGenerator werkt om deze te gebruiken om een veilige coderingssleutel voor uw toepassingsdatabase te maken. Het gebruik van de klasse wordt uitgelegd in
De klasse EncryptionKeyGenerator gebruiken om een veilige coderingssleutel te verkrijgen.
. Het is misschien echter handig om te begrijpen welke technieken de klasse gebruikt. Misschien wilt de klasse bijvoorbeeld aanpassen, of bepaalde technieken van deze klasse gebruiken voor situaties waarin een ander niveau van gegevensprivacy is gewenst.
De klasse EncryptionKeyGenerator is opgenomen in het as3corelib-project (open-source ActionScript 3.0 core library). U kunt
het as3corelib-pakket inclusief broncode en documentatie
downloaden. U kunt ook de broncode op de projectsite bekijken of deze downloaden om tijdens de uitleg te volgen.
Wanneer code een EncryptionKeyGenerator-exemplaar maakt en de methode
getEncryptionKey()
aanroept, moeten enkele stappen worden uitgevoerd om ervoor te zorgen dat alleen de juiste gebruiker toegang heeft tot de gegevens. Het proces voor het genereren van een coderingssleutel vanuit een door de gebruiker ingevoerd wachtwoord voordat de database wordt gemaakt, is precies hetzelfde als wanneer de coderingssleutel opnieuw moet worden gemaakt om de database te openen.
Een sterk wachtwoord verkrijgen en valideren
Wanneer code de methode
getEncryptionKey()
aanroept, wordt een wachtwoord als parameter doorgegeven. Dit wachtwoord wordt gebruikt als de basis voor de coderingssleutel. Door gebruik te maken van informatie die alleen bekend is aan de gebruiker, bent u er met dit ontwerp zeker van dat alleen de gebruiker die het wachtwoord kent, toegang kan krijgen tot de gegevens in de database. Zelfs als een aanvaller toegang kan krijgen tot de account van die gebruiker op de computer, kan de aanvaller de database alleen openen als hij beschikt over het wachtwoord. Voor maximale veiligheid slaat de toepassing het wachtwoord nooit op.
De code van een toepassing maakt een EncryptionKeyGenerator-instantie en roept de methode
getEncryptionKey()
aan, en geeft een door de gebruiker ingevoerd wachtwoord op als argument (het wachtwoord
variable
in dit voorbeeld):
var keyGenerator:EncryptionKeyGenerator = new EncryptionKeyGenerator();
var encryptionKey:ByteArray = keyGenerator.getEncryptionKey(password);
Wanneer de methode
getEncryptionKey()
wordt aangeroepen, controleert de klasse EncryptionKeyGenerator eerst het door de gebruiker ingevoerde wachtwoord om na te gaan of het een sterk wachtwoord is. Het wachtwoord voor de EncryptionKeyGenerator-klasse moet 8 tot 32 tekens lang zijn. Het wachtwoord moet zowel hoofdletters als kleine letters bevatten en ten minste één cijfer of symbool.
De reguliere expressie waarmee dit patroon wordt gecontroleerd, is gedefinieerd als een constante met de naam
STRONG_PASSWORD_PATTERN
:
private static const STRONG_PASSWORD_PATTERN:RegExp = /(?=^.{8,32}$)((?=.*\d)|(?=.*\W+))(?![.\n])(?=.*[A-Z])(?=.*[a-z]).*$/;
De code waarmee het wachtwoord wordt gecontroleerd, bevindt zich in de methode
validateStrongPassword()
van de klasse EncryptionKeyGenerator. De code is als volgt:
public function vaidateStrongPassword(password:String):Boolean
{
if (password == null || password.length <= 0)
{
return false;
}
return STRONG_PASSWORD_PATTERN.test(password))
}
De methode
getEncryptionKey()
roept de methode
validateStrongPassword()
van de EncryptionKeyGenerator-klasse intern aan en genereert een uitzondering als het wachtwoord ongeldig is. De methode
validateStrongPassword()
is een publieke methode zodat de toepassingscode een wachtwoord kan controleren zonder de methode
getEncryptionKey()
aan te roepen. Hierdoor wordt vermeden dat een fout optreedt.
Het wachtwoord uitbreiden tot 256 bits
Verderop in het proces moet het wachtwoord 256 bits lang zijn. De code vraagt niet iedere gebruiker een wachtwoord in te voeren dat precies 256 bits (32 tekens) lang is, maar creëert een langer wachtwoord door de tekens van het wachtwoord te herhalen.
De methode
getEncryptionKey()
roept de methode
concatenatePassword()
aan om het lange wachtwoord te maken.
var concatenatedPassword:String = concatenatePassword(password);
Hier volgt de code voor de methode
concatenatePassword()
:
private function concatenatePassword(pwd:String):String
{
var len:int = pwd.length;
var targetLength:int = 32;
if (len == targetLength)
{
return pwd;
}
var repetitions:int = Math.floor(targetLength / len);
var excess:int = targetLength % len;
var result:String = "";
for (var i:uint = 0; i < repetitions; i++)
{
result += pwd;
}
result += pwd.substr(0, excess);
return result;
}
Als het wachtwoord korter is dan 256 bits, voegt de code het wachtwoord nogmaals toe om er 256 bits van te maken. Als de lengte niet precies klopt, wordt het laatste stuk afgekapt zodat het wachtwoord precies 256 bits lang is.
Een salt-waarde van 256 bits genereren of ophalen
De volgende stap is het verkrijgen van een salt-waarde van 256 bits die in een latere stap wordt gecombineerd met het wachtwoord. Een
salt
is een willekeurige waarde die wordt toegevoegd aan of gecombineerd met een door de gebruiker ingevoerde waarde om een wachtwoord te vormen. Als u een salt gebruikt met een wachtwoord, weet u zeker dat zelfs als de gebruiker een bestaand woord of veelgebruikte term als wachtwoord gebruikt, de combinatie van wachtwoord met salt die het systeem gebruikt, een willekeurige waarde is. Deze willekeurigheid helpt een woordenlijstaanval te voorkomen, waarbij een hacker een lijst met woorden gebruikt om een wachtwoord te raden. Verder wordt door het genereren van de salt-waarde en het opslaan daarvan in de gecodeerde lokale opslag, de salt-waarde gekoppeld aan de account van de gebruiker op de computer waarop de database zich bevindt.
Als de toepassing de methode
getEncryptionKey()
de eerste keer aanroept, maakt de code een willekeurige salt-waarde van 256 bits. In het vervolg laadt de code de waarde uit de gecodeerde lokale opslag.
De salt wordt opgeslagen in een variabele met de naam
salt
. De code bepaalt of er al een salt is gemaakt. Dit gebeurt door de salt te laden vanaf een beveiligde opslagplaats voor lokale gegevens:
var salt:ByteArray = EncryptedLocalStore.getItem(saltKey);
if (salt == null)
{
salt = makeSalt();
EncryptedLocalStore.setItem(saltKey, salt);
}
Als de code een nieuwe salt-waarde maakt, genereert de methode
makeSalt()
een willekeurige waarde van 256 bits. Aangezien deze waarde uiteindelijk wordt opgeslagen in de gecodeerde lokale opslag, wordt de waarde gegenereerd als ByteArray-object. De methode
makeSalt()
gebruikt de methode
Math.random()
om een willekeurige waarde te genereren. De methode
Math.random()
kan niet 256 bits tegelijk genereren. In plaats daarvan gebruikt de code een lus om
Math.random()
acht keer aan te roepen. Iedere keer wordt een willekeurige uint-waarde tussen 0 en 4294967295 (de maximale uint-waarde) gegenereerd. De uint-waarde wordt voor het gemak gebruikt, aangezien een uint precies 32 bits gebruikt. Door acht uint-waarden naar de ByteArray te schrijven wordt een waarde van 256 bits gegenereerd. Hieronder volgt de code voor de methode
makeSalt()
:
private function makeSalt():ByteArray
{
var result:ByteArray = new ByteArray;
for (var i:uint = 0; i < 8; i++)
{
result.writeUnsignedInt(Math.round(Math.random() * uint.MAX_VALUE));
}
return result;
}
Wanneer de code de salt opslaat in de ELS (Encrypted Local Store) of de salt uit de ELS ophaalt, heeft deze een String-sleutel nodig waarin de salt wordt opgeslagen. De salt-waarde kan niet worden opgehaald zonder de sleutel te kennen. In dat geval kan de coderingssleutel niet opnieuw worden gemaakt wanneer de database opnieuw moet worden geopend. Standaard gebruikt de EncryptionKeyGenerator een op voorhand gedefinieerde ELS-sleutel die is gedefinieerd in de constante
SALT_ELS_KEY
. In plaats van de standaardsleutel te gebruiken, kan de toepassingscode ook een ELS-sleutel opgeven die moet worden gebruikt in de aanroep van de methode
getEncryptionKey()
. De standaardsleutel of de in de toepassing opgegeven ELS-sleutels wordt opgeslagen in een variabele met de naam
saltKey
. Deze variabele wordt gebruikt om
EncryptedLocalStore.setItem()
en
EncryptedLocalStore.getItem()
aan te roepen, zoals eerder werd getoond.
Het 256-bits wachtwoord combineren met de salt met behulp van de XOR-operator
De code heeft nu een wachtwoord van 256 bits en een salt-waarde van 256 bits. Vervolgens wordt een bitsgewijze XOR-bewerking gebruikt om de salt en het samengevoegde wachtwoord in één waarde te gebruiken. In feite wordt met deze techniek een wachtwoord van 256 bits gemaakt bestaande uit tekens uit het hele bereik van mogelijke tekens. Dit principe is waar, ook al bestaat het ingevoerde wachtwoord hoogstwaarschijnlijk voornamelijk uit alfanumerieke tekens. Deze extra willekeurigheid biedt het voordeel dat de set mogelijke wachtwoorden groot is zonder dat de gebruiker een lang en complex wachtwoord moet invoeren.
Het resultaat van de XOR-bewerking wordt opgeslagen in de variabele
unhashedKey
. De bitsgewijze XOR voor de twee waarden wordt in werkelijkheid uitgevoerd in de methode
xorBytes()
:
var unhashedKey:ByteArray = xorBytes(concatenatedPassword, salt);
De bitsgewijze XOR-operator (
^
) gebruikt twee uint-waarden en geeft als resultaat een uint-waarde. (Een uint-waarde bevat 32 bits.) De ingevoerde waarden die als argumenten worden doorgegeven aan de methode
xorBytes()
, bestaan uit een tekenreeks (het wachtwoord) en een ByteArray (de salt). De code gebruikt een lus om 32 bits per keer uit ieder invoergegeven te extraheren, die dan worden gecombineerd met gebruikmaking van de XOR-operator.
private function xorBytes(passwordString:String, salt:ByteArray):ByteArray
{
var result:ByteArray = new ByteArray();
for (var i:uint = 0; i < 32; i += 4)
{
// ...
}
return result;
}
Binnen de lus worden de eerste 32 bits (4 bytes) uit de parameter
passwordString
gehaald. Deze bits worden geëxtraheerd en vervolgens geconverteerd naar een (
o1
) met een proces dat uit twee delen bestaat. Eerst haalt de methode
charCodeAt()
de numerieke waarde van ieder teken op. Vervolgens wordt deze waarde naar de juiste positie in de uint verschoven met behulp van de bitsgewijze operator voor verschuiven naar links (
<<
). De verschoven waarde wordt toegevoegd aan
o1
. Het eerste teken (
i
) wordt bijvoorbeeld de eerste 8 bits door de operator voor bitsgewijs naar links verplaatsen (
<<
) te gebruiken om de bits 24 bits naar links te verplaatsen en die waarde toe te wijzen aan
o1
. Het tweede teken
(i + 1
) wordt de tweede 8 bits door de waarde ervan 16 bits naar links te verplaatsen en het resultaat toe te voegen aan
o1
. De waarden voor het derde en het vierde teken worden op dezelfde manier toegevoegd.
// ...
// Extract 4 bytes from the password string and convert to a uint
var o1:uint = passwordString.charCodeAt(i) << 24;
o1 += passwordString.charCodeAt(i + 1) << 16;
o1 += passwordString.charCodeAt(i + 2) << 8;
o1 += passwordString.charCodeAt(i + 3);
// ...
De variabele
o1
bevat nu 32 bits uit de parameter
passwordString
. Vervolgens worden 32 bits geëxtraheerd uit de parameter
salt
door de methode
readUnsignedInt()
ervan aan te roepen. De 32 bits worden opgeslagen in de uint-variabele
o2
.
// ...
salt.position = i;
var o2:uint = salt.readUnsignedInt();
// ...
Tot slot worden de twee 32-bits waarden gecombineerd met de operator XOR en wordt het resultaat weggeschreven naar een ByteArray met de naam
result
.
// ...
var xor:uint = o1 ^ o2;
result.writeUnsignedInt(xor);
// ...
Als de lus is voltooid, wordt de ByteArray met het XOR-resultaat geretourneerd.
// ...
}
return result;
}
De sleutel hashen
Wanneer het samengevoegde wachtwoord en de salt zijn gecombineerd, moet deze tekenreeks verder worden beveiligd door er het hashinglogaritme SHA-256 op toe te passen. Door de waarde te hashen wordt het voor een aanvaller moeilijker deze te reverse-engineeren.
Op dit moment heeft de code een ByteArray met de naam
unhashedKey
die het samengevoegde wachtwoord bevat dat met de salt is gecombineerd. Het as3corelib-project (de kernbibliotheek van ActionScript 3.0) bevat een klasse SHA256 in het pakket com.adobe.crypto. De methode
SHA256.hashBytes()
die een SHA-256-hash uitvoert voor een ByteArray en een tekenreeks retourneert die het hashresultaat van 256 bits bevat als een hexadecimaal getal. De klasse EncryptionKeyGenerator gebruikt de klasse SHA256 om de sleutel te husselen (hashing):
var hashedKey:String = SHA256.hashBytes(unhashedKey);
De coderingssleutel uit de hash extraheren
De coderingssleutel moet een ByteArray zijn die precies 16 bytes (128 bits) lang is. Het resultaat van het hashalgoritme SHA-256 is altijd 256 bits lang. Dat betekent dat in de laatste stap 128 bits uit het hashresultaat gehaald moeten worden die de uiteindelijke coderingssleutel gaan vormen.
In de klasse EncryptionKeyGenerator reduceert de code de sleutel tot 128 bits door de methode
generateEncryptionKey()
aan te roepen. Vervolgens wordt het resultaat van die methode geretourneerd als het resultaat van de methode
getEncryptionKey()
:
var encryptionKey:ByteArray = generateEncryptionKey(hashedKey);
return encryptionKey;
Het is niet noodzakelijk om de eerste 128 bits als coderingssleutel te gebruiken. U zou een reeks bits kunnen selecteren vanaf een willekeurig punt, u zou elke andere bit kunnen selecteren, of bits op een andere manier kunnen selecteren. Essentieel is dat de code 128 specifieke bits selecteert en dat diezelfde 128 bits iedere keer opnieuw worden gebruikt.
In dit geval gebruikt de methode
generateEncryptionKey()
de reeks bits die op de achttiende byte begint, als coderingssleutel. Zoals eerder vermeld geeft de klasse SHA256 als resultaat een tekenreeks met een 256-bits hash als hexadecimaal getal. Eén enkel blok van 128 bits heeft te veel bytes om in één keer aan een ByteArray toe te voegen. De code gebruikte daarom een
for
-lus om tekens uit de hexadecimale tekenreeks te extraheren, deze te converteren naar numerieke waarden en die toe te voegen aan de ByteArray. De resultaatreeks van SHA-256 is 64 tekens lang. Een bereik van 128 bits is gelijk aan 32 tekens in de tekenreeks, en elk teken vertegenwoordigt 4 bits. De kleinste gegevenstoename die u kunt toevoegen aan een ByteArray is 1 byte (8 bits), hetgeen equivalent is aan twee tekens in de
hash
-tekenreeks. De lus telt daarom van 0 tot 31 (32 tekens) in stappen van 2 tekens.
Binnen de lus bepaalt de code eerst de beginpositie voor het huidige paar tekens. Aangezien het gewenste bereik start bij het teken op indexpositie 17 (de achttiende byte), wordt aan de variabele
position
de huidige iteratorwaarde (
i
) plus 17 toegekend. De code gebruikt de methode
substr()
van het tekenreeksobject om de twee tekens op de huidige positie te extraheren. Deze twee tekens worden opgeslagen in de variabele
hex
. Vervolgens gebruikt de code de methode
parseInt()
om de
hex
-tekenreeks te converteren naar een decimaal geheel getal. Deze slaat die waarde op in de int-variabele
byte
. Tot slot voegt de code de waarde in
byte
toe aan de ByteArray
result
met behulp van de methode
writeByte()
. Wanneer de lus gereed is, bevat de ByteArray
result
16 bytes en is deze gereed voor gebruik als databasecoderingssleutel.
private function generateEncryptionKey(hash:String):ByteArray
{
var result:ByteArray = new ByteArray();
for (var i:uint = 0; i < 32; i += 2)
{
var position:uint = i + 17;
var hex:String = hash.substr(position, 2);
var byte:int = parseInt(hex, 16);
result.writeByte(byte);
}
return result;
}
|
|
|