Skip to content

Commit fb7a567

Browse files
authored
Merge pull request #159 from johnwalthallbb/feature/sis-hash-documentation
SIS password hashes
2 parents 886624e + 7ca8742 commit fb7a567

4 files changed

Lines changed: 298 additions & 25 deletions

File tree

.gitignore

Lines changed: 0 additions & 25 deletions
This file was deleted.
Lines changed: 256 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,256 @@
1+
---
2+
title: "SIS Password Hash Security"
3+
sidebar_position: 2
4+
id: sis-password-hashes
5+
author: John Z. Walthall
6+
published: ""
7+
edited: ""
8+
---
9+
10+
## Introduction
11+
12+
You can create or set user passwords by SIS using the 'Person' data type. There are three ways to do this:
13+
14+
1. Plaintext (not recommended)
15+
2. MD5 (not recommended)
16+
3. Salted SHA-1 ("SSHA", recommended)
17+
18+
Here is an example of all three formats. (In each case: the password is 'cyan')
19+
20+
```
21+
user_id|external_person_key|lastname|firstname|passwd|pwencryptiontype|data_source_key
22+
jshaw|jshaw|Shaw|James|{SSHA}foV2dGZ/2FLNdmJUNEpXZ8ijfiGAriwuB9AYrQ==|SSHA|exterminate
23+
jplain|jplain|Plain|Jane|cyan||exterminate
24+
md5|md5|Five|Maddy|6411532ba4971f378391776a9db629d3|MD5|exterminate
25+
```
26+
27+
## Supplying a Password Hash
28+
29+
These hash formats are not the one used by the LMS. When a password is set in the GUI it is hashed using a
30+
[512-bit SHA-2-based](https://help.blackboard.com/Learn/Administrator/SaaS/Security/Identification_Authentication) hash.
31+
32+
If you supply the field `passwd`, but not the field `pwencryptiontype` (or: set this field to blank) the input will be
33+
hashed by the LMS using the aforementioned hash.
34+
35+
Otherwise, the input will be copied verbatim into the database. When the user logs in: which is the only time the clear-
36+
text of the password is accessible to Blackboard, the cleartext will be re-hashed using a strong hash and the existing
37+
one overwritten.
38+
39+
If you set 'change on update' for the password field: the stronger hash will be overwritten.
40+
41+
### Using MD5
42+
43+
:::danger
44+
It's not recommended to use MD5. MD5 is obsolete and no longer secure. It may be subject to removal in a future version
45+
of Blackboard Learn.
46+
:::
47+
48+
The MD5 hash is trivial: it's simply the standard hex representation of the MD5sum of the string. So if the password is
49+
'cyan' then
50+
51+
```shell
52+
$ echo -n "cyan" | md5
53+
6411532ba4971f378391776a9db629d3
54+
```
55+
56+
### Using SSHA
57+
58+
This format is an informal standard and our implementation is _similar to_ but _not the same as_ [the format used by
59+
`slappasswd(8)`](https://github.com/openldap/openldap/blob/34813d9cba02a74216a784636a8d5f0f986d73c7/libraries/liblutil/passwd.c#L749-L779)
60+
from the OpenLDAP project. The key difference is the salt-size. Blackboard Learn uses an **8-byte** salt. OpenLDAP uses
61+
a **4-byte** salt. Since the salt-size is not stored in the format, hashes with different salt-lengths are not
62+
compatible.
63+
Therefore, the hashes generated by `slappasswd(8)` cannot be used.
64+
65+
### Algorithm Description
66+
67+
1. For each password: create 8 random bytes of 'salt'. Never re-use salts. Use a Cryptographically Secure Psuedo-Random
68+
Number Generator, or a true environmentally sourced random-number generator.
69+
2. Convert the password string to a byte array using UTF-8.
70+
3. Digest the concatenation of the password bytes + the salt bytes as SHA-1. The SHA-1 digest must be a byte array
71+
itself, if the implementation produces a hex string, it must be converted to a byte array by parsing each pair of
72+
characters as an unsigned hexadecimal byte.
73+
4. Append another copy of the same salt bytes.
74+
5. Encode this as Base64 in ASCII. Use the default dictionary: not the 'url-safe' one.
75+
7. Prefix with `{SSHA}`.
76+
77+
### Step by Step
78+
79+
1. Let the password be `nucleus`
80+
2. Let the salt be `[21, F0, 25, 09, 15, D2, 68, 1F]` (interpreted as an array of unsigned bytes.) Remember: it must
81+
always be eight new random bytes.
82+
3. The UTF-8 bytes of 'nucleus' are `[6E, 75, 63, 6C, 65, 75, 73]`
83+
4. Thus the value to be digested is `[6E, 75, 63, 6C, 65, 75, 73, 21, F0, 25, 09, 15, D2, 68, 1F]`
84+
5. The SHA-1 digest of this (interpreted as an array of unsigned bytes) is
85+
`[90, FC, 6D, A2, C9, EA, 04, 10, 83, 20, C4, AC, 15, 73, A7, 49, BD, 88, 7A, 63]`
86+
6. Appending the salt to that is
87+
`[90, FC, 6D, A2, C9, EA, 04, 10, 83, 20, C4, AC, 15, 73, A7, 49, BD, 88, 7A, 63, 21, F0, 25, 09, 15, D2, 68, 1F]`
88+
7. The Base64 encoding of this, with prefix is `{SSHA}kPxtosnqBBCDIMSsFXOnSb2IemMh8CUJFdJoHw==`
89+
90+
### Code Examples
91+
92+
Here are some code examples.
93+
94+
:::caution
95+
A particular source of confusion, and many examples on the Internet stumble on this confusion, is that the SHA-1 output
96+
must be bytes, not a hex string. If the programming language or library you choose returns a hex string for SHA-1, then
97+
it must be recast as a byte array. That is to say: `EF` must be a single byte: `239` (unsigned), not a pair of bytes
98+
equal to unsigned `69` (the '`E`') and `70` (the '`F`'.)
99+
:::
100+
101+
:::info
102+
According to the SHA-1 specification: an empty string can be hashed. You must not do this. In the SIS API, an empty
103+
password indicates the LMS should generate a random password. Setting the password to the SSHA hash of empty string
104+
results in a blank password being set.
105+
:::
106+
107+
#### Java
108+
109+
```java
110+
import java.nio.charset.StandardCharsets;
111+
import java.security.MessageDigest;
112+
import java.security.NoSuchAlgorithmException;
113+
import java.security.SecureRandom;
114+
import java.util.Arrays;
115+
import java.util.Base64;
116+
117+
/**
118+
* Encoder for our nonstandard SSHA variant using 8-byte salt.
119+
*/
120+
public final class VariantSSHA {
121+
122+
/**
123+
* Encode string to the hash
124+
* @param password The password to encode
125+
* @return the variant-SSHA hash.
126+
*/
127+
public static String variantSSHAEncode(String password) {
128+
if (password.isBlank()) {
129+
// empty-string password has a special meaning in Learn. But a hash of empty-string is a valid hash
130+
// bail out to preserve semantics
131+
throw new IllegalArgumentException("Password cannot be blank");
132+
}
133+
// The salt is always 8 bytes. This is incompatible with slappasswd(8)
134+
byte[] salt = new byte[8];
135+
SecureRandom rand = null;
136+
try {
137+
rand = SecureRandom.getInstanceStrong();
138+
} catch (NoSuchAlgorithmException e) {
139+
throw new RuntimeException("FATAL: can't load secure randomness", e);
140+
}
141+
rand.nextBytes(salt);
142+
143+
byte[] bytesUTF8 = password.getBytes(StandardCharsets.UTF_8);
144+
145+
try {
146+
// hash the combination of the password + the salt.
147+
MessageDigest sha1Digest = MessageDigest.getInstance("SHA1");
148+
sha1Digest.update(bytesUTF8);
149+
sha1Digest.update(salt);
150+
byte[] binaryHash = sha1Digest.digest();
151+
152+
// append the salt to the hash
153+
byte[] hashPlusSalt = new byte[binaryHash.length + salt.length];
154+
System.arraycopy(binaryHash, 0, hashPlusSalt, 0, binaryHash.length);
155+
System.arraycopy(salt, 0, hashPlusSalt, binaryHash.length, salt.length);
156+
157+
// This is mostly security theater; but customary
158+
Arrays.fill(salt, (byte) 0);
159+
Arrays.fill(bytesUTF8, (byte) 0);
160+
161+
String stringHash = "{SSHA}" + Base64.getEncoder().encodeToString(hashPlusSalt);
162+
Arrays.fill(hashPlusSalt, (byte) 0);
163+
164+
return stringHash;
165+
} catch (NoSuchAlgorithmException e) {
166+
// can't happen. JSSE requires all implementing runtimes to support SHA-1
167+
throw new RuntimeException(e);
168+
}
169+
}
170+
171+
/**
172+
* Convenience method to use as a simple utility. Output format is {@code original_password + tab + SSHA}
173+
*
174+
* <pre>
175+
* $ java VariantSSHA.java the quick brown fox" "jumps over the lazy dog" "when zombies arrive" "quickly fax judge patty"
176+
* the quick brown fox {SSHA}r+QLZ86dFWWp0oXhGC3nW5U/p08DvFVyKH1M/w==
177+
* jumps over the lazy dog {SSHA}yxUScjSM42EBpL2qB7I2wLf/CLHBQX0No18z/w==
178+
* when zombies arrive {SSHA}Yvot6sr1F7XNahlwY0KeXmmukpw19oYSJnZhRQ==
179+
* quickly fax judge patty {SSHA}f01o7IJGet6TzvizERwuVzPX7Ud09Pu3HGJeZg==
180+
* </pre>
181+
* @param args strings to encode.
182+
*/
183+
static void main(String... args) {
184+
if (args.length == 0) {
185+
System.err.println("Try again");
186+
System.err.println("Usage: java VariantSSHA password1 password2 password3...");
187+
} else {
188+
int pad = Arrays.stream(args).mapToInt(String::length).max().orElse(10);
189+
for (String password : args) {
190+
System.out.printf("%" + pad + "s\t%s%n", password, variantSSHAEncode(password));
191+
}
192+
}
193+
}
194+
}
195+
```
196+
197+
#### Python
198+
199+
```python
200+
#!/usr/bin/env python3
201+
"""
202+
Generate Blackboard's nonstandard 8-byte-salted SSHA variant.
203+
"""
204+
import base64
205+
import hashlib
206+
import os
207+
import sys
208+
209+
210+
def variant_ssha_encode(password: str):
211+
if password == "":
212+
raise RuntimeError("Password can not be empty")
213+
214+
# Generate 8 random bytes of salt. os.urandom is cryptographically secure
215+
salt = os.urandom(8)
216+
217+
# convert the password to bytes
218+
bytes_utf8 = password.encode("utf-8")
219+
220+
sha1digest = hashlib.sha1()
221+
222+
# equivalent to byte_utf8 + salt
223+
sha1digest.update(bytes_utf8)
224+
sha1digest.update(salt)
225+
226+
# append the salt to the end of the SHA-1 digest (note the digest must be bytes, not hex string)
227+
hash_with_salt = sha1digest.digest() + salt
228+
229+
# encode to base64, pre-fix the identifier and return.
230+
return "{SSHA}" + base64.b64encode(hash_with_salt).decode("ascii")
231+
232+
233+
def main():
234+
"""
235+
Convenience method to use as a simple utility
236+
invocation:
237+
$ python3 generate_ssha_hash.py "the quick brown fox" "jumps over the lazy dog" "when zombies arrive" "quickly fax judge patty"
238+
the quick brown fox {SSHA}Ffy5dpkMeMIiebd+Sqtu0FJOV6xdAh4Wp9aeSA==
239+
jumps over the lazy dog {SSHA}SmYwGocJidrBS9AfBid9P/JUUOxhTZLylWcKQw==
240+
when zombies arrive {SSHA}layQWCu+uVrFmXeKE4ZeqPGzCJ87OVI0zAnjJQ==
241+
quickly fax judge patty {SSHA}IJbtvQYh6TocBq5m4yoU0sVRvUdMrR+hZUHxCQ==
242+
"""
243+
if len(sys.argv) == 1:
244+
print("Try again\nUsage: python3 generate_ssha_hash.py password1 password2 password3...")
245+
sys.exit(1)
246+
passwords = sys.argv[1:]
247+
pad = len(max(passwords, key=len))
248+
for pw in passwords:
249+
ssha = variant_ssha_encode(pw)
250+
padded = pw.rjust(pad)
251+
print(f"{padded}\t{ssha}")
252+
253+
254+
if __name__ == "__main__":
255+
main()
256+
```

docs/blackboard/sis/welcome.md

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
---
2+
title: "Getting started with SIS"
3+
sidebar_position: 1
4+
id: getting-started
5+
author: John Z. Walthall
6+
published: ""
7+
edited: ""
8+
---
9+
10+
The SIS (Student Information Systems) API is a classic API oriented around bulk or event-driven data loading from a SIS
11+
or ERP program like Anthology Student, Banner or Colleague. Most aspects of SIS are [documented on our main help site](https://help.blackboard.com/Learn/Administrator/SaaS/Integrations/Student_Information_System),
12+
but some developer documentation is provided here.
13+
14+
## SIS Types
15+
16+
The "prime" data type is "[Snapshot Flat File](https://help.blackboard.com/Learn/Administrator/SaaS/Integrations/Student_Information_System/SIS_Integration_Types/Snapshot_Flat_File)", which is a bulk-loading oriented delimited text file. This is the 'native'
17+
format of Blackboard and has the widest support for the LMS's many data types.
18+
19+
We also support three industry-standard formats:
20+
21+
* [LIS 2.0 Final and Draft](https://help.blackboard.com/Learn/Administrator/SaaS/Integrations/Student_Information_System/SIS_Integration_Types/LIS)
22+
* [IMS Enterprise 1.1](https://help.blackboard.com/Learn/Administrator/SaaS/Integrations/Student_Information_System/SIS_Integration_Types/Enterprise_1.1)
23+
* [IMS Enterprise 1.1 - Vista](https://help.blackboard.com/Learn/Administrator/SaaS/Integrations/Student_Information_System/SIS_Integration_Types/Enterprise_1.1)
24+
25+
These are usually used in an event-driven workflow.
26+
27+
## Security
28+
29+
SIS activity occurs at a high level of privilege. It can create, remove, or delete anything. A user that gains access to
30+
an SIS username and password could send arbitrary data files that cause arbitrary changes.
31+
32+
SIS data files also contain potentially sensitive data and access to them should be strictly controlled. (Note: Support
33+
may require the feed file for problem investigation.)
34+
35+
One particular case is setting passwords by SIS. They can be supplied in cleartext, but this is not recommended and they
36+
should be provided in [hashed form.](sis-password-hashes.md)

sidebar.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,12 @@ const sidebars = {
209209
},
210210
],
211211
},
212+
{
213+
SIS: [
214+
"blackboard/sis/getting-started",
215+
"blackboard/sis/sis-password-hashes"
216+
]
217+
},
212218
],
213219
},
214220
// Student

0 commit comments

Comments
 (0)