NAME

DBIO::EncodedColumn - One-way encode selected columns: passwords with Bcrypt or Argon2, tokens with Digest

VERSION

version 0.900004

SYNOPSIS

package MyApp::Schema::Result::User;
use base 'DBIO::Core';

__PACKAGE__->load_components(qw/EncodedColumn/);

__PACKAGE__->add_columns(
  password => {
    data_type           => 'varchar',
    size                => 255,
    encode_column       => 1,
    encode_check_method => 'check_password',
  },
  api_token => {
    data_type     => 'varchar',
    size          => 255,
    encode_column => 1,
    encode_class  => 'Digest',
    encode_args   => { algorithm => 'SHA-256' },
  },
);

my $user = $schema->resultset('User')->new_result({ password => 's3cret' });
$user->check_password('s3cret'); # true

See t/94encoded_column.t for a runnable example.

DESCRIPTION

DBIO::EncodedColumn implements one-way column encoding. Columns are marked with encode_column => 1 in add_columns.

The value is encoded on the way to storage. Reading a column returns the stored (encoded) representation. A verifier method per encoded column is installed.

Classes

encode_class chooses the method:

Bcrypt (default)

Passwords. Crypt::Bcrypt, variant 2b, cost 12 unless encode_args says otherwise. The stored value is the $2b$... string. bcrypt takes values of up to 72 bytes without a NUL byte; a longer value or one with a NUL byte is refused with an exception that points to Argon2.

Argon2

Passwords. Crypt::Argon2, argon2id, with t_cost, m_cost and parallelism from encode_args. The stored value is the $argon2id$... string.

Digest

Tokens and values that are compared by their digest. A salted Digest of SHA-256, SHA-384, SHA-512 or SHA3-224, SHA3-256, SHA3-384, SHA3-512, stored as

dbio$<algorithm>$<salt_base64>$<digest_base64>

The module of the chosen class has to be installed: a class whose module is missing does not load, and the exception names the module.

Every value goes into the method as the bytes of its UTF-8 encoding, when it is written and when it is checked.

What is encoded and what passes through

A value handed to an encoded column through the accessor, new, create or update is encoded, whatever it looks like: a defined value that is neither a reference nor a literal SQL value is always taken as a password. That includes a value that only looks like a hash ($2a$my-own-password, dbio$a$b$c) and the value read from the row itself -- $row->password($row->password) encodes the stored hash a second time. To leave a stored value as it is, do not set it, or use set_column. copy carries the stored value over as it is.

Storing a finished hash

A hash made elsewhere (importing accounts with their hashes) goes past the filter explicitly, with set_column, which the filter does not act on, or with a literal in new/create:

$user->set_column(password => $hash);

$schema->resultset('User')->create({
  name     => 'alice',
  password => \[ '?', $hash ],
});

check_password verifies such a value like any stored one.

Verification

The stored value names its method and the check uses exactly that one: $2...$ values (bcrypt) are checked with Crypt::Bcrypt, $argon2id$... values with Crypt::Argon2, dbio$... values with the algorithm the value names (a name this installation cannot build gives 0; digests of MD5 and SHA-1 that exist in a column stay checkable). The module a check needs has to be installed; without it the check method throws an exception naming the module. A candidate that bcrypt cannot take (over 72 bytes, or with a NUL byte) gives 0.

A stored value that has no hash format at all never matches (accept_unencoded is 0). With accept_unencoded => 1 in encode_args a stored value that equals the candidate matches (compared in constant time, answer 1 or 0) and is written back as a hash of the column's class: the check method updates this one column of the row, only while the database still holds the value that was compared, and warns once. A row that is not in the database yet gets the hash as its column value. A value that claims a hash format but is not a valid one never matches, in either setting.

Serializing and the changelog

A column with encode_column has is_serializable => 0 (it is not part of TO_JSON) and changelog => 0 (DBIO::ChangeLog leaves it out), unless the column definition says otherwise.

Trace

The bind values of an encoded column appear as *** in the SQL trace; see "redact_bind_value" in DBIO::Storage::DBI.

METHODS

add_columns

Component override: gives every column with encode_column the column settings is_serializable => 0 and changelog => 0 unless the definition names them, then delegates to next::method. The changelog setting is handed on as the marker DBIO::ChangeLog reads, so it holds in either order of load_components. Called the same way as the plain add_columns; not normally called directly.

register_column

Component override: after delegating to next::method, checks $info for encode_column and, if present, installs a DBIO::FilterColumn filter_to_storage hook that hashes the value on write (skipping undef, references and literal SQL values) plus a check method (check_$column by default, or encode_check_method). An unknown encode_class, the module of the class missing, an unknown encode_args key or an algorithm outside the Digest list throws here, when the class is loaded. Called the same way as the plain register_column; not normally called directly.

COLUMN OPTIONS

encode_column

Boolean; enables encoding for this column.

encode_check_method

Method name to install for verification. Defaults to check_${column}.

encode_class

Bcrypt (default), Argon2 or Digest. Any other class is refused when the class is loaded.

encode_args

Hashref with optional settings. A key the class does not know is refused when the class is loaded.

cost (Bcrypt)

Integer from 4 to 31, default 12.

t_cost, m_cost, parallelism (Argon2)

Time cost (positive integer, default 3), memory cost (a number with an optional k, M or G suffix, default 32M) and parallelism (positive integer, default 1).

algorithm (Digest)

SHA-256 (default), SHA-384, SHA-512, SHA3-224, SHA3-256, SHA3-384 or SHA3-512; another name is refused when the class is loaded.

salt_length (Digest)

Salt size in bytes, a non-negative integer (default: 16, 0 disables salting).

accept_unencoded (every class)

Boolean, default 0. See "Verification".

RANDOMNESS

Salts come from Crypt::URandom, a required module. Where it cannot deliver the bytes, encoding throws no secure random source -- install Crypt::URandom.

SEE ALSO

DBIO::FilterColumn
DBIO::InflateColumn

AUTHOR

DBIO & DBIx::Class Authors

COPYRIGHT AND LICENSE

Copyright (C) 2026 DBIO Authors Portions Copyright (C) 2005-2025 DBIx::Class Authors Based on DBIx::Class, heavily modified.

This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.