http://qs1969.pair.com?node_id=915483

After reading Debug values encrypt as XXXX and having had similar thoughts before, I have thrown together a candidate solution to the problem of safely passing sensitive data to debugging code, logging code, and external modules. First some code samples:

Update: Revised the usage based on initial reactions (code and original synopsis below readmore remains unchanged). Further revisions to usage and revised code will be uploaded to github within the next couple days (I will add a link here). Further comments on the original or revised usage welcome!

Basic usage

Returns masked value on stringification, returns unmasked value only when explicitly requested.

my $ccn = Text::Hidden->new( "1234567887654321" ); say STDERR "DEBUG: Got a CCN ($ccn)"; # DEBUG: Got a CCN (XXX +XXXXXXXXXXXXX) say "The real CCN: ", $ccn->unhidden_value; # The real CCN: 1234567 +887654321

You may also set a custom obfuscator:

my $ccn = Text::Hidden->new( "1234567887654321", obfuscator => sub { "XXXXXXXXXXXX".substr($_[0], -4) }, );

Creating pinholes

If your sensitive data needs to be processed by external modules you may, of course, pass the unhidden value directly to these modules. However, any logging done or errors thrown by that external module may contain the unhidden sensitive value.

You may instead create a list of exceptional situations where stringification of the Text::Hidden object returns the unmasked text. Such exceptions may be made at the package, sub, or even line level. More complicated situations can be addressed if you provide a CODE reference which may then examine the full call stack.

Basic pinholes are matched against the immediate caller's "PACKAGE::SUB_NAME(LINE_NUMBER)", "PACKAGE::SUB_NAME()", or just "PACKAGE".

my $passwd = Text::Hidden->new( "12345", auto_unhide => [ "Some::Class::Authentication", # anywhere in Authentic +ation package "Some::OtherClass::authenticate()", # anywhere in authentic +ate method "Some::OtherClass::foo(43)", # line 43 in method foo + (FRAGILE!) ], # similar, but search call stack (not just immediate caller) for + a match auto_unhide_recursive => [ ... ], );

As an aid to creating pinhole rules, set the C<debug> option to print a message whenever the object is accessed. Here, we may also wish to temporarily pass the C<default =E<gt> "unhidden"> option so that we can trace the code path for a successful authentication.

my $passwd = Text::Hidden->new( "12345", debug => 1, trace => 0, # set to 1 to show entire call stack );

Then,

say $passwd; say $passwd->unhidden_value; Some::Module->login( $user, $passwd );

may print to STDERR:

Stringification to hidden value at main::(5) Explicit cast to unhidden value at main::(6) Stringification to hidden value at Some::Module::Authentication::log +ger(14) Stringification to hidden value at Some::Module::Authentication::aut +henticate(43)

Default Unhidden

While not generally reccommended, this module supports a "default unhidden" mode of operation which will stringify to the unhidden value except when blocked.

my $ccn = Text::Hidden->new( "1234567887654321", default => "unhidden", hide_from_recursive => [ qr/^DB[ID]/ ], );

TODO: Decide "order" policy: fixed?; depends on "default"? (no probably not); configurable?

Localized default policy

Localizing the default policy can be done on-the-fly which provides a middle ground between pre-declaring all unhidden access points and a default unhidden policy.

Localized default policies still provide an advantage over working with a plain string in that the policy may still be overridden via "hide_from", "auto_unhide", or the global C<$Text::Hidden::Force> variable described in the next section.

{ my $key = $ccn->localized_default_unhidden; # All uses return unmasked value unless hide_from patterns match print "$ccn"; Some::Module->charge_money( $name, $ccn ); } # Return to default hidden policy now that key is dropped { my $key = $ccn->localized_default_hidden; # ... }

Forcing masking (or unmasking)

Masking may be forced by locally setting C<$Text::Hidden::Force>. For instance, a C<__DIE__> or C<__WARN__> handler might want to force a hidden policy to reduce the danger of leakage. Setting this variable overrides all "hide_from", "auto_unhide", or default policies.

$SIG{__DIE__} = sub { local $Text::Hidden::Force = "hidden"; # ... };

I am not entirely convinced myself that a module such as this is a good idea / necessary. For instance, perhaps it is just a better idea to be more careful with your data. However, I have seen the question arise a couple times before and have myself occasionally wished (in passing) for such a module. Are there existing modules which implement this or a similar scheme? (previous searches and a quick search now did not turn up anything.) Is an approach like this even a good idea? Is there a better way?... Should I clean this up and publish this approach to CPAN? If so, is there a better name for it (String::Obfuscate, String::AutoObfuscate, String::Mask, Data::Mask, Text::Mask...)?

Update: Move POD to bottom of module

package SecureString; use 5.010; use strict; use warnings; use re 'taint'; use autodie; our $VERSION = 0.0000;# Created: 2011-07-18 use Carp qw/ confess cluck /; use overload 'bool' => \&_string, '""' => \&_string, '0+' => \&_number, ; use Hash::Util::FieldHash qw(id register); my %STRING; sub new { my ($class, $value, %opt) = @_; my $self = bless { obfuscator => \&_default_obfuscator, cache_masked => 1, recompute_masked => 1, %opt }, $class; register( $self, \%STRING ); $self->set( $value ); return $self; } sub set { my $self = shift; $STRING{id $self} = shift; delete $$self{masked} if $$self{recompute_masked} and $$self{obfusca +tor}; return $self; } sub get { my $self = shift; return $self unless $self->_match_caller( $$self{allow} );# Default +allow all return $STRING{id $self}; } sub get_masked { my $self = shift; return $$self{masked} if $$self{masked}; my $masked; $masked = $$self{obfuscator}->($STRING{id $self}) if $$self{obfuscat +or}; $masked //= 'XXXXX'; $$self{masked} = $masked if $$self{cache_masked}; return $masked; } ## Completely untested: sub STORABLE_freeze { my ($self, $cloning) = @_; # Allow cloning, but do not save value when storing my $value = $cloning ? $STRING{id $self} : $self->get_masked; return ($value, $self); } sub STORABLE_thaw { my ($self, $cloning, $value, $obj) = @_; %$self = %$obj; $STRING{id $self} = $value; } ## Doesn't work: sub yaml_dump { my $self = shift; YAML::Node->new( $self->get_masked ); } sub _string { my $self = shift; if ($$self{cluck} and (1 eq $$self{cluck} or $self->_match_caller( $ +$self{cluck} ))) { $self->_show_caller("Attempt to access string value of ".$self->ge +t_masked); cluck "\n"; } return $self->get_masked unless $self->_match_caller( $$self{auto_ge +t} || [] );# Default do not match return $STRING{id $self}; } sub _number { confess "Attempted to use SecureString as a number"; } sub _show_caller { my ($self, $msg, $level) = @_; $level = $level ? $level + 1 : 2; my ($pkg, $file, $line, $sub) = $self->_caller($level); print STDERR "$msg at $sub($line)"; } sub _caller { my ($self, $level) = @_; my ($pkg, $file, $line) = caller($level); my (undef, undef, undef, $sub) = caller($level+1); $_ //= "" for $pkg, $file, $line, $sub; $sub ||= "${pkg}::"; return ($pkg, $file, $line, $sub); } sub _match_caller { my ($self, $match, $level) = @_; return 1 unless $match; $level = $level ? $level + 1 : 2; my @caller = $self->_caller($level); for ('ARRAY' eq ref($match) ? @$match : $match) { return 1 if $self->_match_caller_item( $_, $level+1, @caller ); } return 0; } sub _match_caller_item { my ($self, $match, $level, $pkg, $file, $line, $sub) = @_; given (ref($match)) { when ('') { return 1 if $match eq $pkg or $match eq "$sub() +" or $match eq "$sub($line)" } when ('Regexp') { return 1 if "$sub($line)" =~ $match } when ('CODE') { return 1 if $match->($level+1, $pkg, $file, $li +ne, $sub) } defult { die "Do not know how to match caller against item of type + $_" } } return 0; } sub _default_obfuscator { "X"x(length($_[0])) } 1; __END__ =pod =head1 NAME SecureString - Obfuscated strings exept when you need them =head1 SYNOPSIS use strict; use SecureString; # for simple cases (beware passing value from -get() to external modu +les!): my $CreditCardNumber = SecureString->new( "1234567887654321" ); say STDERR "DEBUG: Got a CCN ($CreditCardNumber)"; # DEBUG: Got a C +CN (XXXXXXXXXXXXXXXX) say "The real CCN: ", $CreditCardNumber->get; # The real CCN: +1234567887654321 # more complex: use YAML; my $CreditCardNumber = SecureString->new( "1234567887654321", auto_get => qr/^Business::OnlinePayment/, # probably a bit + too permissive allow => "My::Secure::Module", obfuscator => sub { "XXXXXXXXXXXX".substr($_[0], -4) }, ); my %tx_info = ( card_number => $CreditCardNumber, ... ); print Dump \%tx_info; # "card_number: +XXXXXXXXXXXX4321" my $tx = new Business::OnlinePayment("AuthorizeNet"); $tx->content( %tx_info ); $tx->submit; # sends actual c +ard number to AuthorizeNet # debugging and diagnosis (stack trace whenever stringified) # Use stack traces to set appropriate "auto_get" above my $CreditCardNumber = SecureString->new( "1234567887654321", cluck => 1, ); =head1 DESCRIPTION Creates a value which will be obfuscated unless accessed in a particul +ar way. Access can be restricted to specific classes or even specific subroutines/methods. There are no methods which unconditionally return the unmasked string value, thus even code which attempts to walk all defined methods (but +who does that!?) will fail to output the unmasked value unless it has been granted permission. TODO: Storable and YAML hooks have been defined so that these modules +can be safely used with SecureStrings. Patches accepted to support any oth +er serialization modules. The unmasked string is stored "inside-out" so t +hat at worst, unsupported serialization modules will export only the non-sensitive configuration data. =head1 USAGE =head3 new =over 4 =item auto_get ArrayRef or single item describing packages and/or subs for which stringification should yield the unmasked value. This allows you to pa +ss SecureStrings to external dependencies and have them stringify to thei +r unmasked value only when necessary. =item allow ArrayRef or single item describing packages and/or subs for which call +ing the C<get> method will yield the unmasked value. If allow is not speci +fied, the C<get> method will always return the unmasked value. =item obfuscator CODE reference which takes a sensitive (unmasked) value as its first argument and returns a safe (masked) value. The default obfuscator ret +urns a string of "X"s equal to the length of the sensitive value. =item masked Explicitly specify the masked string value. =item cluck When "1", stringification will print a stack trace to STDERR for debug +ging purposes. May also be an ArrayRef or single item describing packages a +nd/or subs for which stringification should print a stack trace. =back =head3 set $str->set( $value ) Set the unmasked string value to C<$value>. ** Note C<$str = $value> i +s WRONG! =head3 get $str->get() Attempts to get unmasked value. Will silently return the masked value +if the C<allow> parameter was set at object construction time and the cur +rent caller is not allowed to access the unmasked value. =head3 get_masked $str->get_masked() Unconditionally returns the masked value for all callers. =head1 CALLER SPECIFICATION The C<allow>, C<auto_get>, and C<cluck> parameters accept caller descriptions. These can take the form of: =over 4 =item string Must exactly match the immediate caller's C<CLASS>, C<CLASS::SUBNAME() +>, or C<CLASS::SUBNAME(LINE_NO)>. For example: "main" # anywhere in main package "main::()" # outside any sub in main "main::foo()" # anywhere in sub foo of package main "main::(34)" # line 34 outside any sub in main "main::foo(42)" # line 42 in sub foo of package main # Similarly for package "My::Class" "My::Class" "My::Class::()" "My::Class::foo()" "My::Class::(45)" "My::Class::foo(67)" Of course, including a line number in the specification is rather frag +ile so shouldn't be used in most situations. =item Regexp Will be applied to the C<CLASS::SUBNAME(LINE_NO)> form of the caller. =item CODEREF Will be passed arguments: $level, $pkg, $file, $line, $sub from the perspective of the code ref. The level may be used to walk up + the call stack if necessary. Example which matches all authentication meth +ods in My::Class: sub { my ($level, $pkg, $file, $line, $sub) = @_; return ( $pkg eq "My::Class" and $sub =~ /^_?authenticate/ ); } =back =head3 Inheritance and importing Class and sub name will always be the class and sub name from the parent/exporting class. For example: my $CC = SecureString->new( "12345", cluck => 1 ); package My::Class; sub foo { say $CC; } foo(); # Attempt to access string value of XXXXX at My +::Class::foo(62) package bar; our @ISA = ("My::Class"); *baz = \&My::Class::foo; bar->foo(); # Attempt to access string value of XXXXX at My +::Class::foo(62) baz(); # Attempt to access string value of XXXXX at My +::Class::foo(62) =head1 SERIALIZATION SUPPORT In general, this module will serialize to the masked value. The one exception is C<Storable::dclone()> which, like thread cloning, doesn't really count as serialization and therefore gets a proper copy of the SecureString object. When possible, serialization will collapse to a plain (masked) string though some serialization hooks to not allow changing object type (or serializing a blessed object to an unblessed scalar) so in these cases +, deserialization will construct a SecureString whose masked and unmaske +d values are the same (both are the original masked values). =head2 Stable Support No serializer hooks are considered stable at this time =head2 Experimental Support =head3 Storable Does not (as far as I can tell) allow changing type to unblessed scala +r, thus C<thaw(freeze($secure_string))> will produce a SecureString of th +e masked value. Storable allows dclone to be treated specially. Therefore, since dclon +e can not result in accidental information leakage, C<dclone($secure_string) +> produces a usable (the unmasked value remains in tact) SecureString. =head3 YAML Alas, doesn't work... =head1 INTERNAL (PRIVATE) METHODS =head3 _string Clucks if appropriate then returns masked value unless the auto_get parameter matches the current caller. =head3 _number Simply dies. Numerical overloading calls this so that masked values ar +e never used in calculations. =head3 _show_caller $self->_show_caller( $message, $level = 1 ) Displays message to STDERR and current caller spec (as would be matche +d by C<_match_caller>. =head3 _caller my ($pkg, $file, $line, $sub) = $self->_caller( $level ) Compute a caller by our definition. =head3 _match_caller $self->_match_caller( $match_data, $level = 1 ) Computes caller information and loops over match items attempting to f +ind any match. If C<$match_data> is undefined, then match succeeds. If C<$match_data> is an empty array, the match fails. =head3 _match_caller_item $self->_match_caller_item( $match, $level, @caller ) Returns true if C<$match> matches the current C<@caller> (at C<$level> +). Can handle scalar, Regexp, or CODE match types. Dies on any other matc +h types. =head3 _default_obfuscator Returns string of "X"s equal in length to first argument. =head1 AUTHOR Dean Serenevy dean@serenevy.net http://dean.serenevy.net/ =head1 COPYRIGHT This module is Copyright (c) 2011 Dean Serenevy. All rights reserved. You may distribute under the terms of either the GNU General Public Li +cense or the Artistic License, as specified in the Perl README file or L<per +lartistic>. =head1 DISCLAIMER OF WARRANTY BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY + FOR THE SOFTWARE, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE SOFTWARE "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPR +ESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK +AS TO THE QUALITY AND PERFORMANCE OF THE SOFTWARE IS WITH YOU. SHOULD THE SOFTWARE PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICI +NG, REPAIR, OR CORRECTION. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR REDISTR +IBUTE THE SOFTWARE AS PERMITTED BY THE ABOVE LICENCE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE SOFTWARE (INCLU +DING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR L +OSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE SOFTWARE TO OPER +ATE WITH ANY OTHER SOFTWARE), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.

Good Day,
    Dean

Replies are listed 'Best First'.
Re: RFC: SecureString - Obfuscated / masked strings exept when you need them
by Tanktalus (Canon) on Jul 19, 2011 at 22:20 UTC

    Conceptually, I love it. Codewise, I've not looked :-)

    Basically, I have code like this at the top of every function:

    my $self = shift; $self->trace_entry(@_);
    (trace_entry jsonishly serialises everything to the trace file.) Without this, I have to pull the password out of the tracing, and do other funky things just to ensure that the password doesn't show up. In fact, I've gone and reversed the whole thing by pushing the password off into another module altogether, and doing non-standard tracing there, just so that passwords don't end up in the trace. The downside is that the password object is global/a singleton. And, while doing this might be doable, it's a bit more painful when I pass around a hash of options through multiple layers between the part that starts with the password, and underlying code that might need it.

    Something like this would be extremely useful, I think. But putting it in the top-level of namespaces seems a bit off :-) Also, calling it "Secure" might be misleading as well. Perhaps "Text::Hidden"?

    Looking a bit more at the code, I see you've gone a little more over the top than I would have thought. Just reading the intro, what I had originally thought was "return the X's for stringification, and the real value for ->get". Because, honestly, this would likely solve nearly the whole problem anyway. Code that just treats it as a string (such as logging) would get the X's, but code that needed the value would already know it's a password (hidden object) and use the ->get. Restricting which pieces of code that actually realise it's a password object and call it correctly from being able to get the value is more of a straight-jacket than I'm used to in Perl. (But about right for Java or C++.) Basically, it's just saying "if you know what you're doing, you get it, but if you're just blindly blasting things around, you're not going to get it." The override ability where modules you may not control get access to it is nice, but, again, you then have to worry about them logging it, which now they can do automatically again.

      Thanks for your comments, concept is most of my concern.

      I do tend to agree with you about the "allow" parameter, it fell in by accident while I was exploring some possibilities. I will take your comment as at least partial support for considering removal of that (mis)feature.

      The "auto_get" option is a different kettle of fish though. That can allow you to pass a "Text::Hidden" object to a module that needs to actually use the data at some point (but may attempt to log its value at other points - I can restrict auto_get to a specific method or even a specific line of code). Using "auto_get" does break open the black box of the external module and may break things if the module reorganizes itself too much. On the one hand, it makes me a bit nervous to use such a feature, but on the other hand, I don't see how "Text::Hidden" could be useful if it did not allow passing usable instances into external code.

      Update: On an additional reading, I do see that you had distinguished "allow" from "auto_get", but I still want to point out that access can be restricted more finely than by class name and that the only alternative to such an "auto_get" feature is passing the raw unmasked string to the external module. So, I guess my question for you (and others) is whether you would use the "auto_get" option at the method (or line number) level?

      Good Day,
          Dean

        I agree with Tanktalus' comments. I wanted to read the code but it is quite hard to find the code with the POD all mixed up with it. Please consider making the code easier to read and the POD easier to read (and both easier to maintain) by keeping them separate.

        I would rename the get() method to something very distinctive. It would be useful to be able to search for all instances of, for example, unhide_string() to audit the places where the sensitive data is being exposed to ensure they are all appropriate.

        I can see the motivation for auto_get() but I also don't think it will be enough of a solution.

        I'd probably go for a more direct approach at preventing specific leakage. The first thing is to prevent the sensitive string from being logged.

        So provide a class that you can tie the log file handle to such that any uses of the file handle set a global "unsecure" flag:

        sub Text::Hidden::Handle::PRINT { local( $Text::Hidden::SECURE ) = 0; ... } sub Text::Hidden::as_string { my( $self ) = @_; return $self->{value} if $SECURE; return $self->get_obscured_value(); }

        Another place you don't want to leak such information is into a database. I'd be tempted to walk caller() information looking for DBI or DBD::* modules, though I somewhat worry about the efficiency of that. Perhaps one only need walk a couple of levels up for such a check, though.

        So, there will be some situations where you can unhide the sensitive string only a couple of places where it is actually used. There will be other situations where the value is used in in a bunch of code, some of which you have no control over and you just want to identify the few places where information can escape the process and block those exits. auto_get() may be sufficient for many of these second cases but I also think it will be harder to get working that way.

        And that last point means that you should provide a debug option that logs the places where the string value was asked for and whether a hidden or unhidden version was provided.

        - tye        

Re: RFC: SecureString - Obfuscated / masked strings exept when you need them
by Anonymous Monk on Jul 19, 2011 at 14:54 UTC

      Well, yes, this not about long-term storage, this is about protecting the data between receiving the data from the client (say, a query parameter) and actually using the data. See, for example, the Business::OnlinePayment example. It is certainly reasonable to log all transactions which are sent to the payment processing site. Of course, to do that one needs to obfuscate the credit card number. In tight code, this should be reasonably easy to do, but of course, 1) not all code is tight and 2) even in tight code it could be convenient to not have to worry about sensitive data leakage. I could certainly be convinced that something like this module is too much (or more precisely makes promises that it can not reliably deliver), but I don't see how you can "not store such data in the first place" - one has to store sensitive information in variables between receipt of the value and use of the value. This module is for that period.

      Update: In particular, this helps saitsfy the PCI DSS requirement (3.2, 3.4) that no sensitive data leak into logs (error logs, trace logs, ...). Some of these error logs may originate from external modules that aren't entirely under your control.

      Good Day,
          Dean

Re: RFC: SecureString - Obfuscated / masked strings exept when you need them
by iguanodon (Priest) on Jul 19, 2011 at 18:26 UTC
    Sorry if I'm missing the point, but why can't you just not log the sensitive data?

      Indeed, possible. That falls under the "Be more careful" option, however, the assumption of "CONSTANT VIGILANCE!" is the enemy of good security. Up until now, I have stuck with the constant vigilance approach, but it can get difficult. For instance, some systems save values/query parameters into some form of "global" request object/hash then pass that thing around. While that is a bad idea security-wise (for exactly this issue), it is not an uncommon approach and can be done in a reasonable way (meaning, I have seen at least one system that did this that was robust and not painful to work with).

      So far, I think that an approach such as SecureString would be easier/safer in these situations, and probably also in more security-ideal situations. Of course, I've been mulling the idea around subconsciously for a while and I wrote the thing, so of course it looks like a good idea to me. I am not yet sure whether this type of approach falls in the "good idea" camp or the "gimmick that on the surface looks like a good idea, but falls down in practice or leads to bad practices or is just plain silly" camp.

      Good Day,
          Dean

Re: RFC: SecureString - Obfuscated / masked strings exept when you need them
by sundialsvc4 (Abbot) on Jul 19, 2011 at 20:07 UTC

    Voted up.   Dealing efficiently with PCI requirements is always a nice thing.

Re: RFC: SecureString - Obfuscated / masked strings exept when you need them
by thargas (Deacon) on Jul 22, 2011 at 16:42 UTC

    One question: what about Data::Dumper and friends? Sure what you've done is interesting, but if a password is part of a structure or object which gets dumped, you're no further ahead. I'd suggest storing the value as sub { $value }. At least it won't be so obvious then. Otherwise, I like it.

    Oh yes. Also I don't think that the default obfusticator ought to use the length of the value in the masked string as that leaks information, i.e. the length of the value.

    And I prefer Text::Masked

      Yes, very important. I store the sensitive value Inside-Out, and incompatilibity with data serialization tools is one of the classic (dis)advantages of inside-out objects. The serializer simply can not access the sensitive data since the sensitive value is stored in a lexical variable in the Text::Hidden package.

      Since debugging is often done with these serialization tools, in future versions I intend to try to serialize as smartly as possible: make it clear in the serialized output that the value has been masked; warn or die if a de-serialized object is used. Of course, I can only support the most popular serializers, but even those which will not have built-in support will not be able to access the sensitive value - even if they dump code references.

      Regarding length of the value: Yes, I had waffled on that for a bit but for debugging purposes (detecting the empty string) went with length for now since overriding the obfuscated value to be a fixed string is easy. I will probably make the default a bit more safe/smart and even more debug-friendly in the next version. My plan now is to return: "«empty»" | "«undef»" | "XXXXX" (fixed length) as appropriate from the default obfuscator.

      Good Day,
          Dean