Java 如何在JNA中映射Windows API CredWrite/CredRead?

Java 如何在JNA中映射Windows API CredWrite/CredRead?,java,winapi,jna,credentials,Java,Winapi,Jna,Credentials,我试图在JNA中映射CredWrite/CredArder,以便在Windows凭据管理器(OS Windows 10)中存储Java应用程序中使用的第三方凭据 以下是C语言的原始签名: // https://msdn.microsoft.com/en-us/library/aa375187(v=vs.85).aspx BOOL CredWrite( _In_ PCREDENTIAL Credential, _In_ DWORD Flags ); // https://ms

我试图在JNA中映射CredWrite/CredArder,以便在Windows凭据管理器(OS Windows 10)中存储Java应用程序中使用的第三方凭据

以下是C语言的原始签名:

// https://msdn.microsoft.com/en-us/library/aa375187(v=vs.85).aspx
BOOL CredWrite(
  _In_ PCREDENTIAL Credential,
  _In_ DWORD       Flags
);

// https://msdn.microsoft.com/en-us/library/aa374804(v=vs.85).aspx
BOOL CredRead(
  _In_  LPCTSTR     TargetName,
  _In_  DWORD       Type,
  _In_  DWORD       Flags,
  _Out_ PCREDENTIAL *Credential
);

typedef struct _CREDENTIAL {
  DWORD                 Flags;
  DWORD                 Type;
  LPTSTR                TargetName;
  LPTSTR                Comment;
  FILETIME              LastWritten;
  DWORD                 CredentialBlobSize;
  LPBYTE                CredentialBlob;
  DWORD                 Persist;
  DWORD                 AttributeCount;
  PCREDENTIAL_ATTRIBUTE Attributes;
  LPTSTR                TargetAlias;
  LPTSTR                UserName;
} CREDENTIAL, *PCREDENTIAL;

typedef struct _CREDENTIAL_ATTRIBUTE {
  LPTSTR Keyword;
  DWORD  Flags;
  DWORD  ValueSize;
  LPBYTE Value;
} CREDENTIAL_ATTRIBUTE, *PCREDENTIAL_ATTRIBUTE;
以下是我的Java地图:

WinCrypt instance = (WinCrypt) Native.loadLibrary("Advapi32", WinCrypt.class, W32APIOptions.DEFAULT_OPTIONS);

public boolean CredWrite(
        CREDENTIAL.ByReference Credential,
        int Flags
        );

public boolean CredRead(
        String TargetName,
        int Type,
        int Flags,
        PointerByReference Credential
        );

public static class CREDENTIAL extends Structure {
    public int Flags;
    public int Type;
    public String TargetName;
    public String Comment;
    public FILETIME LastWritten;
    public int CredentialBlobSize;
    public byte[] CredentialBlob = new byte[128];
    public int Persist;
    public int AttributeCount;
    public CREDENTIAL_ATTRIBUTE.ByReference Attributes;
    public String TargetAlias;
    public String UserName;

    public static class ByReference extends CREDENTIAL implements Structure.ByReference {
        public ByReference() {
        }

        public ByReference(Pointer memory) {
            super(memory);                      // LINE 55
        }
    }

    public CREDENTIAL() {
        super();
    }

    public CREDENTIAL(Pointer memory) {
        super(memory); 
        read();                                 // LINE 65
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList(new String[] {
                "Flags",
                "Type",
                "TargetName",
                "Comment",
                "LastWritten",
                "CredentialBlobSize",
                "CredentialBlob",
                "Persist",
                "AttributeCount",
                "Attributes",
                "TargetAlias",
                "UserName"
        });
    }
}

public static class CREDENTIAL_ATTRIBUTE extends Structure {
    public String Keyword;
    public int Flags;
    public int ValueSize;
    public byte[] Value = new byte[128];

    public static class ByReference extends CREDENTIAL_ATTRIBUTE implements Structure.ByReference {
    }

    @Override
    protected List<String> getFieldOrder() {
        return Arrays.asList(new String[] {
                "Keyword",
                "Flags",
                "ValueSize",
                "Value"
        });
    }
}
“尝试写入”的输出:

CredWrite() - ok: false, errno: 87, errmsg: The parameter is incorrect.
然后我尝试从Windows凭据管理器读取现有凭据:

String password = "passwordtest";
int cbCreds = 1 + password.length();

CREDENTIAL.ByReference credRef = new CREDENTIAL.ByReference();
credRef.Type = WinCrypt.CRED_TYPE_GENERIC;
credRef.TargetName = "TEST/account";
credRef.CredentialBlobSize = cbCreds;
credRef.CredentialBlob = password.getBytes();
credRef.Persist = WinCrypt.CRED_PERSIST_LOCAL_MACHINE;
credRef.UserName = "administrator";

boolean ok = WinCrypt.instance.CredWrite(credRef, 0);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredWrite() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);
PointerByReference pref = new PointerByReference();
boolean ok = WinCrypt.instance.CredRead("build-apps", WinCrypt.CRED_TYPE_DOMAIN_PASSWORD, 0, pref);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredRead() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);
CREDENTIAL cred = new CREDENTIAL.ByReference(pref.getPointer());        // LINE 44
“尝试读取”的输出:

CredRead() - ok: true, errno: 0, errmsg: The operation completed successfully.
Exception in thread "main" java.lang.IllegalArgumentException: Structure exceeds provided memory bounds
    at com.sun.jna.Structure.ensureAllocated(Structure.java:366)
    at com.sun.jna.Structure.ensureAllocated(Structure.java:346)
    at com.sun.jna.Structure.read(Structure.java:552)
    at com.abc.crypt.WinCrypt$CREDENTIAL.<init>(WinCrypt.java:65)
    at com.abc.crypt.WinCrypt$CREDENTIAL$ByReference.<init>(WinCrypt.java:55) 
    at com.abc.crypt.CryptTest.main(CryptTest.java:44)
Caused by: java.lang.IndexOutOfBoundsException: Bounds exceeds available space : size=8, offset=200
    at com.sun.jna.Memory.boundsCheck(Memory.java:203)
    at com.sun.jna.Memory$SharedMemory.boundsCheck(Memory.java:87)
    at com.sun.jna.Memory.share(Memory.java:131)
    at com.sun.jna.Structure.ensureAllocated(Structure.java:363)
    ... 5 more
CREWARD测试:

public boolean CredRead(
        String TargetName,
        int Type,
        int Flags,
        CREDENTIAL.ByReference Credential
        );
CREDENTIAL.ByReference pref = new CREDENTIAL.ByReference();
boolean ok = WinCrypt.instance.CredRead("TEST/account", WinCrypt.CRED_TYPE_GENERIC, 0, pref);
int rc = Kernel32.INSTANCE.GetLastError();
String errMsg = Kernel32Util.formatMessage(rc);
System.out.println("CredRead() - ok: " + ok + ", errno: " + rc + ", errmsg: " + errMsg);
System.out.println(String.format("Read username = '%s', password='%S' (%d bytes)\n",
        pref.UserName, pref.CredentialBlob, pref.CredentialBlobSize));
结果:

CredRead() - ok: true, errno: 0, errmsg: The operation completed successfully.
Read username = 'null', password='NULL' (0 bytes)

我检查了contrib中的JNA示例是如何在out arg上使用ByReference的,并且它们以相同的方式更新ByReference并传递给函数。

CredRead.PCREDENTIAL
应该是
凭证。ByReference
。使用
PointerByReference
最终会传入一个指向空值的指针,而不是指向
CREDENTIAL
struct的预期指针

CREDENTAL.CredentialBlob
需要是
指针
指针类型
(如果您自己初始化块,可能是
内存
)。使用内联字节数组按数组大小移动整个结构,其中被调用方希望有一个指向内存块的指针

更新

我想我误读了
credear()
的声明

creaward
应继续使用
PointerByReference
。使用
PointerByReference.getValue()
CredRead()
中提取“返回的”指针值,以便基于指针创建新的
凭据
实例
PointerByReference.getPointer()
提供分配用于保存指针值的内存地址

public boolean CredWrite(
    CREDENTIAL Credential,
    int Flags
    );

public boolean CredRead(
    String TargetName,
    int Type,
    int Flags,
    PointerByReference pref
    );

PointerByReference pref = new PointerByReference()
CredRead(name, type, flags, pref);
creds = new Credentials(pref.getValue())

如果查看
CredRead()
的WIN32定义,第四个参数的类型为PCREDENTIAL*,即它是指向指针的指针。所以

  • 您需要传入指针地址,即4字节内存块
  • Windows分配一个内存块来保存凭证结构,然后通过将新内存块的地址放在传入的4字节块中来告诉您凭证结构的位置
  • 当您取消引用原始指针(您传递到
    CredRead()
    )的指针)时,您会得到另一个指针(4字节块),它本身需要取消引用才能访问凭据
欢迎来到C:-)

TL;DR:凭证类需要这样定义:

public static class CREDENTIAL extends Structure {
    public int Flags;
    public int Type;
    public WString TargetName;
    public WString Comment;
    public FILETIME LastWritten;
    public int CredentialBlobSize;
    public Pointer CredentialBlob; // <== discussed below
    public int Persist;
    public int AttributeCount;
    public Pointer Attributes;
    public WString TargetAlias;
    public WString UserName;
    private Pointer RawMemBlock; // <== discussed below

    public CREDENTIAL() { }

    public CREDENTIAL( Pointer ptr ) 
    { 
        // initialize ourself from the raw memory block returned to us by ADVAPI32
        super( ptr ) ; 
        RawMemBlock = ptr ; 
        read() ;
    }

    @Override
    protected void finalize()
    {    
        // clean up
        WinCrypt.INSTANCE.CredFree( RawMemBlock ) ;
    }

    @Override
    protected List<String> getFieldOrder()
    {
        return Arrays.asList( new String[] { "Flags" , "Type" , "TargetName" , "Comment" , "LastWritten" , "CredentialBlobSize" , "CredentialBlob" , "Persist" , "AttributeCount" , "Attributes" , "TargetAlias" , "UserName" } ) ;
    }
} ;
public boolean CredRead( String target , int type , int flags , PointerByReference cred ) ;
PointerByReference pptr = new PointerByReference() ;
boolean rc = WinCrypt.INSTANCE.CredRead( target , credType , 0 , pptr ) ;
if ( ! rc )
    ... ; // handle the error
CREDENTIAL cred = new CREDENTIAL( pptr.getValue() ) ;
String userName = cred.UserName.toString() ;
String password = new String( cred.CredentialBlob.getByteArray(0,cred.CredentialBlobSize) , "UTF-16LE" ) ;
public void CredFree( Pointer cred ) ;
然后像这样调用它:

public static class CREDENTIAL extends Structure {
    public int Flags;
    public int Type;
    public WString TargetName;
    public WString Comment;
    public FILETIME LastWritten;
    public int CredentialBlobSize;
    public Pointer CredentialBlob; // <== discussed below
    public int Persist;
    public int AttributeCount;
    public Pointer Attributes;
    public WString TargetAlias;
    public WString UserName;
    private Pointer RawMemBlock; // <== discussed below

    public CREDENTIAL() { }

    public CREDENTIAL( Pointer ptr ) 
    { 
        // initialize ourself from the raw memory block returned to us by ADVAPI32
        super( ptr ) ; 
        RawMemBlock = ptr ; 
        read() ;
    }

    @Override
    protected void finalize()
    {    
        // clean up
        WinCrypt.INSTANCE.CredFree( RawMemBlock ) ;
    }

    @Override
    protected List<String> getFieldOrder()
    {
        return Arrays.asList( new String[] { "Flags" , "Type" , "TargetName" , "Comment" , "LastWritten" , "CredentialBlobSize" , "CredentialBlob" , "Persist" , "AttributeCount" , "Attributes" , "TargetAlias" , "UserName" } ) ;
    }
} ;
public boolean CredRead( String target , int type , int flags , PointerByReference cred ) ;
PointerByReference pptr = new PointerByReference() ;
boolean rc = WinCrypt.INSTANCE.CredRead( target , credType , 0 , pptr ) ;
if ( ! rc )
    ... ; // handle the error
CREDENTIAL cred = new CREDENTIAL( pptr.getValue() ) ;
String userName = cred.UserName.toString() ;
String password = new String( cred.CredentialBlob.getByteArray(0,cred.CredentialBlobSize) , "UTF-16LE" ) ;
public void CredFree( Pointer cred ) ;
凭据blob是Windows分配的另一个内存块,因此您不需要自己分配它,Windows会这样做,并通过将其地址放入CredentialBlob字段来告诉您它在哪里

由于Windows已经为您分配了这些内存块,并且由于它无法知道您何时将完成这些内存块,因此您有责任释放它们。因此,凭证构造函数保留原始指针
CredRead()
的副本,并在终结器中调用
CredFree()
,以释放该内存
CredFree()
声明如下:

public static class CREDENTIAL extends Structure {
    public int Flags;
    public int Type;
    public WString TargetName;
    public WString Comment;
    public FILETIME LastWritten;
    public int CredentialBlobSize;
    public Pointer CredentialBlob; // <== discussed below
    public int Persist;
    public int AttributeCount;
    public Pointer Attributes;
    public WString TargetAlias;
    public WString UserName;
    private Pointer RawMemBlock; // <== discussed below

    public CREDENTIAL() { }

    public CREDENTIAL( Pointer ptr ) 
    { 
        // initialize ourself from the raw memory block returned to us by ADVAPI32
        super( ptr ) ; 
        RawMemBlock = ptr ; 
        read() ;
    }

    @Override
    protected void finalize()
    {    
        // clean up
        WinCrypt.INSTANCE.CredFree( RawMemBlock ) ;
    }

    @Override
    protected List<String> getFieldOrder()
    {
        return Arrays.asList( new String[] { "Flags" , "Type" , "TargetName" , "Comment" , "LastWritten" , "CredentialBlobSize" , "CredentialBlob" , "Persist" , "AttributeCount" , "Attributes" , "TargetAlias" , "UserName" } ) ;
    }
} ;
public boolean CredRead( String target , int type , int flags , PointerByReference cred ) ;
PointerByReference pptr = new PointerByReference() ;
boolean rc = WinCrypt.INSTANCE.CredRead( target , credType , 0 , pptr ) ;
if ( ! rc )
    ... ; // handle the error
CREDENTIAL cred = new CREDENTIAL( pptr.getValue() ) ;
String userName = cred.UserName.toString() ;
String password = new String( cred.CredentialBlob.getByteArray(0,cred.CredentialBlobSize) , "UTF-16LE" ) ;
public void CredFree( Pointer cred ) ;
要保存凭据,您需要按照
CredWrite()
所期望的方式准备凭据blob,即在credential.CredentialBlob字段中存储指向它的指针:

// prepare the credential blob
byte[] credBlob = password.getBytes( "UTF-16LE" ) ;
Memory credBlobMem = new Memory( credBlob.length ) ;
credBlobMem.write( 0 , credBlob , 0 , credBlob.length ) ;

// create the credential
CREDENTIAL cred = new CREDENTIAL() ;
cred.Type = CRED_TYPE_GENERIC ;
cred.TargetName = new WString( target ) ;
cred.CredentialBlobSize = (int) credBlobMem.size() ;
cred.CredentialBlob = credBlobMem ;
cred.Persist = CRED_PERSIST_LOCAL_MACHINE ;
cred.UserName = new WString( userName ) ;

// save the credential
boolean rc = WinCrypt.INSTANCE.CredWrite( cred , 0 ) ;
if ( ! rc )
    ... ; // handle the error
作为补充,如果在服务帐户或任何其他没有永久配置文件的帐户下运行,所有这些都会遇到问题。我需要使用一个没有交互登录权限的服务帐户,通过任务调度器运行一个作业,这样做的结果是:

  • 我创建了一个设置密码的批处理文件,并通过TaskScheduler运行它(这样它就可以在服务帐户下运行,密码就会进入正确的存储)
  • Windows创建一个临时配置文件(检查事件日志)并将密码输入其中
  • 另一个转储密码的批处理文件显示密码已成功设置
  • 运行主作业有效,因为临时配置文件仍然存在,但在5或10分钟后,Windows会删除它,包括您设置的密码:-/,因此下次运行主作业时,它会失败,因为密码不再存在

解决方案是创建一个永久的配置文件,最好是通过交互方式登录,只需执行一次。如果您不能做到这一点,尽管您需要管理员权限,但也可以做到这一点。

Microsoft提供了麻省理工学院许可的Java库,用于访问VSTS令牌。

它们提供了到凭证管理器功能和用法的JNA映射:


如果您是从零开始的,这将非常有用。

基于taka的回答,但考虑到以下其他因素,我实现了一个完整的示例

已考虑了以下其他更正和方面:

  • 在CredArdw中,targetName的类型必须为WString,而不是String。使用CredWriteW将数据写入windows vauld时,也已使用WString
  • 简化:我没有使用getFieldOrder()方法,而是使用了注释样式
  • 不建议直接读取Kernel32 GetLastError,因为JNA可能会调用删除前一个LastError的其他调用。如中所述,我改为捕获最后一个错误作为异常
  • 如taka所示,不需要在credentialBlobSize中添加“1+”,但更重要的是使用实际内存或字节[]大小,因为UTF-8编码导致的字符串长度包含的字符数少于Windows API函数UTF-16LE编码产生的内存数
全样本: (注意,它需要JNA库;我使用的是JNA版本5.6.0,可在上获得)

包装在.christoph-bimminger.sample;
导入java.io.UnsupportedEncodingException;
导入java.util.array;
导入java.util.List;
导入com.sun.jna.LastErrorException;
导入com.sun.jna.Library;
导入com.sun.jna.Memory;
导入com.sun.jna.Native;
接口信息处理器