Skip to content

Commit

Permalink
Merge pull request #19 from paragonie/fieldsets
Browse files Browse the repository at this point in the history
Implement EncryptedRow.
  • Loading branch information
paragonie-scott authored Jun 21, 2018
2 parents ff8d680 + 9a30ecf commit 96f6300
Show file tree
Hide file tree
Showing 7 changed files with 1,164 additions and 7 deletions.
89 changes: 89 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ $engine = new CipherSweet($provider);
Once you have an engine in play, you can start defining encrypted fields and
defining one or more **blind index** to be used for fast search operations.

### EncryptedField

This will primarily involve the `EncryptedField` class (as well as one or more
instances of `BlindIndex`), mostly:

Expand Down Expand Up @@ -264,6 +266,93 @@ array(2) {
*/
```

### EncryptedRow

An alternative approach for datasets with multiple encrypted rows and/or
[encrypted boolean fields](https://github.com/paragonie/ciphersweet/blob/master/docs/solutions/01-boolean.md)
is the `EncryptedRow` API, which looks like this:

```php
<?php
use ParagonIE\CipherSweet\BlindIndex;
use ParagonIE\CipherSweet\CipherSweet;
use ParagonIE\CipherSweet\CompoundIndex;
use ParagonIE\CipherSweet\EncryptedRow;
use ParagonIE\CipherSweet\Transformation\LastFourDigits;

/** @var CipherSweet $engine */
// Define two fields (one text, one boolean) that will be encrypted
$row = (new EncryptedRow($engine, 'contacts'))
->addTextField('ssn')
->addBooleanField('hivstatus');

// Add a normal Blind Index on one field:
$row->addBlindIndex(
'ssn',
new BlindIndex(
'contact_ssn_last_four',
[new LastFourDigits()],
32 // 32 bits = 4 bytes
)
);

// Create/add a compound blind index on multiple fields:
$row->addCompoundIndex(
(
new CompoundIndex(
'contact_ssnlast4_hivstatus',
['ssn', 'hivstatus'],
32, // 32 bits = 4 bytes
true // fast hash
)
)->addTransform('ssn', new LastFourDigits())
);

// Notice: You're passing an entire array at once, not a string
$prepared = $row->prepareRowForStorage([
'extraneous' => true,
'ssn' => '123-45-6789',
'hivstatus' => false
]);

var_dump($prepared);
/*
array(2) {
[0]=>
array(3) {
["extraneous"]=>
bool(true)
["ssn"]=>
string(73) "nacl:wVMElYqnHrGB4hU118MTuANZXWHZjbsd0uK2N0Exz72mrV8sLrI_oU94vgsWlWJc84-u"
["hivstatus"]=>
string(61) "nacl:ctWDJBn-NgeWc2mqEWfakvxkG7qCmIKfPpnA7jXHdbZ2CPgnZF0Yzwg="
}
[1]=>
array(2) {
["contact_ssn_last_four"]=>
array(2) {
["type"]=>
string(13) "3dywyifwujcu2"
["value"]=>
string(8) "805815e4"
}
["contact_ssnlast4_hivstatus"]=>
array(2) {
["type"]=>
string(13) "nqtcc56kcf4qg"
["value"]=>
string(8) "cbfd03c0"
}
}
}
*/
```

With the `EncryptedRow` API, you can encrypt a subset of all of the fields
in a row, and create compound blind indexes based on multiple pieces of
data in the dataset rather than a single field, without writing a ton of
glue code.

## Using CipherSweet with a Database

CipherSweet is database-agnostic, so you'll need to write some code that
Expand Down
67 changes: 64 additions & 3 deletions docs/solutions/01-boolean.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,81 @@ minimize data leaks.

## CipherSweet Features for Protecting Boolean Fields

### Util::boolToChr() and Util::chrToBool()
### EncryptedRow

The simplest solution is to use `EncryptedRow` instead of `EncryptedField`.

Instead of operating on naked string data, `EncryptedRow` operates on a
one-dimensional associative array. Fields will be encrypted in-place and
compound blind indexes (i.e. a blind index constructed of multiple fields
at once) are much easier to use.

For example:

```php
<?php
use ParagonIE\CipherSweet\Backend\FIPSCrypto;
use ParagonIE\CipherSweet\Backend\ModernCrypto;
use ParagonIE\CipherSweet\BlindIndex;
use ParagonIE\CipherSweet\CipherSweet;
use ParagonIE\CipherSweet\CompoundIndex;
use ParagonIE\CipherSweet\EncryptedRow;
use ParagonIE\CipherSweet\Transformation\LastFourDigits;

/** @var CipherSweet $engine */

// Define two fields (one text, one boolean) that will be encrypted
$row = (new EncryptedRow($engine, 'contacts'))
->addTextField('ssn')
->addBooleanField('hivstatus');

// Add a normal Blind Index on one field:
$row->addBlindIndex(
'ssn',
new BlindIndex(
'contact_ssn_last_four',
[new LastFourDigits()],
32 // 32 bits = 4 bytes
)
);

// Create/add a compound blind index on multiple fields:
$row->addCompoundIndex(
(
new CompoundIndex(
'contact_ssnlast4_hivstatus',
['ssn', 'hivstatus'],
32, // 32 bits = 4 bytes
true // fast hash
)
)->addTransform('ssn', new LastFourDigits())
);
```

In the above example, since the `contact_ssnlast4_hivstatus` blind index
depends on the last 4 digits of the contact's social security number AND
the boolean hivstatus field, it has a keyspace larger than 1 bit, and
thus leaks less information via hash collisions.

### EncryptedField

Safely storing boolean fields with the `EncryptedField` API, rather than
the `EncryptedRow` API, is possible but requires a bit more glue code.

#### Util::boolToChr() and Util::chrToBool()

CipherSweet provides a congruent method for compacting a nullable boolean
into one character

### The Compound Transformation
#### The Compound Transformation

The first rule for protect boolean fields is to **never create a blind index
on a boolean field in isolation.**

Instead, consider using the `Compound` transformation to combine
multiple values together.

### Example Snippet
#### Example Snippet

This code assumes an abstract `$dbh` object that supports an API that
looks like this:
Expand Down
179 changes: 179 additions & 0 deletions src/CompoundIndex.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
<?php
namespace ParagonIE\CipherSweet;

use ParagonIE\CipherSweet\Contract\TransformationInterface;
use ParagonIE\CipherSweet\Transformation\Compound;

/**
* Class CompoundIndex
* @package ParagonIE\CipherSweet
*/
class CompoundIndex
{
/**
* @var array<int, string> $columns
*/
protected $columns;

/**
* @var bool $fastHash
*/
protected $fastHash;

/**
* @var array $hashConfig
*/
protected $hashConfig;

/**
* @var string $name
*/
protected $name;

/**
* @var int $outputLength
*/
protected $filterBits = 256;

/**
* @var array<string, array<int, TransformationInterface>>
*/
protected $columnTransforms = [];

/**
* @var Compound
*/
private static $compounder;

/**
* CompoundIndex constructor.
*
* @param string $name
* @param array<int, string> $columns
* @param int $filterBits
* @param bool $fastHash
* @param array $hashConfig
*/
public function __construct(
$name,
array $columns = [],
$filterBits = 256,
$fastHash = false,
array $hashConfig = []
) {
$this->name = $name;
$this->columns = $columns;
$this->filterBits = $filterBits;
$this->fastHash = $fastHash;
$this->hashConfig = $hashConfig;
}

/**
* @return Compound
*/
public static function getCompounder()
{
if (!self::$compounder) {
self::$compounder = new Compound();
}
return self::$compounder;
}

/**
* @param string $column
* @param TransformationInterface $tf
* @return self
*/
public function addTransform($column, TransformationInterface $tf)
{
$this->columnTransforms[$column][] = $tf;
return $this;
}

/**
* @return array<int, string>
*/
public function getColumns()
{
return $this->columns;
}

/**
* @return string
*/
public function getName()
{
return $this->name;
}

/**
* @return bool
*/
public function getFastHash()
{
return $this->fastHash;
}

/**
* @return int
*/
public function getFilterBitLength()
{
return $this->filterBits;
}

/**
* @return array
*/
public function getHashConfig()
{
return $this->hashConfig;
}

/**
* @param string $column
* @return array<int, TransformationInterface>
*/
public function getTransforms($column)
{
if (!\array_key_exists($column, $this->columns)) {
return [];
}
return $this->columnTransforms[$column];
}

/**
* Get a packed plaintext for use in creating a compound blind index
* This is a one-way transformation meant to be distinct from other inputs
* Not all elements of the row will be used.
*
* @param array $row
*
* @return string
* @throws \Exception
*/
public function getPacked(array $row)
{
/** @var array<int, string> $pieces */
$pieces = [];
/** @var string $col */
foreach ($this->columns as $col) {
if (!\array_key_exists($col, $row)) {
continue;
}
/** @var string $piece */
$piece = $row[$col];
if (!empty($this->columnTransforms[$col])) {
foreach ($this->columnTransforms[$col] as $tf) {
if ($tf instanceof TransformationInterface) {
/** @var string $piece */
$piece = $tf($piece);
}
}
}
$pieces[$col] = $piece;
}
$compounder = self::getCompounder();
return (string) $compounder($pieces);
}
}
8 changes: 4 additions & 4 deletions src/EncryptedField.php
Original file line number Diff line number Diff line change
Expand Up @@ -211,9 +211,9 @@ protected function getBlindIndexRaw(
);
}

$Backend = $this->engine->getBackend();
$backend = $this->engine->getBackend();
$subKey = new SymmetricKey(
$Backend,
$backend,
\hash_hmac(
'sha256',
Util::pack([$this->tableName, $this->fieldName, $name]),
Expand All @@ -225,13 +225,13 @@ protected function getBlindIndexRaw(
/** @var BlindIndex $index */
$index = $this->blindIndexes[$name];
if ($index->getFastHash()) {
return $Backend->blindIndexFast(
return $backend->blindIndexFast(
$plaintext,
$subKey,
$index->getFilterBitLength()
);
}
return $Backend->blindIndexSlow(
return $backend->blindIndexSlow(
$plaintext,
$subKey,
$index->getFilterBitLength(),
Expand Down
Loading

0 comments on commit 96f6300

Please sign in to comment.