One-time-pad encryption with R and basic JavaScript

One-time-pad encryption offers unbreakable* security if you can guarantee truly random number generation and security of the generated pad. Implementing it from the perspective of a cryptographic layman is a fun way to learn more about it and could add an extra layer of security to your most sensitive communications.

A format of one-time pad used by the U.S. National Security Agency, code named DIANA. The table on the right is an aid for converting between plain txt and cipher text using the characters at left as the key. The black circle is an artifact of copying, due to a hole punched in the original.
(Image and text copied verbatim from: https://en.wikipedia.org/wiki/File:NSA_DIANA_one_time_pad.tiff)

I started off by trying to mimic the NSA example above. I eventually settled on a 169 character key (mapped as a 13 x 13 grid) as I struggled to encode more characters into the QR codes. It still works fine for short messages and as a proof of concept, it’s more than enough.

The R code is short and simple and is provided below. It starts off by generating 13 x 13 random numbers ranging from 0 to 25, to be mapped as A to Z. It also uses the qrcode library to generate a QR code of the encryption key. This is then exported to a PDF file, to be printed and distributed to both the sender and receiver.

#==========================
# Simple one-time-pad generator
# with QR code generation for encryption key
#
# J. van der Linde (jvdl@jvdl.me)
#
# Updated 30 April 2021
#==========================

rm(list = ls())

library(dplyr)
library(qrcode)
library(grid)
library(gridExtra)

#==========================

setwd("~/R/Projects/onetimepad/")

#export pdf to file
pdf("~/R/Projects/onetimepad/otp.pdf")

#initialise with number of rows, cols, and sheets of paper to generate
dim_row <- 13
dim_col <- 13
total_chars <- dim_row * dim_col
total_sheets <- 3

for (i in 1:total_sheets) {
    print(paste("Producing sheet no.", i, "of", total_sheets))
    otp_num <- floor(runif(total_chars, min = 0, max = 26)) #generate from 0 to 25
    otp_char <- replace(otp_num, TRUE, LETTERS[unlist(otp_num + 1)]) #rewrite numbers 0-25 to A-Z
    otp_string <- paste(unlist(otp_char), collapse = '') #create a continuous string for the QR code
    
    otp_num_matrix <- matrix(otp_num, nrow = dim_row, ncol = dim_col, byrow = TRUE) #create a numeric matrix
    otp_char_matrix <- matrix(otp_char, nrow = dim_row, ncol = dim_col, byrow = TRUE) #create a char matrix
    
    otp_char_df <- data.frame(otp_char_matrix) #create dataframe of char matrix
    rownames(otp_char_df) <- seq(dim_row) #set rownames as 1 ... x
    colnames(otp_char_df) <- seq(dim_col) #set colnames as 1 ... y
    
    #output the table of chars to the PDF
    grid.table(otp_char_df)
    grid.text(paste("SHEET", i, "OF", total_sheets), x = unit(0.5, "npc"), y = unit(0.95, "npc"))
    
    #print QR code of OTP to PDF
    qrcode_gen(otp_string)
    
    if (i != total_sheets){
        grid.newpage()
    }
}

#close pdf device
dev.off()

Below is an excerpt from the PDF output. The idea is that two copies of this document should be printed: one for the sender and one for the receiver. The OTP should be handed over in person to ensure it is not intercepted. Both should store the OTP somewhere safe and destroy this electronic copy.

PDF of generated OTP encryption keys and their corresponding QR codes

Once the need arises to send a message with extra confidentiality, our protagonists, let’s call them Alice and Bob in line with tradition, have to agree on a code to use. They may have prearranged to start at the first sheet and continue sequentially. Or they can decide on a sheet spontaneously; for example, Alice can simply send Bob a text saying, “use sheet 3 today”.

In our scenario, let’s assume Alice needs to meet up with Bob. Perhaps they are concluding a confidential transaction or protesting for democracy in their country. She wishes to send the message, “SEE YOU AT ELEVEN AM IN THE TOWN SQUARE”. Firstly she uses a secure instant messenger like Signal to initiate the conversation with Bob.

Alice: Hi FreedomProtestor123. Use sheet 3 today. Message follows soon.

Bob: Hi DemocracyGirl42, will do.

Current events…

By using modular addition (our code is only for A-Z, numbered 0 to 25) Alice calculates the encrypted message. She adds the first letter of the message to the first letter of the key, modulo 26. She repeats for the 2nd letter of the message and the second letter of the key and so on. She then maps the resulting numbers of the modular addition back to letters.

This can also be done fairly easily with the help of computers. Alice could have used her phone to scan the QR code provided next to the old-fashioned grid and completed the process via app or webpage. Alice must ensure she can trust everything in this chain, from the QR code scanner to not having her internet traffic intercepted.

I link here to a basic HTML/JS example which can either be hosted online or stored offline for use. The code is deliberately kept simple and clean (no CSS) to allow easy verification of the source and to decide whether or not to trust it. You can find my implementation hosted at http://jvdl.me/otp.html ; since it is a simple HTML page any phone or computer can render it.

Using the key on sheet 3

(DKPKAREZTPABSCHIBLTHZZLNARDGRXVGIBGQDPWLESQWKKAVQDKSPHZNMRDBBNJPTWDANLOARNELLMGODWAMQPYACBABRVTYAPDQHVQESOWQJESMMMDLZULCVGTCYXVZYIJBKOODXCXYSAUCAFGNUUPIBXPHLSXPBBLTQTLUO)

and her message “SEE YOU AT ELEVEN AM IN THE TOWN SQUARE”, Alice finally ends up with the following encrypted message:

Alice: VOT IOL ES XAEWWP HU JY MOD SZJN JTAROZ

Bob: Got it, thanks.

Mysterious and cryptic; perhaps even romantic…

Alice can drop the spaces to prevent anything being inferred from word lengths. Bob will still be able to figure out the text. Alice could even make use of a generally lesser-used letter (for example Q or X) instead of a space to split words while still maintaining one continuous message. This avoids highlighting individually encrypted words.

Alice does not need the entire key since her message is short. Bob simply repeats this process with subtraction and modulo instead of addition and modulo. He subtracts the key from the encrypted message to derive the original, decrypted message. He now knows when and where to meet up with Alice.

Alice and Bob are both smart people. They keep all of the following in mind:

  • Alice and Bob ensure that they received the only copies of the pads. Alice generated it and handed it to Bob in person, or vice versa.
  • The message should not be longer than the key (i.e. shouldn’t loop around).
  • They need to guard the pads with their lives and store them securely, especially if being found out could cost them their lives.
  • They need to burn the used sheet after encryption/decryption has occurred.
  • They transmit their encrypted message via another secure platform, such as Signal, if possible.

Thanks for following along! Be safe and secure in your communications. You can easily reproduce the work done here, with free software, if you have a really confidential message you’re trying to get across.

Searching for a string in an entire SAS library

For the most part, experienced SAS programmers know where to look for the source data they need. In the pharmaceutical industry, we are familiar with CDISC standards and data structures.

However, should the data standard be unfamiliar or the source datasets include new or unusual parameters, it may be prudent to have SAS look through the data on your behalf to save time.

We can do this by making use of PROC CONTENTS and SAS macro loops with the forward re-scan rule. If you’re not familiar with the forward re-scan rule, see my other blog post covering the topic.

First, let us define the two input parameters we’ll need: the library we’re delving into, and the character string we’re looking for. These are the only two parameters the end user needs to edit.

/*Search parameters - LIBRARY and text to search for*/
%let lib = %str(sashelp);
%let searchtext = %str(cholesterol);

Next, we’ll find all the datasets in the library specified above by running PROC CONTENTS and packing the results into iterable macro variables. In this case we can allocate a maximum of 999 datasets, but you can increase this value as needed.

/*Find all datasets within the library and allocate into numbered macro variables DS1, DS2, ...*/
proc contents data=&lib.._all_ out=members noprint;
run;

proc sql noprint;
    select distinct memname into :ds1-:ds999 from members;
quit;
%let ds_num = &sqlobs.;

Specify an empty results dataset. As matches are found during iteration, they will be appended to this dataset.

/*Clear the results dataset. As matches are found, they will be appended here.*/
data results;
    set _null_;
run;

Next we will loop over the datasets allocated as DS1, DS2, DS… above. The logical outline of the loop is as follows:

/*The macro logic for looping over each dataset in DS1, DS2, ...*/
%macro loop;
    %do i = 1 %to &ds_num.;
        /*For each dataset in the library, output its metadata contents*/

        /*Check the metadata for matches of the search text. This includes the dataset name, as well as the column names and labels.*/

        /*Check the actual data values for matches of the search text.*/

        /*For the current dataset, set the metadata and row value results.*/

        /*For the output dataset append the results of each loop iteration.*/
    %end;
%mend loop;
%loop;

For each step above, detailed logic follows below:

/*For each dataset in the library, output its metadata contents*/
proc contents data=&lib..&&ds&i out=c noprint;
run;
/*Check the metadata for matches of the search text. This includes the dataset name, as well as the column names and labels.*/
data metadata;
    length msg $500. dataset varname varlabel $100.;
    set c;
            
    /*Dataset name checking*/
    if index(upcase(memname), upcase("&searchtext.")) > 0 then do;
        msg = "Dataset: Found &searchtext.";
        dataset = "&&ds&i";
        varname = "";
        varlabel = "";
        output;
    end;

    /*Column name and label checking*/
    if index(upcase(name), upcase("&searchtext.")) > 0 or index(upcase(label), upcase("&searchtext.")) > 0 then do;
        msg = "Col Meta: Found &searchtext.";
        dataset = "&&ds&i";
        varname = name;
        varlabel = label;
        output;
    end;
            
    keep dataset varname varlabel msg;
            
    /*Get rid of duplicate messages in the case of multiple identical matches.*/
    proc sort nodupkey; by msg dataset varname varlabel;
run;
/*Check the actual data values for matches of the search text.*/
data actualdata;
    length msg $500. dataset varname varlabel varval $100.;
    set &lib..&&ds&i;
            
    /*Create an array of all character variables in the current dataset.*/
    array allChar _CHARACTER_;

    /*Loop over each character column and check for matches of the search text.*/
    do over allChar;
        if index(upcase(allchar), upcase("&searchtext.")) > 0 then do;
            msg = "Value: Found &searchtext.";
            dataset = "&&ds&i";
            varname = vname(allchar);
            varlabel = vlabel(allchar);
            varval = allchar;
            output;
        end;
    end;
    keep dataset varname varlabel varval msg;
            
    /*Get rid of duplicate messages in the case of multiple identical matches.*/
    proc sort nodupkey; by msg dataset varname varlabel varval;
run;
/*For the current dataset, set the metadata and row value results.*/
data allmsgs;
    set metadata actualdata;
    msg = upcase(msg);
run;
/*For the output dataset, OUT, keep appending the results of each loop iteration.*/
data results;
    set 
        results 
        allmsgs
    ;
run;

The steps above conclude the inner logic of the loop. Finally, outside the loop, we can clean up the datasets we no longer need.

/*Clean up*/
proc datasets library=work nolist;
    delete members c metadata actualdata allmsgs;
quit;

We started off with the SASHELP library, looking for the string “cholesterol”. These are the results:

Since we are searching in SASHELP, the results also include values from the SAS helper datasets, i.e., VCOLUMN and VMACRO.

This search function is not case sensitive. Optimise and implement as needed for your particular scenario.

NONMEM ADDL Calculation and Compression in SAS

ADDL represents the number of additional doses that are copies of the current row, with the time since first dose (RTFD) increased at the regular dosing interval (II). This allows for the compression of dose records in the NONMEM dataset. To put it more eloquently:

The NONMEM data item ADDL on a dose record expresses the number of additional implicit doses that should follow at a regular interval II. In the case where explicit doses exist, ADDL supports compacting them into ADDL/II notation.

https://rdrr.io/rforge/metrumrg/man/addl.html

The gist of calculating ADDL is the following:

  • Determine an allowed window between doses – if dosing occurs outside of this window (too soon or too late) the information should not be compressed and the modeller should be aware of it
  • Group and sort the data by subject ID, treatment, date of exposure, and dose
  • Assign a sequence number starting from 1 for each treatment (A, B, C, …) per subject
  • Determine which doses are “must-list”; the first record of every subject should be listed and any exposure attribute changes or exposure delays should be listed
  • Filter the exposure records to keep only the “must-list” data and other data required at the modeller’s discretion, e.g., the exposure records linked to PK samples
  • After filtering, the differences in SEQ values are the ADDL values

For this example, we will focus on compressing explicit doses and we will stick to date-based dosing only. The principles can be easily adjusted for date-and-time-based exposure data. Instead of defining the dosing interval (II) as 24h, we will just define it as 1 day in a macro variable.

%let allowed_dose_delay = 1;

The exposure data will be defined across two subjects, with multiple treatments, multiple dose levels, and occasional gaps between doses exceeding the 1 day window.

data ex;
	format date date9.;
	input usubjid trt $ dose date : date9.;
	cards;
1 A 50 01JAN1990
1 B 75 02JAN1990
1 A 50 02JAN1990
1 B 100 03JAN1990
1 A 50 03JAN1990
1 B 100 04JAN1990
1 A 50 05JAN1990
1 A 100 06JAN1990
1 B 50 06JAN1990
1 A 100 07JAN1990
1 A 100 08JAN1990
1 A 100 09JAN1990
2 B 50 01JAN1990
2 A 75 02JAN1990
2 B 50 02JAN1990
2 A 100 03JAN1990
2 B 50 03JAN1990
2 B 100 04JAN1990
2 B 50 05JAN1990
2 A 100 06JAN1990
2 B 50 06JAN1990
2 A 100 07JAN1990
run;

proc sort data=ex out=addl_pre;
	by usubjid trt date dose;
run;

After the initial dataset has been populated and sorted, a unique sequence number starting from 1 should be assigned to each treatment within each subject. In addition, a “must_list” variable will be populated with “Y” if:

  • the first record for the subject is encountered
  • a new treatment within subject is encountered
  • the dose level within a treatment was changed
  • a delay in dosing longer or shorter than the allowed delay window was encountered
data addl1;
	set addl_pre;
	by usubjid trt date dose;

	retain seq 1;

	if first.usubjid or first.trt then do;
		must_list = "Y";
		seq = 1;
	end;
	else do;
		seq + 1;
	end;

	if trt ^= lag(trt) then must_list = "Y";
	if dose ^= lag(dose) then must_list = "Y";
	if date - lag(date) ^= &allowed_dose_delay. then must_list = "Y";
run;

The maximum sequence per subject and treatment should be determined to calculate ADDL for the final, must-list exposure records.

proc sql noprint;
	create table addl2 as
		select *, max(seq) as max_seq
		from addl1
		group by usubjid, trt
		order by usubjid, trt, date
	;
quit;

The exposure events are filtered to keep only the “must-list” records. The remaining records will be compressed into the ADDL counter. With real-world data, you may want to add any record linked to PK sampling to the “must-list” group. This can be done by linking the exposure date and time with the sample reference dose date and time, e.g., PC.PCRFTDT.

data addl3;
	set addl2(where=(must_list = "Y"));
run;

Reverse sort the dataset to allow peeking at the next sequence records.

proc sort data=addl3 out=addl4_pre; 
	by descending usubjid descending trt descending date descending seq;
run;

data addl4;
	set addl4_pre;
	by descending usubjid descending trt descending date descending seq;

	next_seq = lag(seq);
	if usubjid ^= lag(usubjid) or trt ^= lag(trt) then next_seq = .;

	proc sort; by usubjid trt date seq;
run;

Finally, ADDL is calculated as the difference between MAX_SEQ and SEQ in the case of the final record, otherwise (NEXT_SEQ – (SEQ + 1)) to ensure we have exclusive bounds between the current and next exposure sequence numbers.

data addl5;
	set addl4;

	if missing(next_seq) then addl = max_seq - seq;
	else addl = next_seq - (seq + 1);

	keep usubjid trt date dose addl;
run;
While not explicitly specified here, our data has II defined as 24 hours or 1 day. In real-world data an II column would be visible in the data.

Provided that your data is well-structured and clean, compressing the exposure records and calculating ADDL is a straightforward task!

SAS and the Forward Re-scan Rule

The “Forward Re-scan Rule” (FRR) is used by SAS to resolve macro variables over several passes. This is especially useful when having one macro variable point to another macro variable, or when trying to resolve numbered macro variables.

The SAS Advanced Prep Guide summarises the FRR as follows:

  • When multiple ampersands or percent signs precede a name token, the macro processor resolves two ampersands (&&) to one ampersand (&), and re-scans the reference.
  • To re-scan a reference, the macro processor scans and resolves tokens from left to right from the point where multiple ampersands or percent signs are coded, until no more triggers can be resolved.

Example: numbered list of macro variables

To illustrate this example for a numbered list of macro variables, we can load each unique car manufacturer from SASHELP.CARS into a unique macro variable:

data cars;
	set sashelp.cars;
run;

proc sql;
	select distinct make into :car1-:car999
	from cars;
quit;

%put Number of obs = &sqlobs;

SAS will not create more macro variables than necessary. We have accounted for the possibility of 999 distinct manufacturers, but in reality the dataset contains only 38. SAS will only reserve the variables car1 to car38.

We can loop through the variables we’ve just created, by using the FRR. To further illustrate, also turn on the MPRINT, MLOGIC, and SYMBOLGEN options. Note the use of the double ampersand (&&).

options mprint mlogic symbolgen;
%macro printCars;
	%do i = 1 %to &sqlobs;
		%put &&car&i;
	%end;
%mend printCars;
%printCars;
 MLOGIC(PRINTCARS):  %DO loop beginning; index variable I; start value is 1; stop value is 38; by value is 1.  
 MLOGIC(PRINTCARS):  %PUT &&car&i
 SYMBOLGEN:  && resolves to &.
 SYMBOLGEN:  Macro variable I resolves to 1
 SYMBOLGEN:  Macro variable CAR1 resolves to Acura

From the log output, we can trace the FRR resolution as follows:

  1. &&car&i
  2. &car1
  3. Acura

Example: nested macro variables

Suppose we declare the following macro variables:

%let one = two;
%let two = three;
%let three = one;

To test your understanding of the FRR, can you accurately predict the resolution of these macro variables?

%put &one;
%put &&one;
%put &&&one;
%put &&&&one;
%put &&&&&one;
%put &&&&&&one;
%put &&&&&&&one;
%put &&&&&&&&one;
%put &&&&&&&&&one;
%put &&&&&&&&&&one;

The FRR will process from left to right. Any double ampersand (&&) will be resolved to a single ampersand (&) and any instances of a single ampersand will be resolved.

Let’s work through two examples together.

%put &&&&&one;

To better organise our desk-checking of the code, we can rewrite it in a more human-readable, and -friendly format:

&& && &one;

Each double ampersand (&&) resolves to a single ampersand, and the remaining single ampersand and macro reference is resolved.

& & two;

The remaining two ampersands are resolved to a single ampersand:

&two;

Which resolves to:

three

And upon checking the SAS log, we can see our result has been confirmed:

%put &&&&&one;
three

As a final example, let’s work through:

%put &&&&&&&&one;

Organise the ampersands into a more human-readable format, group and resolve double ampersands, and finally resolve remaining single ampersands:

&&&&&&&&one;
&& && && && one;
& & & & one;     compress ==>  && && one;
& & one;         compress ==>  && one;
&one;
two

Our result is once again confirmed by the SAS log:

%put &&&&&&&&one;
two