Thanks again OpenNota and Nick for your help. I ended up rewriting the password generation part using some inspiration from how rand.Int works, but my implementation is a lot more efficient, on the order of 8-10x.
To generate a 10 digit numerical password, using rand.Int would consume on average 127 bytes from the entropy pool. It generates a number between 0 and 127, if number is > 9, the number is disbanded and the process is repeated. The number will be less than 9 on average 117/127 of the times - about 92% misses, which means for each digit it will take approximately 13 runs of grabbing 1 byte from the entropy pool for each character, and this will be repeated for all ten characters for the estimated total of 127 bytes to generate the 10 digit password.
My implementation grabs 4 byte chunks at a time. For the numerical example, 9 random numbers can be extracted from those 32-bits. I determine the maximum bias level available in the 4 bytes ( floor( 2^32/(10^9) ) * 10^9) or 4*10^9. This is 93% hits, or 7% misses, meaning an expectation of 1.074. So to get the 9 digits would require 1.074 runs times the 4 bytes for a total of 4.3 expected bytes per run. We would have to run this again to get the 10th digit for a total expectation of 8.6 bytes. This is approximately 14-15 times less entropy data used than when using rand.Int
I have also included running times of running both implementations for comparing execution times. Note the flag to increase the timeout because the implementation to use rand.Int did not finish in the default 10 minute timeout.
// With the rand.Int
'0' 0.01 '1' 0.01 '2' 0.01 '3' -0.01 '4' -0.01 '5' 0.01 '6' -0.00 '7' -0.01 '8' -0.01 '9' -0.01
'0' -0.01 '1' -0.02 '2' 0.02 '3' 0.02 '4' -0.01 '5' 0.01 '6' 0.02 '7' -0.01 '8' 0.01 '9' -0.01
'0' -0.02 '1' 0.01 '2' 0.00 '3' -0.00 '4' -0.00 '5' -0.00 '6' -0.01 '7' -0.01 '8' 0.03 '9' 0.01
'0' 0.01 '1' -0.01 '2' 0.01 '3' -0.01 '4' 0.00 '5' 0.01 '6' -0.00 '7' 0.00 '8' -0.00 '9' -0.00
'0' 0.02 '1' -0.00 '2' -0.00 '3' 0.01 '4' -0.00 '5' -0.02 '6' 0.01 '7' -0.00 '8' -0.01 '9' 0.01
'0' -0.01 '1' 0.00 '2' -0.00 '3' -0.01 '4' 0.01 '5' 0.01 '6' -0.01 '7' 0.02 '8' 0.02 '9' -0.02
'0' -0.02 '1' 0.01 '2' -0.00 '3' 0.01 '4' 0.01 '5' 0.00 '6' 0.01 '7' -0.01 '8' 0.01 '9' -0.01
'0' -0.00 '1' 0.01 '2' -0.01 '3' -0.00 '4' 0.01 '5' 0.01 '6' -0.01 '7' 0.00 '8' -0.01 '9' 0.00
'0' 0.01 '1' -0.01 '2' -0.01 '3' 0.00 '4' 0.02 '5' -0.00 '6' -0.00 '7' -0.00 '8' -0.01 '9' 0.00
'0' -0.01 '1' 0.01 '2' -0.01 '3' 0.00 '4' -0.01 '5' 0.00 '6' -0.00 '7' 0.01 '8' 0.02 '9' -0.01
--- FAIL: TestGetPasswordBias (707.88 seconds)
password_test.go:195:
FAIL
// With my implentation
'0' 0.01 '1' -0.01 '2' 0.01 '3' -0.01 '4' 0.01 '5' 0.00 '6' -0.01 '7' -0.01 '8' -0.00 '9' 0.01
'0' 0.02 '1' -0.01 '2' -0.00 '3' -0.00 '4' -0.01 '5' 0.00 '6' 0.01 '7' 0.00 '8' 0.00 '9' -0.01
'0' -0.01 '1' -0.00 '2' 0.02 '3' -0.01 '4' 0.01 '5' -0.01 '6' 0.01 '7' -0.01 '8' -0.01 '9' 0.00
'0' 0.01 '1' 0.00 '2' 0.01 '3' -0.02 '4' 0.01 '5' 0.02 '6' -0.01 '7' -0.00 '8' -0.01 '9' -0.00
'0' 0.01 '1' 0.01 '2' -0.01 '3' -0.01 '4' 0.00 '5' 0.02 '6' -0.00 '7' 0.00 '8' 0.01 '9' -0.03
'0' -0.01 '1' -0.01 '2' -0.00 '3' 0.02 '4' 0.00 '5' -0.02 '6' -0.03 '7' 0.00 '8' 0.01 '9' 0.02
'0' 0.00 '1' 0.01 '2' -0.00 '3' 0.01 '4' 0.01 '5' -0.01 '6' -0.01 '7' -0.00 '8' -0.00 '9' -0.01
'0' -0.00 '1' 0.01 '2' 0.00 '3' 0.01 '4' -0.00 '5' -0.01 '6' 0.00 '7' -0.01 '8' 0.02 '9' -0.01
'0' 0.02 '1' -0.00 '2' -0.02 '3' 0.00 '4' -0.01 '5' 0.02 '6' -0.00 '7' -0.01 '8' 0.00 '9' -0.01
'0' 0.01 '1' 0.00 '2' -0.02 '3' 0.00 '4' 0.01 '5' 0.00 '6' 0.00 '7' 0.00 '8' -0.00 '9' -0.00
--- FAIL: TestGetPasswordBias (83.29 seconds)
password_test.go:195:
FAIL
Once again, thanks for your help. This is a much better implementation, and should address the security concerns raised.
Thanks,
Justin