PasswordAdmin.mesa
Tool for Grapevine password administration. Present functions are: read and save all passwords; compare passwords with ones previously saved and make a list of ones that haven't changed.
Implementation adapted from DBPurge.mesa (Andrew Birrell, October 20, 1983 9:43 am)
Last Edited by: Taft, January 2, 1984 2:38 pm
Hal Murray May 17, 1985 2:34:57 am PDT
DIRECTORY
Ascii USING [Lower],
Basics USING [Comparison],
BasicTime,
BTree,
BTreeVM,
Commander USING [CommandProc, Handle, Register],
CommandTool USING [ArgumentVector, Failed, Parse],
FS,
FSBackdoor,
GVBasics USING [MakeKey, Password, Timestamp],
GVNames,
GVSend,
IO,
Menus,
PrincOpsUtils,
Rope,
TEditOps,
UserCredentials,
ViewerClasses,
ViewerEvents,
ViewerOps;
PasswordAdmin: CEDAR MONITOR
IMPORTS Ascii, BasicTime, BTree, BTreeVM, Commander, CommandTool, FS, FSBackdoor, GVBasics, GVNames, GVSend, IO, Menus, PrincOpsUtils, Rope, TEditOps, UserCredentials, ViewerEvents, ViewerOps
SHARES Rope =
BEGIN
ROPE: TYPE = Rope.ROPE;
Commands
RememberPasswordsCmd: Commander.CommandProc =
BEGIN
argv: CommandTool.ArgumentVector = CommandTool.Parse[cmd !
CommandTool.Failed => {cmd.out.PutRope[errorMsg]; GOTO stop}];
append: BOOLEANFALSE;
fileName: ROPENIL;
patterns: LIST OF ROPENIL;
FOR arg: NAT IN [1..argv.argc) DO
SELECT TRUE FROM
argv[arg].Length=0 => {cmd.out.PutRope["Syntax error"]; GOTO stop};
argv[arg].Fetch[0]='- =>
IF argv[arg].Length=2 AND Ascii.Lower[argv[arg].Fetch[1]]='a THEN append ← TRUE
ELSE {cmd.out.PutRope["Illegal switch"]; GOTO stop};
fileName=NIL => fileName ← argv[arg];
ENDCASE =>
IF patterns=NIL THEN patterns ← LIST[argv[arg]]
ELSE FOR item: LIST OF ROPE ← patterns, item.rest DO
IF item.rest=NIL THEN {item.rest ← LIST[argv[arg]]; EXIT};
ENDLOOP;
ENDLOOP;
IF fileName=NIL OR patterns=NIL THEN
{cmd.out.PutRope["Syntax error"]; GOTO stop};
IF ~GetWizardPassword[cmd] THEN GOTO stop;
OpenBTree[fileName: fileName, create: NOT append !
FS.Error => {cmd.out.PutRope[error.explanation]; GOTO stop}];
FOR item: LIST OF ROPE ← patterns, item.rest UNTIL item=NIL DO
cmd.out.PutF["Remember[\"%g\"]\n", [rope[item.first]]];
Remember[pattern: item.first, out: cmd.out];
ENDLOOP;
CloseBTree[];
EXITS
stop => cmd.out.PutChar['\n];
END;
CheckPasswordsCmd: Commander.CommandProc =
BEGIN
argv: CommandTool.ArgumentVector = CommandTool.Parse[cmd !
CommandTool.Failed => {cmd.out.PutRope[errorMsg]; GOTO stop}];
append: BOOLEANFALSE;
fileName: ROPENIL;
patterns: LIST OF ROPENIL;
FOR arg: NAT IN [1..argv.argc) DO
SELECT TRUE FROM
argv[arg].Length=0 => {cmd.out.PutRope["Syntax error"]; GOTO stop};
argv[arg].Fetch[0]='- =>
IF argv[arg].Length=2 THEN {cmd.out.PutRope["Illegal switch"]; GOTO stop};
fileName=NIL => fileName ← argv[arg];
ENDCASE =>
IF patterns=NIL THEN patterns ← LIST[argv[arg]]
ELSE FOR item: LIST OF ROPE ← patterns, item.rest DO
IF item.rest=NIL THEN {item.rest ← LIST[argv[arg]]; EXIT};
ENDLOOP;
ENDLOOP;
IF fileName=NIL OR patterns=NIL THEN
{cmd.out.PutRope["Syntax error"]; GOTO stop};
IF ~GetWizardPassword[cmd] THEN GOTO stop;
OpenBTree[fileName: fileName, lock: $read !
FS.Error => {cmd.out.PutRope[error.explanation]; GOTO stop}];
FOR item: LIST OF ROPE ← patterns, item.rest UNTIL item=NIL DO
invalid, unchanged, tooShort: LIST OF ROPE;
cmd.out.PutF["Check[\"%g\"]\n", [rope[item.first]]];
[invalid: invalid, unchanged:unchanged, tooShort: tooShort] ← Check[pattern: item.first, out: cmd.out];
invalidNames ← ConcRopeLists[invalidNames, invalid];
unchangedNames ← ConcRopeLists[unchangedNames, unchanged];
tooShortNames ← ConcRopeLists[tooShortNames, tooShort];
ENDLOOP;
CloseBTree[];
EXITS
stop => cmd.out.PutChar['\n];
END;
invalidNames, unchangedNames, tooShortNames: LIST OF ROPENIL;
GetWizardPassword: PROC [cmd: Commander.Handle] RETURNS [ok: BOOLEAN] =
BEGIN
IF wizardPassword=NIL THEN
BEGIN
password: ROPE;
authenticateInfo: GVNames.AuthenticateInfo;
cmd.out.PutRope["Enter wizard's password: "];
EditedStream.SetMode[stream: cmd.in, echoAsterisks: TRUE];
[token: password] ← cmd.in.GetTokenRope[];
EditedStream.SetMode[stream: cmd.in, echoAsterisks: FALSE];
cmd.out.PutRope[" ... "];
authenticateInfo ← GVNames.Authenticate[wizardName, password];
IF authenticateInfo#individual THEN {
cmd.out.PutRope[Code[authenticateInfo]]; RETURN [FALSE]};
cmd.out.PutRope["ok\n"];
wizardPassword ← password;
END;
RETURN [TRUE];
END;
wizardName: ROPE ← "Wizard.GV";
wizardPassword: ROPENIL;
Main operations (callable from interpreter)
Remember: PROC [pattern: Rope.ROPE, out: IO.STREAM] =
Remembers the passwords of all individual RNames matching pattern. Note: the information is added to the existing tree; call OpenBTree before calling Remember.
BEGIN
RememberWork: EntryEnumProc --[info: REF GVNames.GetEntryInfo] RETURNS [exit: BOOLFALSE]-- =
BEGIN
WITH info^ SELECT FROM
i: GVNames.GetEntryInfo.individual =>
Insert[name: i.name, password: i.password, stamp: i.passwordStamp];
ENDCASE =>
out.PutF["Name \"%g\" is not an individual.\n", [rope[info.name]]];
END;
IF tree=NIL OR tree.GetState[].state#open THEN
{out.PutRope["Tree not open yet; call OpenBTree first.\n"]; RETURN};
EnumAllEntries[pattern: pattern, which: individuals, work: RememberWork, out: out];
END;
Check: PROC [pattern: Rope.ROPE, out: IO.STREAM] RETURNS [invalid, unchanged, tooShort: LIST OF ROPE] =
Compares the passwords of all individual RNames matching pattern with those previously remembered, and also checks that the current password is reasonable. Returns lists of names not passing these tests. A BTree containing the remembered values must already be open; call OpenBTree before calling Compare.
BEGIN
CheckWork: EntryEnumProc --[info: REF GVNames.GetEntryInfo] RETURNS [exit: BOOLFALSE]-- =
BEGIN
WITH info^ SELECT FROM
i: GVNames.GetEntryInfo.individual =>
BEGIN
found, changed: BOOLEAN;
password: GVBasics.Password;
stamp: GVBasics.Timestamp;
[found: found, password: password, stamp: stamp] ← Find[i.name];
IF ~found THEN {
out.PutF["Name \"%g\" did not previously exist.\n", [rope[info.name]]];
RETURN};
changed ← password#i.password;
SELECT KosherPassword[i.password] FROM
$ok => IF ~changed THEN unchanged ← CONS[info.name, unchanged];
$invalid => invalid ← CONS[info.name, invalid];
$tooShort => IF changed THEN tooShort ← CONS[info.name, tooShort] ELSE unchanged ← CONS[info.name, unchanged];
ENDCASE;
END;
ENDCASE =>
out.PutF["Name \"%g\" is not an individual.\n", [rope[info.name]]];
END;
invalid ← unchanged ← tooShort ← NIL;
IF tree=NIL OR tree.GetState[].state#open THEN
{out.PutRope["Tree not open yet; call OpenBTree first.\n"]; RETURN};
EnumAllEntries[pattern: pattern, which: individuals, work: CheckWork, out: out];
END;
KosherPassword: PROC [password: GVBasics.Password] RETURNS [status: {ok, invalid, tooShort}] =
BEGIN
p: PasswordRep = LOOPHOLE[password];
FOR i: CARDINAL DECREASING IN [0..7] DO
IF p[i].char#0 THEN RETURN [IF i<5 THEN $tooShort ELSE $ok];
REPEAT
FINISHED => RETURN [$invalid];
ENDLOOP;
END;
RopeFromPassword: PROC [password: GVBasics.Password] RETURNS [ROPE] =
BEGIN
p: PasswordRep = LOOPHOLE[password];
FOR i: CARDINAL DECREASING IN [0..7] DO
IF p[i].char#0 THEN
BEGIN
s: IO.STREAM ← IO.ROS[];
FOR j: CARDINAL IN [0..i] DO s.PutChar[LOOPHOLE[p[j].char]]; ENDLOOP;
RETURN [IO.RopeFromROS[s]];
END;
REPEAT
FINISHED => RETURN [NIL];
ENDLOOP;
END;
PasswordRep: TYPE = PACKED ARRAY [0..7] OF RECORD [char: [0..177B], parity: BOOL];
ReadNamesFromTree: PROC RETURNS [names: LIST OF ROPE] =
BEGIN
Proc: BTreeEnumProc --[name: ROPE, password: GVBasics.Password, stamp: GVBasics.Timestamp] RETURNS [continue: BOOLEANTRUE]-- =
BEGIN
IF KosherPassword[password]#invalid THEN names ← CONS[name, names];
END;
names ← NIL;
[] ← BTreeEnumerate[proc: Proc];
END;
ConcRopeLists: PROC [a, b: LIST OF ROPE] RETURNS [LIST OF ROPE] =
BEGIN
IF a=NIL THEN RETURN[b];
IF b#NIL THEN
FOR pred: LIST OF ROPE ← a, pred.rest DO
IF pred.rest=NIL THEN {pred.rest ← b; EXIT};
ENDLOOP;
RETURN[a];
END;
Grapevine access and utilities
BreakName: PROC [name: Rope.ROPE] RETURNS [sn, reg: Rope.ROPE] =
BEGIN
FOR i: INT DECREASING IN [0..name.Length[])
DO IF name.Fetch[i] = '.
THEN RETURN[
sn: name.Substr[start: 0, len: i],
reg: name.Substr[start: i+1, len: name.Length[]-(i+1)]
];
ENDLOOP;
RETURN[ sn: NIL, reg: name ]
END;
Code: PROC [type: GVNames.Outcome] RETURNS [Rope.ROPE] =
{ RETURN[ SELECT type FROM
noChange => "no change",
group => "that's a group",
individual => "that's an individual",
notFound => "name not found",
protocolError => "protocol error",
wrongServer => "wrong server",
allDown => "all suitable servers are down or inaccessible",
badPwd => "incorrect password",
outOfDate => "out of date",
notAllowed => "you're not authorized to do that",
ENDCASE => "unknown return code" ] };
NameClass: TYPE = { individuals, groups, dead };
NameEnumProc: TYPE = PROC [name: Rope.ROPE] RETURNS[exit: BOOLFALSE];
EnumAllNames: PROC [pattern: Rope.ROPE, which: NameClass, work: NameEnumProc, out: IO.STREAM] =
BEGIN
sn, reg: Rope.ROPE;
[sn, reg] ← BreakName[pattern];
SELECT TRUE FROM
reg.Find["*"]>=0 =>
BEGIN
RegWork: NameEnumProc = {
EnumAllNames[Rope.Cat[sn, ".", BreakName[name].sn], which, work, out]};
out.PutRope["Finding the registries . . .\n"];
EnumAllNames[Rope.Cat[reg, ".GV"], groups, RegWork, out];
END;
sn.Find["*"]>=0 =>
TRUSTED BEGIN -- allegedly unsafe variant record assignments in here
enumName: Rope.ROPE;
info: GVNames.MemberInfo ← [noChange[]];
out.PutF["Enumerating names in %g . . .\n", [rope[reg]] ];
enumName ← Rope.Cat[
SELECT which FROM
individuals => "Individuals.",
groups => "Groups.",
dead => "Dead.",
ENDCASE => ERROR,
reg];
info ← GVNames.GetMembers[enumName];
WITH info SELECT FROM
i: GVNames.MemberInfo.group =>
BEGIN
FOR m: GVNames.RListHandle ← i.members, m.rest UNTIL m = NIL DO
IF Rope.Match[pattern: pattern, object: m.first, case: FALSE]
THEN { IF work[m.first] THEN EXIT };
ENDLOOP;
END;
ENDCASE =>
out.PutF["Couldn't get members of \"%g\": %g\n", [rope[enumName]], [rope[Code[info.type]]]];
END;
ENDCASE => [] ← work[pattern];
END;
EntryEnumProc: TYPE = PROC [info: REF GVNames.GetEntryInfo] RETURNS [exit: BOOLFALSE];
EnumAllEntries: PROC [pattern: Rope.ROPE, which: NameClass, work: EntryEnumProc, out: IO.STREAM] =
BEGIN
EntryWork: NameEnumProc =
BEGIN
info: REF GVNames.GetEntryInfo;
rc: GVNames.NameType;
[rc, info] ← GVNames.AuthenticatedGetEntry[user: wizardName, password: pwd, name: name];
IF rc IN [group..individual] THEN exit ← work[info]
ELSE out.PutF["Couldn't read entry for \"%g\": %g\n", [rope[name]], [rope[Code[rc]]]];
END;
pwd: GVBasics.Password = GVBasics.MakeKey[wizardPassword];
EnumAllNames[pattern: pattern, which: which, work: EntryWork, out: out];
END;
Sending messages
GetMessageText: PROC RETURNS [message: ROPE] =
BEGIN
initialForm: ROPE ← "Subject: \001subject\002\nTo: distribution:;\n\n\001message\002\n";
dateAndFrom: ROPE;
RemoveMenuItem: PROC [name: ROPE] =
BEGIN
entry: Menus.MenuEntry = Menus.FindEntry[menu: viewer.menu, entryName: name];
IF entry#NIL THEN Menus.ReplaceMenuEntry[menu: viewer.menu, oldEntry: entry, newEntry: NIL];
END;
AwaitComposedMessage: ENTRY PROC RETURNS [ok: BOOLEAN] =
BEGIN
action ← wait;
WHILE action=wait DO WAIT messageComposed; ENDLOOP;
RETURN [action=send];
END;
IF viewer#NIL THEN {ViewerOps.DestroyViewer[viewer]; viewer ← NIL};
viewer ← ViewerOps.CreateViewer[flavor: $Text, info: [name: "PasswordAdmin composed message", iconic: FALSE]];
RemoveMenuItem["Reset"];
RemoveMenuItem["Get"];
RemoveMenuItem["GetImpl"];
RemoveMenuItem["PrevFile"];
RemoveMenuItem["Store"];
RemoveMenuItem["Save"];
Menus.AppendMenuEntry[menu: viewer.menu, entry: Menus.CreateEntry[name: "Send", proc: SendButton, fork: FALSE]];
Menus.AppendMenuEntry[menu: viewer.menu, entry: Menus.CreateEntry[name: "Abort", proc: AbortButton, fork: FALSE]];
ViewerOps.PaintViewer[viewer: viewer, hint: menu];
TEditOps.SetTextContents[viewer, initialForm];
[] ← ViewerEvents.RegisterEventProc[proc: DestroyButton, event: destroy];
IF ~AwaitComposedMessage[] THEN RETURN [NIL];
dateAndFrom ← IO.PutFR["Date: %g\nFrom: %g\n", , [rope[ArpaDate[BasicTime.Now[]]]], [rope[UserCredentials.Get[].name]]];
RETURN [Rope.Cat[dateAndFrom, TEditOps.GetTextContents[viewer]]];
END;
SendMessage: PROC [recipients: LIST OF ROPE, message: ROPE, out: IO.STREAM] =
BEGIN
handle: GVSend.Handle = GVSend.Create[];
BEGIN ENABLE GVSend.SendFailed => {
out.PutF["%g ... Retrying...\n", [rope[why]]]; RETRY};
startSendInfo: GVSend.StartSendInfo = handle.StartSend[senderPwd: UserCredentials.Get[].password, sender: UserCredentials.Get[].name, validate: FALSE];
IF startSendInfo#ok THEN {out.PutRope["StartSend failed unexpectedly\n"]; RETURN};
out.PutF["Sending recipient list... \n"];
FOR item: LIST OF ROPE ← recipients, item.rest UNTIL item=NIL DO
handle.AddRecipient[item.first];
ENDLOOP;
out.PutF["Sending message... "];
handle.StartText[];
handle.AddToItem[message];
handle.Send[];
out.PutF["Sent OK.\n"];
END;
END;
viewer: ViewerClasses.Viewer ← NIL;
action: {wait, abort, send};
messageComposed: CONDITION;
SendButton: ENTRY Menus.MenuProc = {
action ← send; NOTIFY messageComposed};
AbortButton: ENTRY Menus.MenuProc = {
action ← abort; NOTIFY messageComposed};
DestroyButton: ENTRY ViewerEvents.EventProc = {
viewer ← NIL; action ← abort; NOTIFY messageComposed};
ArpaDate: PROC [gmt: BasicTime.GMT] RETURNS [text: ROPE] =
BEGIN
unpacked: BasicTime.Unpacked = BasicTime.Unpack[gmt];
zoneHour: CARDINAL = ABS[unpacked.zone]/60;
zoneMinute: CARDINAL = ABS[unpacked.zone] MOD 60;
zoneLetters: ARRAY [4..11] OF CHAR = ['A, 'E, 'C, 'M, 'P, 'Y, 'H, 'B];
dstLetters: ARRAY [0..2] OF CHAR = ['D, 'S, '?];
monthNames: ARRAY BasicTime.MonthOfYear OF ROPE = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", "???"];
zoneText: ROPE = SELECT TRUE FROM
unpacked.zone=721 => NIL,
unpacked.zone>0 AND zoneHour IN [4..11] AND zoneMinute=0 => IO.PutFR["%g%gT", [character[zoneLetters[zoneHour]]], [character[dstLetters[LOOPHOLE[unpacked.dst]]]]],
ENDCASE => IO.PutFR["%g%g:%02g", [character[IF unpacked.zone<0 THEN '- ELSE '+]], [cardinal[zoneHour]], [cardinal[zoneMinute]]];
date: ROPE = IO.PutFR["%2g %g %2g ", [cardinal[unpacked.day]], [rope[monthNames[unpacked.month]]], [cardinal[unpacked.year MOD 100]]];
time: ROPE = IO.PutFR["%2g:%02g:%02g %g", [cardinal[unpacked.hour]], [cardinal[unpacked.minute]], [cardinal[unpacked.second]], [rope[zoneText]]];
RETURN[Rope.Cat[date, time]];
END;
BTree access
tree: BTree.Tree ← NIL;
file: FS.OpenFile ← FS.nullOpenFile;
OpenBTree: PROC [fileName: ROPE, create: BOOLFALSE, lock: FS.Lock ← $write] =
BEGIN
vmHandle: BTreeVM.Handle;
IF create AND lock#$write THEN ERROR;
file ← IF create THEN FS.Create[name: fileName, pages: 40] ELSE FS.Open[name: fileName, lock: lock];
vmHandle ← BTreeVM.Open[file: FSBackdoor.GetFileHandle[file], filePagesPerPage: 4, cacheSize: 25];
IF tree=NIL THEN
tree ← BTree.New[repPrim: [compare: BTreeCompare, compareEntries: NIL, entrySize: BTreeEntrySize, entryFromRecord: BTreeEntryFromRecord, newRecordFromEntry: BTreeNewRecordFromEntry], storPrim: [referencePage: BTreeVM.ReferencePage, releasePage: BTreeVM.ReleasePage], minEntrySize: BTreeEntry[0].SIZE];
BTree.Open[tree: tree, storage: vmHandle, pageSize: FS.WordsForPages[4], initialize: create];
END;
CloseBTree: PROC =
BEGIN
IF tree#NIL THEN tree.SetState[closed];
IF file#FS.nullOpenFile THEN
BEGIN
pages, bytes: INT;
[pages: pages, bytes: bytes] ← file.GetInfo[];
IF bytes#FS.BytesForPages[pages] THEN
file.SetByteCountAndCreatedTime[bytes: FS.BytesForPages[pages]];
file.Close[];
END;
file ← FS.nullOpenFile;
END;
Find: PROC [name: ROPE] RETURNS [found: BOOLEAN, password: GVBasics.Password, stamp: GVBasics.Timestamp] = TRUSTED
BEGIN
textName: Rope.Text = name.Flatten[];
entry: REF BTreeEntry = NARROW[tree.ReadRecord[key: textName]];
IF (found ← entry#NIL) THEN {password ← entry.password; stamp ← entry.stamp};
END;
Insert: PROC [name: ROPE, password: GVBasics.Password, stamp: GVBasics.Timestamp] = TRUSTED
BEGIN
textName: Rope.Text = name.Flatten[];
entry: REF BTreeEntry = NEW[BTreeEntry[textName.Length[]] ← [password: password, stamp: stamp, name: ]];
PrincOpsUtils.LongCopy[to: BASE[DESCRIPTOR[entry^]], from: BASE[DESCRIPTOR[textName^]], nwords: (textName.Length[]+1)/2];
tree.UpdateRecord[key: textName, record: entry];
END;
BTreeEnumerate: PROC [startingKey: ROPENIL, relation: BTree.Relation ← equal, proc: BTreeEnumProc] RETURNS [exhausted: BOOLEAN] = TRUSTED
BEGIN
EnumEntry: UNSAFE PROC [entry: BTree.Entry] RETURNS [continue: BOOLEAN] =
BEGIN
e: LONG POINTER TO BTreeEntry = NARROW[entry];
t: Rope.Text = Rope.NewText[e.length];
PrincOpsUtils.LongCopy[to: BASE[DESCRIPTOR[t.text]], from: BASE[DESCRIPTOR[e.name]], nwords: (e.length+1)/2];
continue ← proc[name: t, password: e.password, stamp: e.stamp];
END;
textName: Rope.Text = startingKey.Flatten[];
exhausted ← tree.EnumerateEntries[key: textName, relation: relation, Proc: EnumEntry];
END;
BTreeEnumProc: TYPE = PROC [name: ROPE, password: GVBasics.Password, stamp: GVBasics.Timestamp] RETURNS [continue: BOOLEANTRUE];
BTreeCompare: BTree.Compare --[key: Key, entry: Entry] RETURNS [Comparison]-- = UNCHECKED
BEGIN
k: REF TEXT = LOOPHOLE[NARROW[key, Rope.Text]];
e: LONG POINTER TO BTreeEntry = LOOPHOLE[entry];
FOR i: CARDINAL IN [0 .. MIN[k.length, e.length]) DO
kc: CHAR ← Ascii.Lower[k[i]];
ec: CHAR ← Ascii.Lower[e[i]];
IF kc<ec THEN RETURN [less];
IF kc>ec THEN RETURN [greater];
ENDLOOP;
IF k.length<e.length THEN RETURN [less];
IF k.length>e.length THEN RETURN [greater];
RETURN [equal];
END;
BTreeEntrySize: BTree.EntrySize --[entry: Entry] RETURNS [words: EntSize]-- = UNCHECKED
BEGIN
RETURN [BTreeEntry[LOOPHOLE[entry, LONG POINTER TO BTreeEntry].length].SIZE];
END;
BTreeEntryFromRecord: BTree.EntryFromRecord --[record: Record] RETURNS [entry: Entry]-- = UNCHECKED
{ RETURN [LOOPHOLE [record]] };
BTreeNewRecordFromEntry: BTree.NewRecordFromEntry --[entry: Entry] RETURNS [record: Record]-- = UNCHECKED
BEGIN
e: LONG POINTER TO BTreeEntry = LOOPHOLE[entry];
record ← NEW [BTreeEntry[e.length]];
PrincOpsUtils.LongCopy[to: LOOPHOLE[record], from: e, nwords: BTreeEntry[e.length].SIZE];
END;
BTreeEntry: TYPE = MACHINE DEPENDENT RECORD [
password: GVBasics.Password,
stamp: GVBasics.Timestamp, -- most recent password update
name: PACKED SEQUENCE length: CARDINAL OF CHAR];
Initialization
Commander.Register[key: "RememberPasswords", proc: RememberPasswordsCmd, doc: "[-a] filename pattern1 pattern2 ..."];
Commander.Register[key: "CheckPasswords", proc: CheckPasswordsCmd, doc: "filename pattern1 pattern2 ..."];
END.