pt>

niedziela, 22 kwietnia 2012

SQLite i szyfrowanie kolumn, pomaga MyBatis i Jasypt

    Niedawno, pisząc wieczorami dla siebie aplikacje w .NET natrafiłem na bardzo przyjemną i lekką bazę danych - SQLite. Tak mi się ta baza spodobała, że postanowiłem sprawdzić czy z poziomu Javy i JDBC uda się do niej dostać. SQLite ma jednak jedną wadę - brak szyfrowania i zabezpieczenia hasłem. Baza SQLite mieści się w jednym pliku, który nie jest w jakikolwiek sposób zabezpieczony. Jako że na codzień pracuję w firmie tworzącej oprogramowanie dla banków i policji brak szyfrowania jest dość poważną wadą nawet w bardzo prostych narzędziach jak program do fakturowania.
    Z jakiego powodu potrzebne jest szyfrowanie samych danych? Wiadomo że błędem w sztuce jest trzymanie haseł w bazie danych - co najwyżej można trzymać posolone hashe. Zaszyfrowanie całej bazy danych (pliku sqlite) również nie wydaje się być dobrym pomysłem z powodów wydajnościowych. Dlatego należy rozpatrzyć szyfrowanie poszczególnych kolumn z danymi wrażliwymi. Jako przykład podam program do faktur gdzie są dane naszych kontrahentów. Co jeśli konkurencja przekupi naszego pracownika by zgrał na dysk USB plik z bazą naszych klientów? Ten problem dotyczy wszelkich aplikacji typu standalone, gdzie przetwarzane są wrażliwe dane. Częściowo ten problem jest rozwiązany w aplikacjach typu klient-serwer lecz nie zawsze jest sens stawiania takiej infrastruktury.
    Wróćmy jednak do bazy SQLite. Domyślnie nie wspiera szyfrowania, dane są w pliku, gdzie można je podejrzeć zwykłym edytorem. To w zasadzie jedyna wada dla tego typu bazy danych. Zaletami są szybkość (śmiało można wydajność porównać do SQL Server 2008 o czym zresztą można wyczytać nie tylko na stronach projektu), mozna definiować procedury oraz triggery (wyzwalacze), bardzo ważne jest wsparcie  dla transakcji (pełne ACID). Te wszystkie przymioty czynią SQLite idealną bazą dla niewielkich aplikacji, a czasem i większych co skutkuje jej niezwykłą popularnością i często nie wiemy nawet, że jej używamy na co dzień.W dzisiejszym wpisie pokażę w jaki sposób dodać szyfrowanie kolumn dla SQLite, zrobimy to w sposób przezroczysty dla użytkownika-programisty. Do dostępu do SQLite użyjemy Mybatisa, do szyfrowania uzyjemy bibliotekę Jasypt, dla osiągnięcia przezroczystości przedefiniujemy typeHandlery, w którym faktycznie będzie odbywało się szyfrowanie. Zatem zaczynamy!

Potrzebne narzędzia i biblioteki :
  • Eclipse - wymienienie tej pozycji to w zasadzie formalność
  • SQLite - sterownik JDBC będący jednocześnie enginem bazy
  • Jasypt - pozwoli nam na łatwe szyfrowanie
  • MyBatis - ukryje przed nami szczegóły dostępu do bazy danych
  • SQLadmin - mały program do administracji bazami SQLite - posłuzy do stworzenia nowej bazy

    Pierwsza rzecz to stworzenie nowego projektu, w Eclipse : New -> Project... -> Java -> Java Project. Następnie tworzę bazę danych w katalogu projektowym za pomocą SQLadmina.


    Ale to nie wszystko, należy jeszcze stworzyć tabelę w bazie danych. Definiujemy tabelę o nazwie "myBean" gdzie będą 2 pola textowe ("name" i "val") oraz klucz główny ID. Klucz główny powienien być typu integer oraz posiadać własność autoinkrementacji - to bardzo ważne gdyż wstawiając dane nie określamy ID : 


    Następnie w katalogu projektowym tworzymy podkatalog "lib" gdzie kopiujemy niezbędne biblioteki dodając je do build path. W tak skonfigurowanym projekcie tworzymy następujce pakiety : example, sqlite, typehandlers oraz bean. W katalogu źródłowym (domyślnie src) projektu tworzymy 2 pliki xml : BatisConf.xml oraz mapper.xml :



    Pierwszą rzeczą, którą zrobię jest skonfigurowanie MyBatisa. Jak można się domyśleć po nazwach chodzi tutaj o plik MybatisConf.xml gdzie zapisuję konfigurację dostępu do bazy SQLite.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
 <environments default="development">
  <environment id="development">
   <transactionManager type="JDBC" />
   <dataSource type="POOLED">
    <property name="driver" value="org.sqlite.JDBC" />    
    <property name="url" value="jdbc:sqlite:kynBase.s3db" />    
   </dataSource>
  </environment>
 </environments>
 <mappers> 
  <mapper resource="mapper.xml"/>
 </mappers> 
</configuration>


    Już objaśniam o co chodzi, w głównym pliku konfiguracyjnym dla MyBatisa. Najpierw określamy ID taga opisującego domyślne środowisko (linia 6), opis tego środowiska znajduje się poniżej (l. 7). Następnie definiujemy datasource podając sterownik JDBC dla bazy, klasę (l. 10) oraz definiujemy url do bazy (l. 11). Ostatnim ważnym fragmentem jest zdefiniowanie lokacji dla mapperów - plików gdzie znajdują się właściwe zapytania SQL dla bazy (l. 16). Czas na zdefiniowanie zapytań do bazy w mapper.xml:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="kynNamespace">

 <resultMap id="beanRes" type="sqlite.bean.MyBean">
  <result property="ID" column="id" />
  <result property="name" column="name" />
  <result property="val" column="val" typeHandler="sqlite.typehandlers.kynStringHandler" />
 </resultMap> 

 <select id="firstSelect" resultMap="beanRes">
  select * from myBean
 </select> 
 <insert id="insMyb" parameterType="sqlite.bean.MyBean"> 
  insert into myBean ( name, val) values ( #{name}, #{val, typeHandler=sqlite.typehandlers.kynStringHandler} )
 </insert>
</mapper>

    Najpierw definiujemy "resultMap" (l. 4), dzieki temu możemy określić w jaki sposób MyBatis ma dokonać mapowania z kolumn tabeli w bazie, na pola obiektu. Sposób mapowania poszczególnych kolumn/pól określamy w tagach <result>, zwrócmy uwagę na ostatni wpis (l. 8) gdzie oprócz mapowania nazw określamy użytego typeHandlera. To dzięki temu podczas odczytu zaszyfrowanej kolumny na wyjściu dostajemy wartość odszyfrowaną. Operacje wybierania SQL definiujemy za pomocą tagów <select> (l. 11), za pomocą atrybutu "resultMap" określamy sposób mapowania rezultatu kolumna -> pole obiektu. Uważny czytelnik spostrzegł zapewne, że klasy wymienionej jako typ rezultatu jeszcze nie napisaliśmy - i słusznie, dopiero to zrobimy. Przejdźmy do operacji wstawiania definiowanej wewnątrz tagów <insert>, gdzie definiujemy typ parametru wyjściowego (l. 14. Kolejna linia również jest ważna gdzie znajduje się właściwa instrukcja SQL wstawiania. Pierwsza część instrukcji (l. 15) nie różni się znacząco od tego co znamy z klasycznego SQL. Najpierw wymieniamy listę kolumn w bazie danych a następnie definiujemy w jaki sposób mają zostać przypisane wartości. W #{nazwa_pola} przypisujemy dane pole obiektu do kolumny w bazie danych, to najprostsza postać. Dla zaszyfrowania kolumny musimy nieco rozbudować definicję - #{val, typeHandler=sqLite.typeHandlers.kynStringHandler} - to oznacza, że dla tej kolumny zostanie użyty typeHandler, który zaraz również zdefiniujemy. Teraz moja drobna uwaga, bardzo skrupulatnie należy sprawdzać treść zapytania by ustrzec się przed literówkami. Czas teraz zdefiniować typeHandler :

package sqlite.typehandlers;

import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.TypeHandler;

public class kynStringHandler implements TypeHandler
{
 String encryptionPass;
 
 BasicTextEncryptor textEncryptor;
 
 public kynStringHandler()
 {
  encryptionPass = "QwertY1234 :)";//albo pobieramy z servera... whatever...
  textEncryptor = new BasicTextEncryptor();
  textEncryptor.setPassword(encryptionPass);  
 }
 

 @Override
 public Object getResult(ResultSet rs, String colName) throws SQLException
 {
  return  textEncryptor.decrypt( rs.getString( colName  )  );  
 }

 @Override
 public Object getResult(CallableStatement cs, int i) throws SQLException
 {
  return textEncryptor.decrypt( cs.getString(i) );
 }

 @Override
 public void setParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException
 {
  ps.setString(i, textEncryptor.encrypt( ((String)parameter) ) );
 }
}


    TypeHandler jest jednym z kluczowych elementów MyBatia, kontrolę działania na etapie odczytu pojedyńczych wierszy z bazy danych. Za każdym razem gdy MyBatis odczytuje pojedyńczy wiersz, tworzy obiekt typeHandlera. Hasło (linia 12) i obiekt szyfrujący, BasicTextEncryptor (l. 14), które to elementy inicjalizujemy w kontruktorze (l. 18-20). Sposób inicjalizowania tych obiektów może być dowolny - ważne by był do nich dostęp z poziomu kluczowych metod hypeHandlera. Gdy odczytujemy dane z bazy musimy je odszyfrować za pomocą tego samego klucza, którym zostały zaszyfrowane (l. 25, 27). Podczas przesyłania danych do bazy musimy je zaszyfrować  (l. 37, 39). Do szyfrowania użyłem bibliotekę Jasypt gdyż bardzo ułatwia szyfrowanie i tworzenie posolonych skrótów (tego akurat nie omawiam w tym tutorialu) skracając jednocześnie kod. Dzięki takiemu podejściu do bazy trafiają już zaszyfrowane dane, dodatkowo tylko wrażliwe dane są szyfrowane co nie powoduje tak dużych narzutów na wydajność jak w przypadku szyfrowania całej bazy danych.
    Następnym elementem układanki jest klasa repezentująca obiekty, tworzone na podstawie wierszy z bazy danych z tabeli "myBean" (patrz początek wpisu).


package sqlite.bean;

public class MyBean
{
 public int ID;
 
 public String name;
 
 public String val;
 
 
 public MyBean()
 {
  ;
 }
 
 public MyBean(String va, String nam)
 {
  val = va;
  name = nam;
 }
 
 public MyBean(int iid, String nam, String va)
 {
  ID = iid;
  name = nam;
  val = va;
 }
}

    W definicji powyższej klasy nie ma nic niezwykłego, konstruktory są dodane dla wygody i nie są wymagane przez Mybatisa, więc nie będę się rozpisywał. Przejdźmy do kodu testującego :


  String resource = "BatisConf.xml";
  Reader reader = Resources.getResourceAsReader(resource);
  SqlSessionFactory sqlMapper = new SqlSessionFactoryBuilder().build(reader);
  
  
  SqlSession session = sqlMapper.openSession();
  System.out.println("begin");
  
  
  for(int i = 0; i < 20; i++)
  {
   session.insert("insMyb", new MyBean("Jeszcze jeden kolejny  :) : " + i * 10, "pole_" + i));
  }
  
  List resList = session.selectList("firstSelect");
  
  for(MyBean mb : resList)
  {
   System.out.println(mb.ID + " : " + mb.name + " : " + mb.val);
  }
    
  session.commit();  
  session.close();

    Pierwszą rzaczą którą należy zrobić chcąc skorzystać z MyBatisa jest stworzenie obiektu SqlSessionFacktory (linia 3) by następnie za jego pomocą stworzyć obiekt reprezentujący sesję do bazy danych (l. 6). Podczas wstawiania rekordów do bazy danych (l. 12) widać prawdziwą wygodę MyBatisa, przesyłamy obiekt i tylko tyle, o resztę nie musimy dbać. Jeszcze wygodniej przedstawia się kwestia odczytu danych (l. 15) gdy jako rezultat dostajemy listę obiektów. z tego poziomu proces szyfrowania jest przezroczysty. na koniec nie pozostaje nic innego jak zatwierdzenie zmian (pamiętajmy że SQLite jest tranzakcyjną bazą wspierającą w pełni postulaty ACID) oraz zamknięcie sesji (l. 22, 23).
    To wszystko! Udało się zaszyfrować dane w bazie, a w dodatku tylko newlargiczne kolumny. Lecz jeszcze ważniejszą rzaczą którą pokazałem był MyBatis. Jest to narzędzie niezwykle wygodne i jego możliwości nie są mniejsza w żadnym wypadku niż klasyczne JDBC, wszystko dzięki możliwości przedifiniowywania typeHandlerów. Biblioteka Jasypt również bardzo ułatwia życie, zaoszczędza parę linii kodu, a wykorzystałem jedynie ułamek jej możliwości. Na koniec podam link do gotowego projektu w Eclipse.

1 komentarz:

  1. Ja proponuję inne rozwiązanie: db4o. Jest to mala lekka obiektowa baza danych. Również zapisuje dane w pojedyńczym pliku i świetnie nadaje się do aplikacji standalone. Wspiera ACID a dane są zapisywane w formacie binarnym. I co najważniejsze i najpiękniejsze: nie trzeba robić kopletnie żadnego mapowania! Model domenowy (obiektowy) jest dokładnie tym co jest przechowywane w bazie! Więc małe aplikacje / prototypy tworzy się jeszcze szybciej.

    Jak byś chciał się przyjrzeć plikom z danymi, to w moim projekcie: http://code.google.com/p/datawander w katalogu testCompiledCode znajdziesz pliki *.yap i możesz sobie sprawdzić jak wygląda format pliku bazy.

    OdpowiedzUsuń